diff --git a/src/body_writer.rs b/src/body_writer/buffering_chunked.rs similarity index 79% rename from src/body_writer.rs rename to src/body_writer/buffering_chunked.rs index 6385139..6100f49 100644 --- a/src/body_writer.rs +++ b/src/body_writer/buffering_chunked.rs @@ -1,115 +1,10 @@ -use core::mem::size_of; - use embedded_io::{Error as _, ErrorType}; use embedded_io_async::Write; -const NEWLINE: &[u8; 2] = b"\r\n"; -const EMPTY_CHUNK: &[u8; 5] = b"0\r\n\r\n"; - -pub struct FixedBodyWriter(C, usize); - -impl FixedBodyWriter -where - C: Write, -{ - pub fn new(conn: C) -> Self { - Self(conn, 0) - } - - pub fn written(&self) -> usize { - self.1 - } -} - -impl ErrorType for FixedBodyWriter -where - C: Write, -{ - type Error = C::Error; -} - -impl Write for FixedBodyWriter -where - C: Write, -{ - async fn write(&mut self, buf: &[u8]) -> Result { - let written = self.0.write(buf).await?; - self.1 += written; - Ok(written) - } - - async fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { - self.0.write_all(buf).await?; - self.1 += buf.len(); - Ok(()) - } - - async fn flush(&mut self) -> Result<(), Self::Error> { - self.0.flush().await - } -} - -pub struct ChunkedBodyWriter(C); - -impl ChunkedBodyWriter -where - C: Write, -{ - pub fn new(conn: C) -> Self { - Self(conn) - } - - /// Terminate the request body by writing an empty chunk - pub async fn terminate(&mut self) -> Result<(), C::Error> { - self.0.write_all(EMPTY_CHUNK).await - } -} - -impl ErrorType for ChunkedBodyWriter -where - C: Write, -{ - type Error = embedded_io::ErrorKind; -} - -impl Write for ChunkedBodyWriter -where - C: Write, -{ - async fn write(&mut self, buf: &[u8]) -> Result { - self.write_all(buf).await.map_err(|e| e.kind())?; - Ok(buf.len()) - } - - async fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { - let len = buf.len(); - - // Do not write an empty chunk as that will terminate the body - // Use `ChunkedBodyWriter.write_empty_chunk` instead if this is intended - if len == 0 { - return Ok(()); - } +use super::chunked::write_chunked_header; - // Write chunk header - let mut header_buf = [0; 2 * size_of::() + 2]; - let header_len = write_chunked_header(&mut header_buf, len); - self.0 - .write_all(&header_buf[..header_len]) - .await - .map_err(|e| e.kind())?; - - // Write chunk - self.0.write_all(buf).await.map_err(|e| e.kind())?; - - // Write newline footer - self.0.write_all(NEWLINE).await.map_err(|e| e.kind())?; - Ok(()) - } - - async fn flush(&mut self) -> Result<(), Self::Error> { - self.0.flush().await.map_err(|e| e.kind()) - } -} +const EMPTY_CHUNK: &[u8; 5] = b"0\r\n\r\n"; +const NEWLINE: &[u8; 2] = b"\r\n"; /// A body writer that buffers internally and emits chunks as expected by the /// `Transfer-Encoding: chunked` header specification. @@ -294,16 +189,6 @@ const fn get_max_chunk_header_size(buffer_size: usize) -> usize { } } -fn write_chunked_header(buf: &mut [u8], chunk_len: usize) -> usize { - let mut hex = [0; 2 * size_of::()]; - hex::encode_to_slice(chunk_len.to_be_bytes(), &mut hex).unwrap(); - let leading_zeros = hex.iter().position(|x| *x != b'0').unwrap_or(hex.len() - 1); - let hex_chars = hex.len() - leading_zeros; - buf[..hex_chars].copy_from_slice(&hex[leading_zeros..]); - buf[hex_chars..hex_chars + NEWLINE.len()].copy_from_slice(NEWLINE); - hex_chars + 2 -} - #[cfg(test)] mod tests { use super::*; @@ -332,23 +217,6 @@ mod tests { assert_eq!(4, get_max_chunk_header_size(0x12 + 2 + 2)); } - #[test] - fn can_write_chunked_header() { - let mut buf = [0; 4]; - - let len = write_chunked_header(&mut buf, 0x00); - assert_eq!(b"0\r\n", &buf[..len]); - - let len = write_chunked_header(&mut buf, 0x01); - assert_eq!(b"1\r\n", &buf[..len]); - - let len = write_chunked_header(&mut buf, 0x0F); - assert_eq!(b"f\r\n", &buf[..len]); - - let len = write_chunked_header(&mut buf, 0x10); - assert_eq!(b"10\r\n", &buf[..len]); - } - #[tokio::test] async fn preserves_already_written_bytes_in_the_buffer_without_any_chunks() { // Given diff --git a/src/body_writer/chunked.rs b/src/body_writer/chunked.rs new file mode 100644 index 0000000..678632a --- /dev/null +++ b/src/body_writer/chunked.rs @@ -0,0 +1,101 @@ +use core::mem::size_of; + +use embedded_io::{Error, ErrorType}; +use embedded_io_async::Write; + +pub struct ChunkedBodyWriter(C); + +const EMPTY_CHUNK: &[u8; 5] = b"0\r\n\r\n"; +const NEWLINE: &[u8; 2] = b"\r\n"; + +impl ChunkedBodyWriter +where + C: Write, +{ + pub fn new(conn: C) -> Self { + Self(conn) + } + + /// Terminate the request body by writing an empty chunk + pub async fn terminate(&mut self) -> Result<(), C::Error> { + self.0.write_all(EMPTY_CHUNK).await + } +} + +impl ErrorType for ChunkedBodyWriter +where + C: Write, +{ + type Error = embedded_io::ErrorKind; +} + +impl Write for ChunkedBodyWriter +where + C: Write, +{ + async fn write(&mut self, buf: &[u8]) -> Result { + self.write_all(buf).await.map_err(|e| e.kind())?; + Ok(buf.len()) + } + + async fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + let len = buf.len(); + + // Do not write an empty chunk as that will terminate the body + // Use `ChunkedBodyWriter.write_empty_chunk` instead if this is intended + if len == 0 { + return Ok(()); + } + + // Write chunk header + let mut header_buf = [0; 2 * size_of::() + 2]; + let header_len = write_chunked_header(&mut header_buf, len); + self.0 + .write_all(&header_buf[..header_len]) + .await + .map_err(|e| e.kind())?; + + // Write chunk + self.0.write_all(buf).await.map_err(|e| e.kind())?; + + // Write newline footer + self.0.write_all(NEWLINE).await.map_err(|e| e.kind())?; + Ok(()) + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.0.flush().await.map_err(|e| e.kind()) + } +} + +pub(super) fn write_chunked_header(buf: &mut [u8], chunk_len: usize) -> usize { + let mut hex = [0; 2 * size_of::()]; + hex::encode_to_slice(chunk_len.to_be_bytes(), &mut hex).unwrap(); + let leading_zeros = hex.iter().position(|x| *x != b'0').unwrap_or(hex.len() - 1); + let hex_chars = hex.len() - leading_zeros; + buf[..hex_chars].copy_from_slice(&hex[leading_zeros..]); + buf[hex_chars..hex_chars + NEWLINE.len()].copy_from_slice(NEWLINE); + hex_chars + 2 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_write_chunked_header() { + let mut buf = [0; 4]; + + let len = write_chunked_header(&mut buf, 0x00); + assert_eq!(b"0\r\n", &buf[..len]); + + let len = write_chunked_header(&mut buf, 0x01); + assert_eq!(b"1\r\n", &buf[..len]); + + let len = write_chunked_header(&mut buf, 0x0F); + assert_eq!(b"f\r\n", &buf[..len]); + + let len = write_chunked_header(&mut buf, 0x10); + assert_eq!(b"10\r\n", &buf[..len]); + } +} diff --git a/src/body_writer/fixed.rs b/src/body_writer/fixed.rs new file mode 100644 index 0000000..20363a5 --- /dev/null +++ b/src/body_writer/fixed.rs @@ -0,0 +1,45 @@ +use embedded_io::ErrorType; +use embedded_io_async::Write; + +pub struct FixedBodyWriter(C, usize); + +impl FixedBodyWriter +where + C: Write, +{ + pub fn new(conn: C) -> Self { + Self(conn, 0) + } + + pub fn written(&self) -> usize { + self.1 + } +} + +impl ErrorType for FixedBodyWriter +where + C: Write, +{ + type Error = C::Error; +} + +impl Write for FixedBodyWriter +where + C: Write, +{ + async fn write(&mut self, buf: &[u8]) -> Result { + let written = self.0.write(buf).await?; + self.1 += written; + Ok(written) + } + + async fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + self.0.write_all(buf).await?; + self.1 += buf.len(); + Ok(()) + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.0.flush().await + } +} diff --git a/src/body_writer/mod.rs b/src/body_writer/mod.rs new file mode 100644 index 0000000..dbfee63 --- /dev/null +++ b/src/body_writer/mod.rs @@ -0,0 +1,7 @@ +mod buffering_chunked; +mod chunked; +mod fixed; + +pub use buffering_chunked::BufferingChunkedBodyWriter; +pub use chunked::ChunkedBodyWriter; +pub use fixed::FixedBodyWriter; diff --git a/src/client.rs b/src/client.rs index 5f5cc98..051e725 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,6 @@ -use crate::body_writer::BufferingChunkedBodyWriter; -use crate::body_writer::ChunkedBodyWriter; -use crate::body_writer::FixedBodyWriter; /// Client using embedded-nal-async traits to establish connections and perform HTTP requests. /// +use crate::body_writer::{BufferingChunkedBodyWriter, ChunkedBodyWriter, FixedBodyWriter}; use crate::headers::ContentType; use crate::request::*; use crate::response::*;