From 59a15aae5d2ed171dbd677cef86458b259781eee Mon Sep 17 00:00:00 2001 From: denghao Date: Tue, 11 Apr 2023 17:59:27 +0800 Subject: [PATCH] Import Upstream version 0.4.5 --- .cargo_vcs_info.json | 6 + .github/workflows/CI.yml | 30 +++ .gitignore | 2 + CHANGELOG.md | 62 ++++++ Cargo.toml | 45 ++++ Cargo.toml.orig | 34 +++ LICENSE | 25 +++ README.md | 27 +++ debian-orig/cargo-checksum.json | 1 + debian-orig/changelog | 23 ++ debian-orig/compat | 1 + debian-orig/control | 38 ++++ debian-orig/copyright | 40 ++++ debian-orig/copyright.debcargo.hint | 51 +++++ debian-orig/debcargo.toml | 3 + debian-orig/rules | 3 + debian-orig/source/format | 1 + debian-orig/tests/control | 14 ++ debian-orig/watch | 4 + src/combinators/box_body.rs | 134 ++++++++++++ src/combinators/map_data.rs | 94 ++++++++ src/combinators/map_err.rs | 97 +++++++++ src/combinators/mod.rs | 11 + src/empty.rs | 75 +++++++ src/full.rs | 151 +++++++++++++ src/lib.rs | 324 ++++++++++++++++++++++++++++ src/limited.rs | 299 +++++++++++++++++++++++++ src/next.rs | 31 +++ src/size_hint.rs | 86 ++++++++ tests/is_end_stream.rs | 79 +++++++ 30 files changed, 1791 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 .github/workflows/CI.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 LICENSE create mode 100644 README.md create mode 100644 debian-orig/cargo-checksum.json create mode 100644 debian-orig/changelog create mode 100644 debian-orig/compat create mode 100644 debian-orig/control create mode 100644 debian-orig/copyright create mode 100644 debian-orig/copyright.debcargo.hint create mode 100644 debian-orig/debcargo.toml create mode 100755 debian-orig/rules create mode 100644 debian-orig/source/format create mode 100644 debian-orig/tests/control create mode 100644 debian-orig/watch create mode 100644 src/combinators/box_body.rs create mode 100644 src/combinators/map_data.rs create mode 100644 src/combinators/map_err.rs create mode 100644 src/combinators/mod.rs create mode 100644 src/empty.rs create mode 100644 src/full.rs create mode 100644 src/lib.rs create mode 100644 src/limited.rs create mode 100644 src/next.rs create mode 100644 src/size_hint.rs create mode 100644 tests/is_end_stream.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..2d0bb3d --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "6f722a14f7c6488cd53dcc4126a84e6ee201964f" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..6f09a5d --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,30 @@ +name: CI +on: [push, pull_request] + +jobs: + style: + name: Check Style + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Install Rustfmt + run: rustup component add rustfmt + - name: Check formatting + run: cargo fmt --all -- --check + + test: + name: Test + needs: [style] + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@master + - name: Install Rust + run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }} + - name: Run tests + run: cargo test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4708a20 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Unreleased + +None. + +# 0.4.5 (May 20, 2022) + +- Add `String` impl for `Body`. +- Add `Limited` body implementation. + +# 0.4.4 (October 22, 2021) + +- Add `UnsyncBoxBody` and `Body::boxed_unsync`. + +# 0.4.3 (August 8, 2021) + +- Implement `Default` for `BoxBody`. + +# 0.4.2 (May 8, 2021) + +- Correctly override `Body::size_hint` and `Body::is_end_stream` for `Empty`. +- Add `Full` which is a body that consists of a single chunk. + +# 0.4.1 (March 18, 2021) + +- Add combinators to `Body`: + - `map_data`: Change the `Data` chunks produced by the body. + - `map_err`: Change the `Error`s produced by the body. + - `boxed`: Convert the `Body` into a boxed trait object. +- Add `Empty`. + +# 0.4.0 (December 23, 2020) + +- Update `bytes` to v1.0. + +# 0.3.1 (December 13, 2019) + +- Implement `Body` for `http::Request` and `http::Response`. + +# 0.3.0 (December 4, 2019) + +- Rename `next` combinator to `data`. + +# 0.2.0 (December 3, 2019) + +- Update `http` to v0.2. +- Update `bytes` to v0.5. + +# 0.2.0-alpha.3 (October 1, 2019) + +- Fix `Body` to be object-safe. + +# 0.2.0-alpha.2 (October 1, 2019) + +- Add `next` and `trailers` combinator methods. + +# 0.2.0-alpha.1 (August 20, 2019) + +- Update to use `Pin` in `poll_data` and `poll_trailers`. + +# 0.1.0 (May 7, 2019) + +- Initial release diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..202ce7d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,45 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "http-body" +version = "0.4.5" +authors = [ + "Carl Lerche ", + "Lucio Franco ", + "Sean McArthur ", +] +description = """ +Trait representing an asynchronous, streaming, HTTP request or response body. +""" +documentation = "https://docs.rs/http-body" +readme = "README.md" +keywords = ["http"] +categories = ["web-programming"] +license = "MIT" +repository = "https://github.com/hyperium/http-body" + +[dependencies.bytes] +version = "1" + +[dependencies.http] +version = "0.2" + +[dependencies.pin-project-lite] +version = "0.2" + +[dev-dependencies.tokio] +version = "1" +features = [ + "macros", + "rt", +] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..4324486 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,34 @@ +[package] +name = "http-body" +# When releasing to crates.io: +# - Remove path dependencies +# - Update html_root_url. +# - Update doc url +# - Cargo.toml +# - README.md +# - Update CHANGELOG.md. +# - Create "vx.y.z" git tag. +version = "0.4.5" +authors = [ + "Carl Lerche ", + "Lucio Franco ", + "Sean McArthur ", +] +edition = "2018" +readme = "README.md" +documentation = "https://docs.rs/http-body" +repository = "https://github.com/hyperium/http-body" +license = "MIT" +description = """ +Trait representing an asynchronous, streaming, HTTP request or response body. +""" +keywords = ["http"] +categories = ["web-programming"] + +[dependencies] +bytes = "1" +http = "0.2" +pin-project-lite = "0.2" + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..27b08f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2019 Hyper Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c82ba29 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# HTTP Body + +A trait representing asynchronous operations on an HTTP body. + +[![crates.io][crates-badge]][crates-url] +[![documentation][docs-badge]][docs-url] +[![MIT License][mit-badge]][mit-url] +[![CI Status][ci-badge]][ci-url] + +[crates-badge]: https://img.shields.io/crates/v/http-body.svg +[crates-url]: https://crates.io/crates/http-body +[docs-badge]: https://docs.rs/http-body/badge.svg +[docs-url]: https://docs.rs/http-body +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: LICENSE +[ci-badge]: https://github.com/hyperium/http-body/workflows/CI/badge.svg +[ci-url]: https://github.com/hyperium/http-body/actions?query=workflow%3ACI + +## License + +This project is licensed under the [MIT license](LICENSE). + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in `http-body` by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/debian-orig/cargo-checksum.json b/debian-orig/cargo-checksum.json new file mode 100644 index 0000000..dbbd97b --- /dev/null +++ b/debian-orig/cargo-checksum.json @@ -0,0 +1 @@ +{"package":"d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1","files":{}} diff --git a/debian-orig/changelog b/debian-orig/changelog new file mode 100644 index 0000000..f7e0515 --- /dev/null +++ b/debian-orig/changelog @@ -0,0 +1,23 @@ +rust-http-body (0.4.5-1) unstable; urgency=medium + + * Team upload. + * Package http-body 0.4.5 from crates.io using debcargo 2.5.0 + + [ Fabian Gruenbichler ] + * Package http-body 0.4.4 from crates.io using debcargo 2.5.0 + + -- Daniel Kahn Gillmor Sat, 21 May 2022 20:26:34 -0400 + +rust-http-body (0.1.0-2) unstable; urgency=medium + + * Team upload. + * Package http-body 0.1.0 from crates.io using debcargo 2.2.10 + * Source upload for migration + + -- Sylvestre Ledru Tue, 06 Aug 2019 18:10:37 +0200 + +rust-http-body (0.1.0-1) unstable; urgency=medium + + * Package http-body 0.1.0 from crates.io using debcargo 2.2.10 + + -- Robin Krahl Sat, 13 Jul 2019 22:14:15 +0200 diff --git a/debian-orig/compat b/debian-orig/compat new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/debian-orig/compat @@ -0,0 +1 @@ +12 diff --git a/debian-orig/control b/debian-orig/control new file mode 100644 index 0000000..f9bc1d0 --- /dev/null +++ b/debian-orig/control @@ -0,0 +1,38 @@ +Source: rust-http-body +Section: rust +Priority: optional +Build-Depends: debhelper (>= 12), + dh-cargo (>= 25), + cargo:native , + rustc:native , + libstd-rust-dev , + librust-bytes-1+default-dev , + librust-http-0.2+default-dev , + librust-pin-project-lite-0.2+default-dev +Maintainer: Debian Rust Maintainers +Uploaders: + Robin Krahl +Standards-Version: 4.5.1 +Vcs-Git: https://salsa.debian.org/rust-team/debcargo-conf.git [src/http-body] +Vcs-Browser: https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/http-body +Rules-Requires-Root: no + +Package: librust-http-body-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-bytes-1+default-dev, + librust-http-0.2+default-dev, + librust-pin-project-lite-0.2+default-dev +Provides: + librust-http-body+default-dev (= ${binary:Version}), + librust-http-body-0-dev (= ${binary:Version}), + librust-http-body-0+default-dev (= ${binary:Version}), + librust-http-body-0.4-dev (= ${binary:Version}), + librust-http-body-0.4+default-dev (= ${binary:Version}), + librust-http-body-0.4.5-dev (= ${binary:Version}), + librust-http-body-0.4.5+default-dev (= ${binary:Version}) +Description: trait representing asynchronous operations on an HTTP body - Rust source code + This package contains the source for the Rust http-body crate, packaged by + debcargo for use with cargo and dh-cargo. diff --git a/debian-orig/copyright b/debian-orig/copyright new file mode 100644 index 0000000..4872383 --- /dev/null +++ b/debian-orig/copyright @@ -0,0 +1,40 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: http-body +Upstream-Contact: + Carl Lerche + Lucio Franco + Sean McArthur +Source: https://github.com/hyperium/http-body + +Files: * +Copyright: + 2019 Carl Lerche + 2019 Lucio Franco + 2019 Sean McArthur + 2019 Hyper Contributors +License: MIT + +Files: debian/* +Copyright: + 2019 Debian Rust Maintainers + 2019 Robin Krahl +License: MIT + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/debian-orig/copyright.debcargo.hint b/debian-orig/copyright.debcargo.hint new file mode 100644 index 0000000..6538767 --- /dev/null +++ b/debian-orig/copyright.debcargo.hint @@ -0,0 +1,51 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: http-body +Upstream-Contact: + Carl Lerche + Lucio Franco + Sean McArthur +Source: https://github.com/hyperium/http-body + +Files: * +Copyright: + FIXME (overlay) UNKNOWN-YEARS Carl Lerche + FIXME (overlay) UNKNOWN-YEARS Lucio Franco + FIXME (overlay) UNKNOWN-YEARS Sean McArthur +License: MIT +Comment: + FIXME (overlay): Since upstream copyright years are not available in + Cargo.toml, they were extracted from the upstream Git repository. This may not + be correct information so you should review and fix this before uploading to + the archive. + +Files: ./LICENSE +Copyright: 2019 Hyper Contributors +License: UNKNOWN-LICENSE; FIXME (overlay) +Comment: + FIXME (overlay): These notices are extracted from files. Please review them + before uploading to the archive. + +Files: debian/* +Copyright: + 2019-2022 Debian Rust Maintainers + 2019-2022 Robin Krahl +License: MIT + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/debian-orig/debcargo.toml b/debian-orig/debcargo.toml new file mode 100644 index 0000000..8fb7c70 --- /dev/null +++ b/debian-orig/debcargo.toml @@ -0,0 +1,3 @@ +overlay = "." +uploaders = ["Robin Krahl "] +summary = "trait representing asynchronous operations on an HTTP body" diff --git a/debian-orig/rules b/debian-orig/rules new file mode 100755 index 0000000..044c1c2 --- /dev/null +++ b/debian-orig/rules @@ -0,0 +1,3 @@ +#!/usr/bin/make -f +%: + dh $@ --buildsystem cargo diff --git a/debian-orig/source/format b/debian-orig/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian-orig/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian-orig/tests/control b/debian-orig/tests/control new file mode 100644 index 0000000..2880642 --- /dev/null +++ b/debian-orig/tests/control @@ -0,0 +1,14 @@ +Test-Command: /usr/share/cargo/bin/cargo-auto-test http-body 0.4.5 --all-targets --all-features +Features: test-name=rust-http-body:@ +Depends: dh-cargo (>= 18), librust-tokio-1+default-dev, librust-tokio-1+macros-dev, librust-tokio-1+rt-dev, @ +Restrictions: allow-stderr, skip-not-installable + +Test-Command: /usr/share/cargo/bin/cargo-auto-test http-body 0.4.5 --all-targets +Features: test-name=librust-http-body-dev:default +Depends: dh-cargo (>= 18), librust-tokio-1+default-dev, librust-tokio-1+macros-dev, librust-tokio-1+rt-dev, @ +Restrictions: allow-stderr, skip-not-installable + +Test-Command: /usr/share/cargo/bin/cargo-auto-test http-body 0.4.5 --all-targets --no-default-features +Features: test-name=librust-http-body-dev: +Depends: dh-cargo (>= 18), librust-tokio-1+default-dev, librust-tokio-1+macros-dev, librust-tokio-1+rt-dev, @ +Restrictions: allow-stderr, skip-not-installable diff --git a/debian-orig/watch b/debian-orig/watch new file mode 100644 index 0000000..64703b8 --- /dev/null +++ b/debian-orig/watch @@ -0,0 +1,4 @@ +version=4 +opts=filenamemangle=s/.*\/(.*)\/download/http-body-$1\.tar\.gz/g,\ +uversionmangle=s/(\d)[_\.\-\+]?((RC|rc|pre|dev|beta|alpha)\d*)$/$1~$2/ \ +https://qa.debian.org/cgi-bin/fakeupstream.cgi?upstream=crates.io/http-body .*/crates/http-body/@ANY_VERSION@/download diff --git a/src/combinators/box_body.rs b/src/combinators/box_body.rs new file mode 100644 index 0000000..97c8313 --- /dev/null +++ b/src/combinators/box_body.rs @@ -0,0 +1,134 @@ +use crate::Body; +use bytes::Buf; +use std::{ + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +/// A boxed [`Body`] trait object. +pub struct BoxBody { + inner: Pin + Send + Sync + 'static>>, +} + +/// A boxed [`Body`] trait object that is !Sync. +pub struct UnsyncBoxBody { + inner: Pin + Send + 'static>>, +} + +impl BoxBody { + /// Create a new `BoxBody`. + pub fn new(body: B) -> Self + where + B: Body + Send + Sync + 'static, + D: Buf, + { + Self { + inner: Box::pin(body), + } + } +} + +impl fmt::Debug for BoxBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BoxBody").finish() + } +} + +impl Body for BoxBody +where + D: Buf, +{ + type Data = D; + type Error = E; + + fn poll_data( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.inner.as_mut().poll_data(cx) + } + + fn poll_trailers( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + self.inner.as_mut().poll_trailers(cx) + } + + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } + + fn size_hint(&self) -> crate::SizeHint { + self.inner.size_hint() + } +} + +impl Default for BoxBody +where + D: Buf + 'static, +{ + fn default() -> Self { + BoxBody::new(crate::Empty::new().map_err(|err| match err {})) + } +} + +// === UnsyncBoxBody === +impl UnsyncBoxBody { + /// Create a new `BoxBody`. + pub fn new(body: B) -> Self + where + B: Body + Send + 'static, + D: Buf, + { + Self { + inner: Box::pin(body), + } + } +} + +impl fmt::Debug for UnsyncBoxBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UnsyncBoxBody").finish() + } +} + +impl Body for UnsyncBoxBody +where + D: Buf, +{ + type Data = D; + type Error = E; + + fn poll_data( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.inner.as_mut().poll_data(cx) + } + + fn poll_trailers( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + self.inner.as_mut().poll_trailers(cx) + } + + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } + + fn size_hint(&self) -> crate::SizeHint { + self.inner.size_hint() + } +} + +impl Default for UnsyncBoxBody +where + D: Buf + 'static, +{ + fn default() -> Self { + UnsyncBoxBody::new(crate::Empty::new().map_err(|err| match err {})) + } +} diff --git a/src/combinators/map_data.rs b/src/combinators/map_data.rs new file mode 100644 index 0000000..6d9c5a8 --- /dev/null +++ b/src/combinators/map_data.rs @@ -0,0 +1,94 @@ +use crate::Body; +use bytes::Buf; +use pin_project_lite::pin_project; +use std::{ + any::type_name, + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +pin_project! { + /// Body returned by the [`map_data`] combinator. + /// + /// [`map_data`]: crate::util::BodyExt::map_data + #[derive(Clone, Copy)] + pub struct MapData { + #[pin] + inner: B, + f: F + } +} + +impl MapData { + #[inline] + pub(crate) fn new(body: B, f: F) -> Self { + Self { inner: body, f } + } + + /// Get a reference to the inner body + pub fn get_ref(&self) -> &B { + &self.inner + } + + /// Get a mutable reference to the inner body + pub fn get_mut(&mut self) -> &mut B { + &mut self.inner + } + + /// Get a pinned mutable reference to the inner body + pub fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut B> { + self.project().inner + } + + /// Consume `self`, returning the inner body + pub fn into_inner(self) -> B { + self.inner + } +} + +impl Body for MapData +where + B: Body, + F: FnMut(B::Data) -> B2, + B2: Buf, +{ + type Data = B2; + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let this = self.project(); + match this.inner.poll_data(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(None) => Poll::Ready(None), + Poll::Ready(Some(Ok(data))) => Poll::Ready(Some(Ok((this.f)(data)))), + Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))), + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + self.project().inner.poll_trailers(cx) + } + + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } +} + +impl fmt::Debug for MapData +where + B: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("MapData") + .field("inner", &self.inner) + .field("f", &type_name::()) + .finish() + } +} diff --git a/src/combinators/map_err.rs b/src/combinators/map_err.rs new file mode 100644 index 0000000..c77168d --- /dev/null +++ b/src/combinators/map_err.rs @@ -0,0 +1,97 @@ +use crate::Body; +use pin_project_lite::pin_project; +use std::{ + any::type_name, + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +pin_project! { + /// Body returned by the [`map_err`] combinator. + /// + /// [`map_err`]: crate::util::BodyExt::map_err + #[derive(Clone, Copy)] + pub struct MapErr { + #[pin] + inner: B, + f: F + } +} + +impl MapErr { + #[inline] + pub(crate) fn new(body: B, f: F) -> Self { + Self { inner: body, f } + } + + /// Get a reference to the inner body + pub fn get_ref(&self) -> &B { + &self.inner + } + + /// Get a mutable reference to the inner body + pub fn get_mut(&mut self) -> &mut B { + &mut self.inner + } + + /// Get a pinned mutable reference to the inner body + pub fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut B> { + self.project().inner + } + + /// Consume `self`, returning the inner body + pub fn into_inner(self) -> B { + self.inner + } +} + +impl Body for MapErr +where + B: Body, + F: FnMut(B::Error) -> E, +{ + type Data = B::Data; + type Error = E; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let this = self.project(); + match this.inner.poll_data(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(None) => Poll::Ready(None), + Poll::Ready(Some(Ok(data))) => Poll::Ready(Some(Ok(data))), + Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err((this.f)(err)))), + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + let this = self.project(); + this.inner.poll_trailers(cx).map_err(this.f) + } + + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } + + fn size_hint(&self) -> crate::SizeHint { + self.inner.size_hint() + } +} + +impl fmt::Debug for MapErr +where + B: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("MapErr") + .field("inner", &self.inner) + .field("f", &type_name::()) + .finish() + } +} diff --git a/src/combinators/mod.rs b/src/combinators/mod.rs new file mode 100644 index 0000000..c52f367 --- /dev/null +++ b/src/combinators/mod.rs @@ -0,0 +1,11 @@ +//! Combinators for the `Body` trait. + +mod box_body; +mod map_data; +mod map_err; + +pub use self::{ + box_body::{BoxBody, UnsyncBoxBody}, + map_data::MapData, + map_err::MapErr, +}; diff --git a/src/empty.rs b/src/empty.rs new file mode 100644 index 0000000..7d63ceb --- /dev/null +++ b/src/empty.rs @@ -0,0 +1,75 @@ +use super::{Body, SizeHint}; +use bytes::Buf; +use http::HeaderMap; +use std::{ + convert::Infallible, + fmt, + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, +}; + +/// A body that is always empty. +pub struct Empty { + _marker: PhantomData D>, +} + +impl Empty { + /// Create a new `Empty`. + pub fn new() -> Self { + Self::default() + } +} + +impl Body for Empty { + type Data = D; + type Error = Infallible; + + #[inline] + fn poll_data( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(None) + } + + #[inline] + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } + + fn is_end_stream(&self) -> bool { + true + } + + fn size_hint(&self) -> SizeHint { + SizeHint::with_exact(0) + } +} + +impl fmt::Debug for Empty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Empty").finish() + } +} + +impl Default for Empty { + fn default() -> Self { + Self { + _marker: PhantomData, + } + } +} + +impl Clone for Empty { + fn clone(&self) -> Self { + Self { + _marker: PhantomData, + } + } +} + +impl Copy for Empty {} diff --git a/src/full.rs b/src/full.rs new file mode 100644 index 0000000..f1d063b --- /dev/null +++ b/src/full.rs @@ -0,0 +1,151 @@ +use crate::{Body, SizeHint}; +use bytes::{Buf, Bytes}; +use http::HeaderMap; +use pin_project_lite::pin_project; +use std::borrow::Cow; +use std::convert::{Infallible, TryFrom}; +use std::pin::Pin; +use std::task::{Context, Poll}; + +pin_project! { + /// A body that consists of a single chunk. + #[derive(Clone, Copy, Debug)] + pub struct Full { + data: Option, + } +} + +impl Full +where + D: Buf, +{ + /// Create a new `Full`. + pub fn new(data: D) -> Self { + let data = if data.has_remaining() { + Some(data) + } else { + None + }; + Full { data } + } +} + +impl Body for Full +where + D: Buf, +{ + type Data = D; + type Error = Infallible; + + fn poll_data( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(self.data.take().map(Ok)) + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } + + fn is_end_stream(&self) -> bool { + self.data.is_none() + } + + fn size_hint(&self) -> SizeHint { + self.data + .as_ref() + .map(|data| SizeHint::with_exact(u64::try_from(data.remaining()).unwrap())) + .unwrap_or_else(|| SizeHint::with_exact(0)) + } +} + +impl Default for Full +where + D: Buf, +{ + /// Create an empty `Full`. + fn default() -> Self { + Full { data: None } + } +} + +impl From for Full +where + D: Buf + From, +{ + fn from(bytes: Bytes) -> Self { + Full::new(D::from(bytes)) + } +} + +impl From> for Full +where + D: Buf + From>, +{ + fn from(vec: Vec) -> Self { + Full::new(D::from(vec)) + } +} + +impl From<&'static [u8]> for Full +where + D: Buf + From<&'static [u8]>, +{ + fn from(slice: &'static [u8]) -> Self { + Full::new(D::from(slice)) + } +} + +impl From> for Full +where + D: Buf + From<&'static B> + From, + B: ToOwned + ?Sized, +{ + fn from(cow: Cow<'static, B>) -> Self { + match cow { + Cow::Borrowed(b) => Full::new(D::from(b)), + Cow::Owned(o) => Full::new(D::from(o)), + } + } +} + +impl From for Full +where + D: Buf + From, +{ + fn from(s: String) -> Self { + Full::new(D::from(s)) + } +} + +impl From<&'static str> for Full +where + D: Buf + From<&'static str>, +{ + fn from(slice: &'static str) -> Self { + Full::new(D::from(slice)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn full_returns_some() { + let mut full = Full::new(&b"hello"[..]); + assert_eq!(full.size_hint().exact(), Some(b"hello".len() as u64)); + assert_eq!(full.data().await, Some(Ok(&b"hello"[..]))); + assert!(full.data().await.is_none()); + } + + #[tokio::test] + async fn empty_full_returns_none() { + assert!(Full::<&[u8]>::default().data().await.is_none()); + assert!(Full::new(&b""[..]).data().await.is_none()); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..84efd91 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,324 @@ +#![doc(html_root_url = "https://docs.rs/http-body/0.4.5")] +#![deny( + missing_debug_implementations, + missing_docs, + unreachable_pub, + broken_intra_doc_links +)] +#![cfg_attr(test, deny(warnings))] + +//! Asynchronous HTTP request or response body. +//! +//! See [`Body`] for more details. +//! +//! [`Body`]: trait.Body.html + +mod empty; +mod full; +mod limited; +mod next; +mod size_hint; + +pub mod combinators; + +pub use self::empty::Empty; +pub use self::full::Full; +pub use self::limited::{LengthLimitError, Limited}; +pub use self::next::{Data, Trailers}; +pub use self::size_hint::SizeHint; + +use self::combinators::{BoxBody, MapData, MapErr, UnsyncBoxBody}; +use bytes::{Buf, Bytes}; +use http::HeaderMap; +use std::convert::Infallible; +use std::ops; +use std::pin::Pin; +use std::task::{Context, Poll}; + +/// Trait representing a streaming body of a Request or Response. +/// +/// Data is streamed via the `poll_data` function, which asynchronously yields `T: Buf` values. The +/// `size_hint` function provides insight into the total number of bytes that will be streamed. +/// +/// The `poll_trailers` function returns an optional set of trailers used to finalize the request / +/// response exchange. This is mostly used when using the HTTP/2.0 protocol. +/// +pub trait Body { + /// Values yielded by the `Body`. + type Data: Buf; + + /// The error type this `Body` might generate. + type Error; + + /// Attempt to pull out the next data buffer of this stream. + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>>; + + /// Poll for an optional **single** `HeaderMap` of trailers. + /// + /// This function should only be called once `poll_data` returns `None`. + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>; + + /// Returns `true` when the end of stream has been reached. + /// + /// An end of stream means that both `poll_data` and `poll_trailers` will + /// return `None`. + /// + /// A return value of `false` **does not** guarantee that a value will be + /// returned from `poll_stream` or `poll_trailers`. + fn is_end_stream(&self) -> bool { + false + } + + /// Returns the bounds on the remaining length of the stream. + /// + /// When the **exact** remaining length of the stream is known, the upper bound will be set and + /// will equal the lower bound. + fn size_hint(&self) -> SizeHint { + SizeHint::default() + } + + /// Returns future that resolves to next data chunk, if any. + fn data(&mut self) -> Data<'_, Self> + where + Self: Unpin + Sized, + { + Data(self) + } + + /// Returns future that resolves to trailers, if any. + fn trailers(&mut self) -> Trailers<'_, Self> + where + Self: Unpin + Sized, + { + Trailers(self) + } + + /// Maps this body's data value to a different value. + fn map_data(self, f: F) -> MapData + where + Self: Sized, + F: FnMut(Self::Data) -> B, + B: Buf, + { + MapData::new(self, f) + } + + /// Maps this body's error value to a different value. + fn map_err(self, f: F) -> MapErr + where + Self: Sized, + F: FnMut(Self::Error) -> E, + { + MapErr::new(self, f) + } + + /// Turn this body into a boxed trait object. + fn boxed(self) -> BoxBody + where + Self: Sized + Send + Sync + 'static, + { + BoxBody::new(self) + } + + /// Turn this body into a boxed trait object that is !Sync. + fn boxed_unsync(self) -> UnsyncBoxBody + where + Self: Sized + Send + 'static, + { + UnsyncBoxBody::new(self) + } +} + +impl Body for &mut T { + type Data = T::Data; + type Error = T::Error; + + fn poll_data( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::new(&mut **self).poll_data(cx) + } + + fn poll_trailers( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Pin::new(&mut **self).poll_trailers(cx) + } + + fn is_end_stream(&self) -> bool { + Pin::new(&**self).is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + Pin::new(&**self).size_hint() + } +} + +impl

Body for Pin

+where + P: Unpin + ops::DerefMut, + P::Target: Body, +{ + type Data = <

::Target as Body>::Data; + type Error = <

::Target as Body>::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::get_mut(self).as_mut().poll_data(cx) + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Pin::get_mut(self).as_mut().poll_trailers(cx) + } + + fn is_end_stream(&self) -> bool { + self.as_ref().is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + self.as_ref().size_hint() + } +} + +impl Body for Box { + type Data = T::Data; + type Error = T::Error; + + fn poll_data( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::new(&mut **self).poll_data(cx) + } + + fn poll_trailers( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Pin::new(&mut **self).poll_trailers(cx) + } + + fn is_end_stream(&self) -> bool { + self.as_ref().is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + self.as_ref().size_hint() + } +} + +impl Body for http::Request { + type Data = B::Data; + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + unsafe { + self.map_unchecked_mut(http::Request::body_mut) + .poll_data(cx) + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + unsafe { + self.map_unchecked_mut(http::Request::body_mut) + .poll_trailers(cx) + } + } + + fn is_end_stream(&self) -> bool { + self.body().is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + self.body().size_hint() + } +} + +impl Body for http::Response { + type Data = B::Data; + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + unsafe { + self.map_unchecked_mut(http::Response::body_mut) + .poll_data(cx) + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + unsafe { + self.map_unchecked_mut(http::Response::body_mut) + .poll_trailers(cx) + } + } + + fn is_end_stream(&self) -> bool { + self.body().is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + self.body().size_hint() + } +} + +impl Body for String { + type Data = Bytes; + type Error = Infallible; + + fn poll_data( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if !self.is_empty() { + let s = std::mem::take(&mut *self); + Poll::Ready(Some(Ok(s.into_bytes().into()))) + } else { + Poll::Ready(None) + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } + + fn is_end_stream(&self) -> bool { + self.is_empty() + } + + fn size_hint(&self) -> SizeHint { + SizeHint::with_exact(self.len() as u64) + } +} + +#[cfg(test)] +fn _assert_bounds() { + fn can_be_trait_object(_: &dyn Body>, Error = std::io::Error>) {} +} diff --git a/src/limited.rs b/src/limited.rs new file mode 100644 index 0000000..a40add9 --- /dev/null +++ b/src/limited.rs @@ -0,0 +1,299 @@ +use crate::{Body, SizeHint}; +use bytes::Buf; +use http::HeaderMap; +use pin_project_lite::pin_project; +use std::error::Error; +use std::fmt; +use std::pin::Pin; +use std::task::{Context, Poll}; + +pin_project! { + /// A length limited body. + /// + /// This body will return an error if more than the configured number + /// of bytes are returned on polling the wrapped body. + #[derive(Clone, Copy, Debug)] + pub struct Limited { + remaining: usize, + #[pin] + inner: B, + } +} + +impl Limited { + /// Create a new `Limited`. + pub fn new(inner: B, limit: usize) -> Self { + Self { + remaining: limit, + inner, + } + } +} + +impl Body for Limited +where + B: Body, + B::Error: Into>, +{ + type Data = B::Data; + type Error = Box; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let this = self.project(); + let res = match this.inner.poll_data(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => None, + Poll::Ready(Some(Ok(data))) => { + if data.remaining() > *this.remaining { + *this.remaining = 0; + Some(Err(LengthLimitError.into())) + } else { + *this.remaining -= data.remaining(); + Some(Ok(data)) + } + } + Poll::Ready(Some(Err(err))) => Some(Err(err.into())), + }; + + Poll::Ready(res) + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + let this = self.project(); + let res = match this.inner.poll_trailers(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(data)) => Ok(data), + Poll::Ready(Err(err)) => Err(err.into()), + }; + + Poll::Ready(res) + } + + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + use std::convert::TryFrom; + match u64::try_from(self.remaining) { + Ok(n) => { + let mut hint = self.inner.size_hint(); + if hint.lower() >= n { + hint.set_exact(n) + } else if let Some(max) = hint.upper() { + hint.set_upper(n.min(max)) + } else { + hint.set_upper(n) + } + hint + } + Err(_) => self.inner.size_hint(), + } + } +} + +/// An error returned when body length exceeds the configured limit. +#[derive(Debug)] +#[non_exhaustive] +pub struct LengthLimitError; + +impl fmt::Display for LengthLimitError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("length limit exceeded") + } +} + +impl Error for LengthLimitError {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Full; + use bytes::Bytes; + use std::convert::Infallible; + + #[tokio::test] + async fn read_for_body_under_limit_returns_data() { + const DATA: &[u8] = b"testing"; + let inner = Full::new(Bytes::from(DATA)); + let body = &mut Limited::new(inner, 8); + + let mut hint = SizeHint::new(); + hint.set_upper(7); + assert_eq!(body.size_hint().upper(), hint.upper()); + + let data = body.data().await.unwrap().unwrap(); + assert_eq!(data, DATA); + hint.set_upper(0); + assert_eq!(body.size_hint().upper(), hint.upper()); + + assert!(matches!(body.data().await, None)); + } + + #[tokio::test] + async fn read_for_body_over_limit_returns_error() { + const DATA: &[u8] = b"testing a string that is too long"; + let inner = Full::new(Bytes::from(DATA)); + let body = &mut Limited::new(inner, 8); + + let mut hint = SizeHint::new(); + hint.set_upper(8); + assert_eq!(body.size_hint().upper(), hint.upper()); + + let error = body.data().await.unwrap().unwrap_err(); + assert!(matches!(error.downcast_ref(), Some(LengthLimitError))); + } + + struct Chunky(&'static [&'static [u8]]); + + impl Body for Chunky { + type Data = &'static [u8]; + type Error = Infallible; + + fn poll_data( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + let mut this = self; + match this.0.split_first().map(|(&head, tail)| (Ok(head), tail)) { + Some((data, new_tail)) => { + this.0 = new_tail; + + Poll::Ready(Some(data)) + } + None => Poll::Ready(None), + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(Some(HeaderMap::new()))) + } + } + + #[tokio::test] + async fn read_for_chunked_body_around_limit_returns_first_chunk_but_returns_error_on_over_limit_chunk( + ) { + const DATA: &[&[u8]] = &[b"testing ", b"a string that is too long"]; + let inner = Chunky(DATA); + let body = &mut Limited::new(inner, 8); + + let mut hint = SizeHint::new(); + hint.set_upper(8); + assert_eq!(body.size_hint().upper(), hint.upper()); + + let data = body.data().await.unwrap().unwrap(); + assert_eq!(data, DATA[0]); + hint.set_upper(0); + assert_eq!(body.size_hint().upper(), hint.upper()); + + let error = body.data().await.unwrap().unwrap_err(); + assert!(matches!(error.downcast_ref(), Some(LengthLimitError))); + } + + #[tokio::test] + async fn read_for_chunked_body_over_limit_on_first_chunk_returns_error() { + const DATA: &[&[u8]] = &[b"testing a string", b" that is too long"]; + let inner = Chunky(DATA); + let body = &mut Limited::new(inner, 8); + + let mut hint = SizeHint::new(); + hint.set_upper(8); + assert_eq!(body.size_hint().upper(), hint.upper()); + + let error = body.data().await.unwrap().unwrap_err(); + assert!(matches!(error.downcast_ref(), Some(LengthLimitError))); + } + + #[tokio::test] + async fn read_for_chunked_body_under_limit_is_okay() { + const DATA: &[&[u8]] = &[b"test", b"ing!"]; + let inner = Chunky(DATA); + let body = &mut Limited::new(inner, 8); + + let mut hint = SizeHint::new(); + hint.set_upper(8); + assert_eq!(body.size_hint().upper(), hint.upper()); + + let data = body.data().await.unwrap().unwrap(); + assert_eq!(data, DATA[0]); + hint.set_upper(4); + assert_eq!(body.size_hint().upper(), hint.upper()); + + let data = body.data().await.unwrap().unwrap(); + assert_eq!(data, DATA[1]); + hint.set_upper(0); + assert_eq!(body.size_hint().upper(), hint.upper()); + + assert!(matches!(body.data().await, None)); + } + + #[tokio::test] + async fn read_for_trailers_propagates_inner_trailers() { + const DATA: &[&[u8]] = &[b"test", b"ing!"]; + let inner = Chunky(DATA); + let body = &mut Limited::new(inner, 8); + let trailers = body.trailers().await.unwrap(); + assert_eq!(trailers, Some(HeaderMap::new())) + } + + #[derive(Debug)] + enum ErrorBodyError { + Data, + Trailers, + } + + impl fmt::Display for ErrorBodyError { + fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result { + Ok(()) + } + } + + impl Error for ErrorBodyError {} + + struct ErrorBody; + + impl Body for ErrorBody { + type Data = &'static [u8]; + type Error = ErrorBodyError; + + fn poll_data( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(Some(Err(ErrorBodyError::Data))) + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Err(ErrorBodyError::Trailers)) + } + } + + #[tokio::test] + async fn read_for_body_returning_error_propagates_error() { + let body = &mut Limited::new(ErrorBody, 8); + let error = body.data().await.unwrap().unwrap_err(); + assert!(matches!(error.downcast_ref(), Some(ErrorBodyError::Data))); + } + + #[tokio::test] + async fn trailers_for_body_returning_error_propagates_error() { + let body = &mut Limited::new(ErrorBody, 8); + let error = body.trailers().await.unwrap_err(); + assert!(matches!( + error.downcast_ref(), + Some(ErrorBodyError::Trailers) + )); + } +} diff --git a/src/next.rs b/src/next.rs new file mode 100644 index 0000000..fc87ffc --- /dev/null +++ b/src/next.rs @@ -0,0 +1,31 @@ +use crate::Body; + +use core::future::Future; +use core::pin::Pin; +use core::task; + +#[must_use = "futures don't do anything unless polled"] +#[derive(Debug)] +/// Future that resolves to the next data chunk from `Body` +pub struct Data<'a, T: ?Sized>(pub(crate) &'a mut T); + +impl<'a, T: Body + Unpin + ?Sized> Future for Data<'a, T> { + type Output = Option>; + + fn poll(mut self: Pin<&mut Self>, ctx: &mut task::Context<'_>) -> task::Poll { + Pin::new(&mut self.0).poll_data(ctx) + } +} + +#[must_use = "futures don't do anything unless polled"] +#[derive(Debug)] +/// Future that resolves to the optional trailers from `Body` +pub struct Trailers<'a, T: ?Sized>(pub(crate) &'a mut T); + +impl<'a, T: Body + Unpin + ?Sized> Future for Trailers<'a, T> { + type Output = Result, T::Error>; + + fn poll(mut self: Pin<&mut Self>, ctx: &mut task::Context<'_>) -> task::Poll { + Pin::new(&mut self.0).poll_trailers(ctx) + } +} diff --git a/src/size_hint.rs b/src/size_hint.rs new file mode 100644 index 0000000..00a8f19 --- /dev/null +++ b/src/size_hint.rs @@ -0,0 +1,86 @@ +use std::u64; + +/// A `Body` size hint +/// +/// The default implementation returns: +/// +/// * 0 for `lower` +/// * `None` for `upper`. +#[derive(Debug, Default, Clone)] +pub struct SizeHint { + lower: u64, + upper: Option, +} + +impl SizeHint { + /// Returns a new `SizeHint` with default values + #[inline] + pub fn new() -> SizeHint { + SizeHint::default() + } + + /// Returns a new `SizeHint` with both upper and lower bounds set to the + /// given value. + #[inline] + pub fn with_exact(value: u64) -> SizeHint { + SizeHint { + lower: value, + upper: Some(value), + } + } + + /// Returns the lower bound of data that the `Body` will yield before + /// completing. + #[inline] + pub fn lower(&self) -> u64 { + self.lower + } + + /// Set the value of the `lower` hint. + /// + /// # Panics + /// + /// The function panics if `value` is greater than `upper`. + #[inline] + pub fn set_lower(&mut self, value: u64) { + assert!(value <= self.upper.unwrap_or(u64::MAX)); + self.lower = value; + } + + /// Returns the upper bound of data the `Body` will yield before + /// completing, or `None` if the value is unknown. + #[inline] + pub fn upper(&self) -> Option { + self.upper + } + + /// Set the value of the `upper` hint value. + /// + /// # Panics + /// + /// This function panics if `value` is less than `lower`. + #[inline] + pub fn set_upper(&mut self, value: u64) { + assert!(value >= self.lower, "`value` is less than than `lower`"); + + self.upper = Some(value); + } + + /// Returns the exact size of data that will be yielded **if** the + /// `lower` and `upper` bounds are equal. + #[inline] + pub fn exact(&self) -> Option { + if Some(self.lower) == self.upper { + self.upper + } else { + None + } + } + + /// Set the value of the `lower` and `upper` bounds to exactly the same. + #[inline] + pub fn set_exact(&mut self, value: u64) { + self.lower = value; + self.upper = Some(value); + } +} diff --git a/tests/is_end_stream.rs b/tests/is_end_stream.rs new file mode 100644 index 0000000..beaeb0b --- /dev/null +++ b/tests/is_end_stream.rs @@ -0,0 +1,79 @@ +use http::HeaderMap; +use http_body::{Body, SizeHint}; +use std::pin::Pin; +use std::task::{Context, Poll}; + +struct Mock { + size_hint: SizeHint, +} + +impl Body for Mock { + type Data = ::std::io::Cursor>; + type Error = (); + + fn poll_data( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(None) + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } + + fn size_hint(&self) -> SizeHint { + self.size_hint.clone() + } +} + +#[test] +fn is_end_stream_true() { + let combos = [ + (None, None, false), + (Some(123), None, false), + (Some(0), Some(123), false), + (Some(123), Some(123), false), + (Some(0), Some(0), false), + ]; + + for &(lower, upper, is_end_stream) in &combos { + let mut size_hint = SizeHint::new(); + assert_eq!(0, size_hint.lower()); + assert!(size_hint.upper().is_none()); + + if let Some(lower) = lower { + size_hint.set_lower(lower); + } + + if let Some(upper) = upper { + size_hint.set_upper(upper); + } + + let mut mock = Mock { size_hint }; + + assert_eq!( + is_end_stream, + Pin::new(&mut mock).is_end_stream(), + "size_hint = {:?}", + mock.size_hint.clone() + ); + } +} + +#[test] +fn is_end_stream_default_false() { + let mut mock = Mock { + size_hint: SizeHint::default(), + }; + + assert_eq!( + false, + Pin::new(&mut mock).is_end_stream(), + "size_hint = {:?}", + mock.size_hint.clone() + ); +}