diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f024e26..b0a9092 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,16 +1,56 @@ -name: Test +name: CI on: - push: - branches: [master, develop] pull_request: - branches: [master, develop] + push: + branches: + - master + - develop + +env: + RUSTFLAGS: -Dwarnings jobs: - build: - runs-on: ubuntu-latest + build_and_test: + name: Build and test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + rust: [stable, nightly] steps: - - uses: actions/checkout@v2 - - name: Run tests - run: cargo test --verbose --features="all" + - uses: actions/checkout@master + + - name: Install ${{ matrix.rust }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + + - name: check + uses: actions-rs/cargo@v1 + with: + command: check + args: --all --bins --examples --features=all + + - name: tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all --features=all + + check_fmt_and_docs: + name: Checking fmt, clippy, and docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + + - name: clippy + run: cargo clippy --tests --examples --bins -- -D warnings + + - name: fmt + run: cargo fmt --all -- --check + + - name: Docs + run: cargo doc --no-deps diff --git a/Cargo.toml b/Cargo.toml index 15a4b21..9e551fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,12 +47,7 @@ tokio = { version = "0.2", features = ["full"] } hyper = "0.13" routerify = "1.1" -[[example]] -name = "test" -path = "examples/test.rs" -required-features = ["json", "reader"] - [[example]] name = "parse_async_read" path = "examples/parse_async_read.rs" -required-features = ["reader"] \ No newline at end of file +required-features = ["reader"] diff --git a/examples/test.rs b/examples/test.rs deleted file mode 100644 index 8b1893d..0000000 --- a/examples/test.rs +++ /dev/null @@ -1,42 +0,0 @@ -use bytes::Bytes; -use futures::stream::{Stream, StreamExt}; -use futures::TryStreamExt; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Request, Response, Server}; -use multer::{Constraints, Error, Field, Multipart, SizeLimit}; -use std::{convert::Infallible, net::SocketAddr}; -use tokio::fs::{File, OpenOptions}; -use tokio::io::{AsyncWrite, AsyncWriteExt}; - -async fn handle(req: Request) -> Result, Infallible> { - let stream = req.into_body(); - - // let multipart_constraints = Constraints::new() - // .allowed_fields(vec!["a", "b"]) - // .size_limit(SizeLimit::new().per_field(30).for_field("a", 10)); - - let mut multipart = Multipart::new(stream, "X-INSOMNIA-BOUNDARY"); - - while let Some(field) = multipart.next_field().await.unwrap() { - println!("name: {:?}", field.name()); - println!("filename: {:?}", field.file_name()); - let text = field.text().await.unwrap(); - println!("content: {}", text); - } - - Ok(Response::new("Hello, World!".into())) -} - -#[tokio::main] -async fn main() { - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - - let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handle)) }); - - let server = Server::bind(&addr).serve(make_svc); - - println!("Server is running at: {}", addr); - if let Err(e) = server.await { - eprintln!("server error: {}", e); - } -} diff --git a/src/buffer.rs b/src/buffer.rs index cfa41b4..b83b1dd 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,6 +1,7 @@ use crate::constants; use bytes::{Bytes, BytesMut}; use futures::stream::Stream; +use std::fmt; use std::pin::Pin; use std::task::{Context, Poll}; @@ -26,7 +27,7 @@ impl StreamBuffer { } } - pub fn poll_stream(&mut self, cx: &mut Context) -> Result<(), crate::Error> { + pub fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), crate::Error> { if self.eof { return Ok(()); } @@ -116,12 +117,10 @@ impl StreamBuffer { Err(crate::Error::IncompleteFieldData { field_name: field_name.map(|s| s.to_owned()), }) + } else if bytes.is_empty() { + Ok(None) } else { - if bytes.is_empty() { - Ok(None) - } else { - Ok(Some((false, bytes))) - } + Ok(Some((false, bytes))) } } None => { @@ -153,3 +152,9 @@ impl StreamBuffer { self.buf.split_to(self.buf.len()).freeze() } } + +impl fmt::Debug for StreamBuffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("StreamBuffer").finish() + } +} diff --git a/src/constants.rs b/src/constants.rs index 34d278e..f09aa14 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -5,12 +5,12 @@ pub(crate) const DEFAULT_WHOLE_STREAM_SIZE_LIMIT: u64 = std::u64::MAX; pub(crate) const DEFAULT_PER_FIELD_SIZE_LIMIT: u64 = std::u64::MAX; pub(crate) const MAX_HEADERS: usize = 32; -pub(crate) const BOUNDARY_EXT: &'static str = "--"; -pub(crate) const CR: &'static str = "\r"; +pub(crate) const BOUNDARY_EXT: &str = "--"; +pub(crate) const CR: &str = "\r"; #[allow(dead_code)] -pub(crate) const LF: &'static str = "\n"; -pub(crate) const CRLF: &'static str = "\r\n"; -pub(crate) const CRLF_CRLF: &'static str = "\r\n\r\n"; +pub(crate) const LF: &str = "\n"; +pub(crate) const CRLF: &str = "\r\n"; +pub(crate) const CRLF_CRLF: &str = "\r\n\r\n"; lazy_static! { pub(crate) static ref CONTENT_DISPOSITION_FIELD_NAME_RE: Regex = Regex::new(r#"(?-u)name="([^"]+)""#).unwrap(); diff --git a/src/constraints.rs b/src/constraints.rs index 29e5c09..fde59a1 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -42,6 +42,7 @@ use crate::size_limit::SizeLimit; /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` +#[derive(Debug)] pub struct Constraints { pub(crate) size_limit: SizeLimit, pub(crate) allowed_fields: Option>, diff --git a/src/content_disposition.rs b/src/content_disposition.rs index a185358..7d2fe04 100644 --- a/src/content_disposition.rs +++ b/src/content_disposition.rs @@ -1,6 +1,7 @@ use crate::constants; use http::header::{self, HeaderMap}; +#[derive(Debug)] pub(crate) struct ContentDisposition { pub(crate) field_name: Option, pub(crate) file_name: Option, diff --git a/src/error.rs b/src/error.rs index 22a85d5..8058836 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,7 @@ type BoxError = Box; /// A set of errors that can occur during parsing multipart stream and in other operations. #[derive(Display)] #[display(fmt = "multer: {}")] +#[non_exhaustive] pub enum Error { /// An unknown field is detected when multipart [`constraints`](./struct.Constraints.html#method.allowed_fields) are added. #[display( @@ -77,9 +78,6 @@ pub enum Error { #[cfg(feature = "json")] #[display(fmt = "Failed to decode the field data as JSON: {}", _0)] DecodeJson(BoxError), - - #[doc(hidden)] - __Nonexhaustive, } impl Debug for Error { diff --git a/src/field.rs b/src/field.rs index 5cc6723..fbe4fca 100644 --- a/src/field.rs +++ b/src/field.rs @@ -7,8 +7,6 @@ use futures::stream::{Stream, TryStreamExt}; use http::header::HeaderMap; #[cfg(feature = "json")] use serde::de::DeserializeOwned; -#[cfg(feature = "json")] -use serde_json; use std::borrow::Cow; use std::ops::DerefMut; use std::pin::Pin; @@ -50,6 +48,7 @@ use std::task::{Context, Poll}; /// then the parent [`Multipart`](./struct.Multipart.html) will never be able to yield the next field in the stream. /// The task waiting on the [`Multipart`](./struct.Multipart.html) will also never be notified, which, depending on the executor implementation, /// may cause a deadlock. +#[derive(Debug)] pub struct Field { state: Arc>, headers: HeaderMap, @@ -57,6 +56,7 @@ pub struct Field { meta: FieldMeta, } +#[derive(Debug)] struct FieldMeta { content_disposition: ContentDisposition, content_type: Option, @@ -86,20 +86,12 @@ impl Field { /// The field name found in the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header. pub fn name(&self) -> Option<&str> { - self.meta - .content_disposition - .field_name - .as_ref() - .map(|name| name.as_str()) + self.meta.content_disposition.field_name.as_deref() } /// The file name found in the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header. pub fn file_name(&self) -> Option<&str> { - self.meta - .content_disposition - .file_name - .as_ref() - .map(|file_name| file_name.as_str()) + self.meta.content_disposition.file_name.as_deref() } /// Get the content type of the field. @@ -238,7 +230,7 @@ impl Field { /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// - /// while let Some(mut field) = multipart.next_field().await.unwrap() { + /// while let Some(field) = multipart.next_field().await.unwrap() { /// let content = field.text().await.unwrap(); /// assert_eq!(content, "abcd"); /// } @@ -268,7 +260,7 @@ impl Field { /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// - /// while let Some(mut field) = multipart.next_field().await.unwrap() { + /// while let Some(field) = multipart.next_field().await.unwrap() { /// let content = field.text_with_charset("utf-8").await.unwrap(); /// assert_eq!(content, "abcd"); /// } @@ -324,7 +316,7 @@ impl Field { impl Stream for Field { type Item = Result; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if self.done { return Poll::Ready(None); } @@ -391,7 +383,7 @@ impl Drop for Field { state.is_prev_field_consumed = true; if let Some(waker) = state.next_field_waker.take() { - waker.clone().wake(); + waker.wake(); } } } diff --git a/src/helpers.rs b/src/helpers.rs index 485c2bd..e4fc286 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -2,7 +2,7 @@ use http::header::{self, HeaderMap, HeaderName, HeaderValue}; use httparse::Header; use std::convert::TryFrom; -pub(crate) fn convert_raw_headers_to_header_map(raw_headers: &[Header]) -> crate::Result { +pub(crate) fn convert_raw_headers_to_header_map(raw_headers: &[Header<'_>]) -> crate::Result { let mut headers = HeaderMap::with_capacity(raw_headers.len()); for raw_header in raw_headers { diff --git a/src/lib.rs b/src/lib.rs index e5c9157..1cafa93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,8 +99,17 @@ //! //! For more examples, please visit [examples](https://github.com/rousan/multer-rs/tree/master/examples). -pub use bytes; +#![forbid(unsafe_code, future_incompatible)] +#![warn( + missing_debug_implementations, + rust_2018_idioms, + trivial_casts, + unused_qualifications +)] +#![doc(test(attr(deny(rust_2018_idioms, warnings))))] +#![doc(test(attr(allow(unused_extern_crates, unused_variables))))] +pub use bytes; pub use constraints::Constraints; pub use error::Error; pub use field::Field; @@ -133,7 +142,7 @@ pub type Result = std::result::Result; /// # } /// # run(); /// ``` -pub fn parse_boundary>(content_type: T) -> crate::Result { +pub fn parse_boundary>(content_type: T) -> Result { let m = content_type .as_ref() .parse::() @@ -145,7 +154,7 @@ pub fn parse_boundary>(content_type: T) -> crate::Result { m.get_param(mime::BOUNDARY) .map(|name| name.as_str().to_owned()) - .ok_or_else(|| crate::Error::NoBoundary) + .ok_or(crate::Error::NoBoundary) } #[cfg(test)] diff --git a/src/multipart.rs b/src/multipart.rs index 8eaf50a..767d01c 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -46,6 +46,7 @@ use tokio_util::codec::{BytesCodec, FramedRead}; /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); /// ``` +#[derive(Debug)] pub struct Multipart { state: Arc>, constraints: Constraints, @@ -124,9 +125,6 @@ impl Multipart { /// /// ``` /// use multer::Multipart; - /// use bytes::Bytes; - /// use std::convert::Infallible; - /// use futures::stream::once; /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; @@ -161,9 +159,6 @@ impl Multipart { /// /// ``` /// use multer::Multipart; - /// use bytes::Bytes; - /// use std::convert::Infallible; - /// use futures::stream::once; /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; @@ -202,10 +197,9 @@ impl Multipart { /// # Examples /// /// ``` + /// # #[cfg(feature = "reader")] + /// # { /// use multer::Multipart; - /// use bytes::Bytes; - /// use std::convert::Infallible; - /// use futures::stream::once; /// /// # async fn run() { /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; @@ -217,6 +211,7 @@ impl Multipart { /// } /// # } /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); + /// # } /// ``` pub async fn next_field_with_idx(&mut self) -> crate::Result> { self.try_next().await.map(|f| f.map(|field| (field.index(), field))) @@ -226,7 +221,7 @@ impl Multipart { impl Stream for Multipart { type Item = Result; - fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut mutex_guard = match self.state.lock() { Ok(lock) => lock, Err(err) => { @@ -360,9 +355,7 @@ impl Stream for Multipart { let field_name = next_field.name().map(|name| name.to_owned()); if !self.constraints.is_it_allowed(field_name.as_deref()) { - return Poll::Ready(Some(Err(crate::Error::UnknownField { - field_name: field_name.clone(), - }))); + return Poll::Ready(Some(Err(crate::Error::UnknownField { field_name }))); } return Poll::Ready(Some(Ok(next_field))); diff --git a/src/size_limit.rs b/src/size_limit.rs index a5def6f..51bc769 100644 --- a/src/size_limit.rs +++ b/src/size_limit.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; /// Represents size limit of the stream to prevent DoS attacks. /// /// Please refer [`Constraints`](./struct.Constraints.html) for more info. +#[derive(Debug)] pub struct SizeLimit { pub(crate) whole_stream: u64, pub(crate) per_field: u64, diff --git a/src/state.rs b/src/state.rs index 9d35442..8b2de46 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,7 @@ use crate::buffer::StreamBuffer; use std::task::Waker; +#[derive(Debug)] pub(crate) struct MultipartState { pub(crate) buffer: StreamBuffer, pub(crate) boundary: String,