From: DongHun Kwak Date: Mon, 1 May 2023 22:44:04 +0000 (+0900) Subject: Import partial-io 0.5.4 X-Git-Tag: upstream/0.5.4 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=refs%2Fheads%2Fupstream;p=platform%2Fupstream%2Frust-partial-io.git Import partial-io 0.5.4 --- ea7fff82a0af59b66eab2d22941cc373e58889c9 diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..4580be0 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xfmt = "fmt -- --config imports_granularity=Crate" diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..01dac5e --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "4d3bbd5e8a125423a7955462408d918ad7464a23" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5a8048a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: sunshowers diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..eaa00e7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Reference: +# https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: all diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9afb6a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +on: + push: + branches: + - main + pull_request: + branches: + - main + +name: CI +env: + RUSTFLAGS: -D warnings + CARGO_TERM_COLOR: always + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy + - name: Lint (clippy) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-features --all-targets + - name: Lint (rustfmt) + uses: actions-rs/cargo@v1 + with: + command: xfmt + args: --check + - name: Install cargo readme + uses: baptiste0928/cargo-install@v1 + with: + crate: cargo-readme + version: latest + - name: Run cargo readme + run: ./scripts/regenerate-readmes.sh + - name: Check for differences + run: git diff --exit-code + + build: + name: Build and test + runs-on: ubuntu-latest + strategy: + matrix: + rust-version: [1.56, stable] + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust-version }} + - uses: Swatinem/rust-cache@f8f67b7515e98e4ac991ccb5c11240861e0f712b + - uses: taiki-e/install-action@cargo-hack + - uses: taiki-e/install-action@nextest + - name: Build + uses: actions-rs/cargo@v1 + with: + # Build all targets to ensure examples are built as well. + command: hack + args: --feature-powerset build --all-targets + - name: Test + uses: actions-rs/cargo@v1 + with: + command: hack + args: --feature-powerset nextest run --all-targets + - name: Run doctests + uses: actions-rs/cargo@v1 + with: + command: hack + args: --feature-powerset test --doc diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..0fb579a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,36 @@ +on: + push: + branches: + - main + +name: Docs + +jobs: + docs: + name: Build and deploy documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: Build + env: + # So that feature(doc_cfg) can be used. + RUSTC_BOOTSTRAP: 1 + RUSTDOCFLAGS: --cfg doc_cfg + uses: actions-rs/cargo@v1 + with: + command: doc + args: --all-features + - name: Organize + run: | + mkdir target/gh-pages + touch target/gh-pages/.nojekyll + mv target/doc target/gh-pages/rustdoc + - name: Deploy + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: target/gh-pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..744bd94 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +# adapted from https://github.com/taiki-e/cargo-hack/blob/main/.github/workflows/release.yml + +name: Publish releases to GitHub +on: + push: + tags: + - '*' + +jobs: + create-release: + if: github.repository_owner == 'sunshowers-code' + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + persist-credentials: false + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - uses: taiki-e/create-gh-release-action@v1 + with: + changelog: CHANGELOG.md + title: partial-io $version + branch: main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38db119 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# will have compiled files and executables +/target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# IntelliJ settings +/.idea diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..efcd9c7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +## [0.5.4] - 2022-09-27 + +### Fixed + +- For proptest, generate `PartialOp::Limited` byte counts starting at 1 rather than 0. This is because + 0 can mean no more data is available in the stream. + +## [0.5.3] - 2022-09-27 + +### Updated + +- Documentation updates. + +## [0.5.2] - 2022-09-27 + +### Added + +- Support for generating random `PartialOp`s using `proptest`. + +## [0.5.1] - 2022-09-27 + +### Changed + +- Updated repository location. + +## [0.5.0] - 2021-01-27 + +### Changed +- Updated `quickcheck` to version 1.0. Feature renamed to `quickcheck1`. +- Updated `tokio` to version 1.0. Feature renamed to `tokio1`. + +## [0.4.0] - 2020-09-24 + +### Changed +- Updated to quickcheck 0.9, tokio 0.2 and futures 0.3. + +For information about earlier versions, please review the [commit history](https://github.com/sunshowers-code/partial-io/commits/main). + +[0.5.4]: https://github.com/sunshowers-code/partial-io/releases/tag/0.5.4 +[0.5.3]: https://github.com/sunshowers-code/partial-io/releases/tag/0.5.3 +[0.5.2]: https://github.com/sunshowers-code/partial-io/releases/tag/0.5.2 +[0.5.1]: https://github.com/sunshowers-code/partial-io/releases/tag/0.5.1 + + +[0.5.0]: https://github.com/facebookincubator/rust-partial-io/releases/tag/0.5.0 +[0.4.0]: https://github.com/facebookincubator/rust-partial-io/releases/tag/0.4.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0a0cce3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c18f322 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing to partial-io +We want to make contributing to this project as easy and transparent as +possible. + +## Our Development Process +partial-io is developed on GitHub. We invite you to submit pull requests +as described below. + +## Pull Requests +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `main`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes (`cargo test --all-features`). +5. Make sure your code is well-formatted (using `cargo fmt`). +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +## Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +## License +By contributing to partial-io, you agree that your contributions will be +licensed under the LICENSE file in the root directory of this source tree. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..241fb73 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,569 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" + +[[package]] +name = "futures-executor" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" + +[[package]] +name = "futures-macro" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" + +[[package]] +name = "futures-task" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" + +[[package]] +name = "futures-util" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + +[[package]] +name = "partial-io" +version = "0.5.4" +dependencies = [ + "futures", + "itertools", + "once_cell", + "pin-project", + "proptest", + "quickcheck", + "rand", + "tokio", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.1", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "syn" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +dependencies = [ + "autocfg", + "bytes", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..50342f4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,114 @@ +# 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 = "2021" +rust-version = "1.56" +name = "partial-io" +version = "0.5.4" +authors = ["Rain "] +exclude = [ + "TARGETS", + "publish-docs.sh", + "rust-partial-io.iml", + ".travis.yml", + "**/*.bk", +] +description = "Helpers to test partial, interrupted and would-block I/O operations, with support for property-based testing through proptest and quickcheck." +documentation = "https://docs.rs/partial-io" +readme = "README.md" +keywords = [ + "partial", + "interrupted", + "tokio", + "quickcheck", + "proptest", +] +categories = [ + "development-tools::testing", + "asynchronous", +] +license = "MIT" +repository = "https://github.com/sunshowers-code/partial-io" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "doc_cfg", +] + +[[example]] +name = "buggy_write" +required-features = [ + "quickcheck1", + "proptest1", +] + +[dependencies.futures] +version = "0.3" +optional = true + +[dependencies.pin-project] +version = "1.0.4" +optional = true + +[dependencies.proptest] +version = "1.0.0" +optional = true + +[dependencies.quickcheck] +version = "1.0.3" +optional = true + +[dependencies.rand] +version = "0.8.5" +features = [ + "getrandom", + "small_rng", +] +optional = true + +[dependencies.tokio] +version = "1.21.1" +optional = true + +[dev-dependencies.itertools] +version = "0.10.5" + +[dev-dependencies.once_cell] +version = "1.15.0" + +[dev-dependencies.quickcheck] +version = "1.0.3" + +[dev-dependencies.tokio] +version = "1.21.1" +features = [ + "io-util", + "macros", + "rt-multi-thread", +] + +[features] +futures03 = [ + "futures", + "pin-project", +] +proptest1 = ["proptest"] +quickcheck1 = [ + "quickcheck", + "rand", +] +tokio1 = [ + "futures03", + "tokio", +] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..c112de3 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,55 @@ +[package] +name = "partial-io" +version = "0.5.4" +edition = "2021" +authors = ["Rain "] +description = "Helpers to test partial, interrupted and would-block I/O operations, with support for property-based testing through proptest and quickcheck." +documentation = "https://docs.rs/partial-io" +repository = "https://github.com/sunshowers-code/partial-io" +readme = "README.md" +keywords = ["partial", "interrupted", "tokio", "quickcheck", "proptest"] +categories = ["development-tools::testing", "asynchronous"] +license = "MIT" +rust-version = "1.56" +exclude = [ + "TARGETS", + "publish-docs.sh", + "rust-partial-io.iml", + ".travis.yml", + "**/*.bk", +] + +[dependencies] +futures = { version = "0.3", optional = true } +pin-project = { version = "1.0.4", optional = true } +proptest = { version = "1.0.0", optional = true } +quickcheck = { version = "1.0.3", optional = true } +rand = { version = "0.8.5", features = [ + "getrandom", + "small_rng", +], optional = true } +tokio = { version = "1.21.1", optional = true } + +[dev-dependencies] +itertools = "0.10.5" +once_cell = "1.15.0" +quickcheck = "1.0.3" +tokio = { version = "1.21.1", features = [ + "io-util", + "macros", + "rt-multi-thread", +] } + +[[example]] +name = "buggy_write" +required-features = ["quickcheck1", "proptest1"] + +[features] +futures03 = ["futures", "pin-project"] +tokio1 = ["futures03", "tokio"] +quickcheck1 = ["quickcheck", "rand"] +proptest1 = ["proptest"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "doc_cfg"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..39a9a06 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) The partial-io Contributors. +Originally copyright (c) Facebook, Inc. and its affiliates. + +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..bca8df5 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# partial-io + +[![partial-io on crates.io](https://img.shields.io/crates/v/partial-io)](https://crates.io/crates/partial-io) +[![Documentation (latest release)](https://docs.rs/partial-io/badge.svg)](https://docs.rs/partial-io/) +[![Documentation (main)](https://img.shields.io/badge/docs-main-brightgreen)](https://sunshowers-code.github.io/partial-io/rustdoc/partial_io/) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) + +Helpers for testing I/O behavior with partial, interrupted and blocking reads and writes. + +This library provides: + +* `PartialRead` and `PartialWrite`, which wrap existing `Read` and + `Write` implementations and allow specifying arbitrary behavior on the + next `read`, `write` or `flush` call. +* With the optional `futures03` and `tokio1` features, `PartialAsyncRead` and + `PartialAsyncWrite` to wrap existing `AsyncRead` and `AsyncWrite` + implementations. These implementations are task-aware, so they will know + how to pause and unpause tasks if they return a `WouldBlock` error. +* With the optional `proptest1` ([proptest]) and `quickcheck1` ([quickcheck]) features, + generation of random sequences of operations for property-based testing. See the + `proptest_types` and `quickcheck_types` documentation for more. + +## Motivation + +A `Read` or `Write` wrapper is conceptually simple but can be difficult to +get right, especially if the wrapper has an internal buffer. Common +issues include: + +* A partial read or write, even without an error, might leave the wrapper + in an invalid state ([example fix][1]). + +With the `AsyncRead` and `AsyncWrite` provided by `futures03` and `tokio1`: + +* A call to `read_to_end` or `write_all` within the wrapper might be partly + successful but then error out. These functions will return the error + without informing the caller of how much was read or written. Wrappers + with an internal buffer will want to advance their state corresponding + to the partial success, so they can't use `read_to_end` or `write_all` + ([example fix][2]). +* Instances must propagate `Poll::Pending` up, but that shouldn't leave + them in an invalid state. + +These situations can be hard to think about and hard to test. + +`partial-io` can help in two ways: + +1. For a known bug involving any of these situations, `partial-io` can help + you write a test. +2. With the `quickcheck1` feature enabled, `partial-io` can also help shake + out bugs in your wrapper. See `quickcheck_types` for more. + +## Examples + +```rust +use std::io::{self, Cursor, Read}; + +use partial_io::{PartialOp, PartialRead}; + +let data = b"Hello, world!".to_vec(); +let cursor = Cursor::new(data); // Cursor> implements io::Read +let ops = vec![PartialOp::Limited(7), PartialOp::Err(io::ErrorKind::Interrupted)]; +let mut partial_read = PartialRead::new(cursor, ops); + +let mut out = vec![0; 256]; + +// The first read will read 7 bytes. +assert_eq!(partial_read.read(&mut out).unwrap(), 7); +assert_eq!(&out[..7], b"Hello, "); +// The second read will fail with ErrorKind::Interrupted. +assert_eq!(partial_read.read(&mut out[7..]).unwrap_err().kind(), io::ErrorKind::Interrupted); +// The iterator has run out of operations, so it no longer truncates reads. +assert_eq!(partial_read.read(&mut out[7..]).unwrap(), 6); +assert_eq!(&out[..13], b"Hello, world!"); +``` + +For a real-world example, see the [tests in `zstd-rs`]. + +[proptest]: https://altsysrq.github.io/proptest-book/intro.html +[quickcheck]: https://docs.rs/quickcheck +[1]: https://github.com/gyscos/zstd-rs/commit/3123e418595f6badd5b06db2a14c4ff4555e7705 +[2]: https://github.com/gyscos/zstd-rs/commit/02dc9d9a3419618fc729542b45c96c32b0f178bb +[tests in `zstd-rs`]: https://github.com/gyscos/zstd-rs/blob/master/src/stream/mod.rs + +## Minimum supported Rust version + +The minimum supported Rust version (MSRV) is **1.56**. + +While a crate is pre-release status (0.x.x) it may have its MSRV bumped in a patch release. Once a crate has reached +1.x, any MSRV bump will be accompanied with a new minor version. + +## Contributing + +See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out. + +## License + +This project is available under the [MIT license](LICENSE). + + diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..bb6d6db --- /dev/null +++ b/README.tpl @@ -0,0 +1,30 @@ +# {{crate}} + +[![partial-io on crates.io](https://img.shields.io/crates/v/partial-io)](https://crates.io/crates/partial-io) +[![Documentation (latest release)](https://docs.rs/partial-io/badge.svg)](https://docs.rs/partial-io/) +[![Documentation (main)](https://img.shields.io/badge/docs-main-brightgreen)](https://sunshowers-code.github.io/partial-io/rustdoc/partial_io/) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) + +{{readme}} + +## Minimum supported Rust version + +The minimum supported Rust version (MSRV) is **1.56**. + +While a crate is pre-release status (0.x.x) it may have its MSRV bumped in a patch release. Once a crate has reached +1.x, any MSRV bump will be accompanied with a new minor version. + +## Contributing + +See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out. + +## License + +This project is available under the [MIT license](LICENSE). + + diff --git a/examples/buggy_write.rs b/examples/buggy_write.rs new file mode 100644 index 0000000..53296e2 --- /dev/null +++ b/examples/buggy_write.rs @@ -0,0 +1,216 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +//! An example of a buggy buffered writer that does not handle +//! `io::ErrorKind::Interrupted` properly. + +#![allow(dead_code)] + +use std::io::{self, Write}; + +/// A buffered writer whose `write` method is faulty. +pub struct BuggyWrite { + inner: W, + buf: Vec, + offset: usize, +} + +impl BuggyWrite { + pub fn new(inner: W) -> Self { + BuggyWrite { + inner, + buf: Vec::with_capacity(256), + offset: 0, + } + } + + fn write_from_offset(&mut self) -> io::Result<()> { + while self.offset < self.buf.len() { + self.offset += self.inner.write(&self.buf[self.offset..])?; + } + Ok(()) + } + + fn reset_buffer(&mut self) { + unsafe { + self.buf.set_len(0); + } + self.offset = 0; + } + + pub fn into_inner(self) -> W { + self.inner + } +} + +impl Write for BuggyWrite { + fn write(&mut self, buf: &[u8]) -> io::Result { + // Write out anything that is currently in the internal buffer. + if self.offset < self.buf.len() { + self.write_from_offset()?; + } + + // Reset the internal buffer. + self.reset_buffer(); + + // Read from the provided buffer. + self.buf.extend_from_slice(buf); + + // BUG: it is incorrect to call write immediately because if it fails, + // we'd have read some bytes from the buffer without telling the caller + // how many. + // XXX: To fix the bug, comment out the next line. + self.write_from_offset()?; + Ok(self.buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + // Flush out any data that can be flushed out. + while self.offset < self.buf.len() { + self.write_from_offset()?; + } + + // If that succeeded, reset the internal buffer. + self.reset_buffer(); + + // Flush the inner writer + self.inner.flush() + } +} + +fn main() { + check::check_write_is_buggy(); +} + +mod check { + use super::*; + use once_cell::sync::Lazy; + use partial_io::{PartialOp, PartialWrite}; + + // These strings have been chosen to be around the default size for + // quickcheck (100). With significantly smaller or larger inputs, the + // results might not be as good. + pub(crate) static HELLO_STR: Lazy> = Lazy::new(|| "Hello".repeat(50).into_bytes()); + pub(crate) static WORLD_STR: Lazy> = Lazy::new(|| "World".repeat(40).into_bytes()); + + pub fn check_write_is_buggy() { + let partial = vec![ + PartialOp::Err(io::ErrorKind::Interrupted), + PartialOp::Unlimited, + ]; + let (hello_res, world_res, flush_res, inner) = buggy_write_internal(partial); + assert_eq!(hello_res.unwrap_err().kind(), io::ErrorKind::Interrupted); + assert_eq!(world_res.unwrap(), 5 * 40); + flush_res.unwrap(); + + // Note that inner has both "Hello" and "World" in it, even though according + // to what the API returned it should only have had "World" in it. + let mut expected = Vec::new(); + expected.extend_from_slice(&HELLO_STR); + expected.extend_from_slice(&WORLD_STR); + assert_eq!(inner, expected); + } + + pub(crate) fn buggy_write_internal( + partial_iter: I, + ) -> ( + io::Result, + io::Result, + io::Result<()>, + Vec, + ) + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + let inner = Vec::new(); + let partial_writer = PartialWrite::new(inner, partial_iter); + + let mut buggy_write = BuggyWrite::new(partial_writer); + + // Try writing a couple of things into it. + let hello_res = buggy_write.write(&HELLO_STR); + let world_res = buggy_write.write(&WORLD_STR); + + // Flush the contents to make sure nothing remains in the internal buffer. + let flush_res = buggy_write.flush(); + + let inner = buggy_write.into_inner().into_inner(); + + (hello_res, world_res, flush_res, inner) + } +} + +#[cfg(test)] +mod tests { + //! Tests to demonstrate how to use partial-io to catch bugs in `buggy_write`. + use super::*; + use partial_io::{ + proptest_types::{interrupted_strategy, partial_op_strategy}, + quickcheck_types::{GenInterrupted, PartialWithErrors}, + }; + use proptest::{collection::vec, prelude::*}; + use quickcheck::{quickcheck, TestResult}; + + /// Test that BuggyWrite is actually buggy. + #[test] + fn test_check_write_is_buggy() { + check::check_write_is_buggy(); + } + + /// Test that quickcheck catches buggy writes. + /// + /// To run this test and see it fail, run this example with `--ignored`. To fix the bug, see the + /// section of this file marked "// BUG:". + #[test] + #[ignore] + fn test_quickcheck_buggy_write() { + quickcheck(quickcheck_buggy_write2 as fn(PartialWithErrors) -> TestResult); + } + + fn quickcheck_buggy_write2(partial: PartialWithErrors) -> TestResult { + let (hello_res, world_res, flush_res, inner) = check::buggy_write_internal(partial); + // If flush_res failed then we can't really do anything since we don't know + // how much was written internally. Otherwise hello_res and world_res should + // work. + if flush_res.is_err() { + return TestResult::discard(); + } + + let mut expected = Vec::new(); + if hello_res.is_ok() { + expected.extend_from_slice(&check::HELLO_STR); + } + if world_res.is_ok() { + expected.extend_from_slice(&check::WORLD_STR); + } + assert_eq!(inner, expected); + TestResult::passed() + } + + proptest! { + /// Test that proptest catches buggy writes. + /// + /// To run this test and see it fail, run this example with `--ignored`. To + /// fix the bug, see the section of this file marked "// BUG:". + #[test] + #[ignore] + fn test_proptest_buggy_write(ops in vec(partial_op_strategy(interrupted_strategy(), 128), 0..128)) { + let (hello_res, world_res, flush_res, inner) = check::buggy_write_internal(ops); + + // If flush_res failed then we can't really do anything since we don't know + // how much was written internally. Otherwise hello_res and world_res should + // work. + prop_assume!(flush_res.is_ok(), "if flush failed, we don't know what was written"); + + let mut expected = Vec::new(); + if hello_res.is_ok() { + expected.extend_from_slice(&check::HELLO_STR); + } + if world_res.is_ok() { + expected.extend_from_slice(&check::WORLD_STR); + } + prop_assert_eq!(inner, expected, "actual value matches expected"); + } + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e69de29 diff --git a/scripts/regenerate-readmes.sh b/scripts/regenerate-readmes.sh new file mode 100755 index 0000000..0e0865e --- /dev/null +++ b/scripts/regenerate-readmes.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Copyright (c) The cargo-guppy Contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + +# Regenerate readme files in this repository. + +set -eo pipefail + +cd "$(git rev-parse --show-toplevel)" +git ls-files | grep README.tpl$ | while read -r readme; do + dir=$(dirname "$readme") + cargo readme --project-root "$dir" > "$dir/README.md" +done diff --git a/src/async_read.rs b/src/async_read.rs new file mode 100644 index 0000000..3a58d3f --- /dev/null +++ b/src/async_read.rs @@ -0,0 +1,609 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +//! This module contains an `AsyncRead` wrapper that breaks its inputs up +//! according to a provided iterator. +//! +//! This is separate from `PartialWrite` because on `WouldBlock` errors, it +//! causes `futures` to try writing or flushing again. + +use crate::{futures_util::FuturesOps, PartialOp}; +use futures::prelude::*; +use pin_project::pin_project; +use std::{ + fmt, io, + pin::Pin, + task::{Context, Poll}, +}; + +/// A wrapper that breaks inner `AsyncRead` instances up according to the +/// provided iterator. +/// +/// Available with the `futures03` feature for `futures` traits, and with the `tokio1` feature for +/// `tokio` traits. +/// +/// # Examples +/// +/// This example uses `tokio`. +/// +/// ```rust +/// # #[cfg(feature = "tokio1")] +/// use partial_io::{PartialAsyncRead, PartialOp}; +/// # #[cfg(feature = "tokio1")] +/// use std::io::{self, Cursor}; +/// # #[cfg(feature = "tokio1")] +/// use tokio::io::AsyncReadExt; +/// +/// # #[cfg(feature = "tokio1")] +/// #[tokio::main] +/// async fn main() -> io::Result<()> { +/// let reader = Cursor::new(vec![1, 2, 3, 4]); +/// // Sequential calls to `poll_read()` and the other `poll_` methods simulate the following behavior: +/// let iter = vec![ +/// PartialOp::Err(io::ErrorKind::WouldBlock), // A not-ready state. +/// PartialOp::Limited(2), // Only allow 2 bytes to be read. +/// PartialOp::Err(io::ErrorKind::InvalidData), // Error from the underlying stream. +/// PartialOp::Unlimited, // Allow as many bytes to be read as possible. +/// ]; +/// let mut partial_reader = PartialAsyncRead::new(reader, iter); +/// let mut out = vec![0; 256]; +/// +/// // This causes poll_read to be called twice, yielding after the first call (WouldBlock). +/// assert_eq!(partial_reader.read(&mut out).await?, 2, "first read with Limited(2)"); +/// assert_eq!(&out[..4], &[1, 2, 0, 0]); +/// +/// // This next call returns an error. +/// assert_eq!( +/// partial_reader.read(&mut out[2..]).await.unwrap_err().kind(), +/// io::ErrorKind::InvalidData, +/// ); +/// +/// // And this one causes the last two bytes to be written. +/// assert_eq!(partial_reader.read(&mut out[2..]).await?, 2, "second read with Unlimited"); +/// assert_eq!(&out[..4], &[1, 2, 3, 4]); +/// +/// Ok(()) +/// } +/// +/// # #[cfg(not(feature = "tokio1"))] +/// # fn main() { +/// # assert!(true, "dummy test"); +/// # } +/// ``` +#[pin_project] +pub struct PartialAsyncRead { + #[pin] + inner: R, + ops: FuturesOps, +} + +impl PartialAsyncRead { + /// Creates a new `PartialAsyncRead` wrapper over the reader with the specified `PartialOp`s. + pub fn new(inner: R, iter: I) -> Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + PartialAsyncRead { + inner, + ops: FuturesOps::new(iter), + } + } + + /// Sets the `PartialOp`s for this reader. + pub fn set_ops(&mut self, iter: I) -> &mut Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + self.ops.replace(iter); + self + } + + /// Sets the `PartialOp`s for this reader in a pinned context. + pub fn pin_set_ops(self: Pin<&mut Self>, iter: I) -> Pin<&mut Self> + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + let mut this = self; + this.as_mut().project().ops.replace(iter); + this + } + + /// Returns a shared reference to the underlying reader. + pub fn get_ref(&self) -> &R { + &self.inner + } + + /// Returns a mutable reference to the underlying reader. + pub fn get_mut(&mut self) -> &mut R { + &mut self.inner + } + + /// Returns a pinned mutable reference to the underlying reader. + pub fn pin_get_mut(self: Pin<&mut Self>) -> Pin<&mut R> { + self.project().inner + } + + /// Consumes this wrapper, returning the underlying reader. + pub fn into_inner(self) -> R { + self.inner + } +} + +// --- +// Futures impls +// --- + +impl AsyncRead for PartialAsyncRead +where + R: AsyncRead, +{ + #[inline] + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll> { + let this = self.project(); + let inner = this.inner; + let len = buf.len(); + + this.ops.poll_impl( + cx, + |cx, len| match len { + Some(len) => inner.poll_read(cx, &mut buf[..len]), + None => inner.poll_read(cx, buf), + }, + len, + "error during poll_read, generated by partial-io", + ) + } + + // TODO: do we need to implement poll_read_vectored? It's a bit tricky to do. +} + +impl AsyncBufRead for PartialAsyncRead +where + R: AsyncBufRead, +{ + fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl_no_limit( + cx, + |cx| inner.poll_fill_buf(cx), + "error during poll_read, generated by partial-io", + ) + } + + #[inline] + fn consume(self: Pin<&mut Self>, amt: usize) { + self.project().inner.consume(amt) + } +} + +/// This is a forwarding impl to support duplex structs. +impl AsyncWrite for PartialAsyncRead +where + R: AsyncWrite, +{ + fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll> { + self.project().inner.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().inner.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().inner.poll_close(cx) + } +} + +/// This is a forwarding impl to support duplex structs. +impl AsyncSeek for PartialAsyncRead +where + R: AsyncSeek, +{ + #[inline] + fn poll_seek( + self: Pin<&mut Self>, + cx: &mut Context, + pos: io::SeekFrom, + ) -> Poll> { + self.project().inner.poll_seek(cx, pos) + } +} + +// --- +// Tokio impls +// --- + +#[cfg(feature = "tokio1")] +pub(crate) mod tokio_impl { + use super::PartialAsyncRead; + use std::{ + io::{self, SeekFrom}, + pin::Pin, + task::{Context, Poll}, + }; + use tokio::io::{AsyncBufRead, AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; + + impl AsyncRead for PartialAsyncRead + where + R: AsyncRead, + { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let inner = this.inner; + let capacity = buf.capacity(); + + this.ops.poll_impl( + cx, + |cx, len| match len { + Some(len) => { + buf.with_limited(len, |limited_buf| inner.poll_read(cx, limited_buf)) + } + None => inner.poll_read(cx, buf), + }, + capacity, + "error during poll_read, generated by partial-io", + ) + } + } + + /// Extensions to `tokio`'s `ReadBuf`. + /// + /// Requires the `tokio1` feature to be enabled. + pub trait ReadBufExt { + /// Convert this `ReadBuf` into a limited one backed by the same storage, then + /// call the callback with this limited instance.. + /// + /// Any changes to the `ReadBuf` made by the callback are reflected in the original + /// `ReadBuf`. + fn with_limited(&mut self, limit: usize, callback: F) -> T + where + F: FnOnce(&mut ReadBuf<'_>) -> T; + } + + impl<'a> ReadBufExt for ReadBuf<'a> { + fn with_limited(&mut self, limit: usize, callback: F) -> T + where + F: FnOnce(&mut ReadBuf<'_>) -> T, + { + // Use limit to set upper limits on the capacity and both cursors. + let capacity_limit = self.capacity().min(limit); + let old_initialized_len = self.initialized().len().min(limit); + let old_filled_len = self.filled().len().min(limit); + + // SAFETY: We assume that the input buf's initialized length is trustworthy. + let mut limited_buf = unsafe { + let inner_mut = &mut self.inner_mut()[..capacity_limit]; + let mut limited_buf = ReadBuf::uninit(inner_mut); + // Note: assume_init adds the passed-in value to self.filled, but for a freshly created + // uninitialized buffer, self.filled is 0. The value of filled is updated below + // with the set_filled() call. + limited_buf.assume_init(old_initialized_len); + limited_buf + }; + limited_buf.set_filled(old_filled_len); + + // Call the callback. + let ret = callback(&mut limited_buf); + + // The callback may have modified the cursors in `limited_buf` -- if so, port them back to + // the original. + let new_initialized_len = limited_buf.initialized().len(); + let new_filled_len = limited_buf.filled().len(); + + if new_initialized_len > old_initialized_len { + // SAFETY: We assume that if new_initialized_len > old_initialized_len, that + // the extra bytes were initialized by the callback. + unsafe { + // Note: assume_init adds the passed-in value to buf.filled.len(). + self.assume_init(new_initialized_len - self.filled().len()); + } + } + + if new_filled_len != old_filled_len { + // This can happen if either: + // * old_filled_len < limit, and the callback filled some more bytes into buf -> + // reflect that in the original buffer. + // * old_filled_len <= limit, and the callback *shortened* the filled bytes -> reflect + // that in the original buffer as well. + // + // (Note if old_filled_len == limit, then new_filled_len cannot be greater than + // old_filled_len since it's at the limit already.) + self.set_filled(new_filled_len); + } + + ret + } + } + + impl AsyncBufRead for PartialAsyncRead + where + R: AsyncBufRead, + { + fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl_no_limit( + cx, + |cx| inner.poll_fill_buf(cx), + "error during poll_fill_buf, generated by partial-io", + ) + } + + fn consume(self: Pin<&mut Self>, amt: usize) { + self.project().inner.consume(amt) + } + } + + /// This is a forwarding impl to support duplex structs. + impl AsyncWrite for PartialAsyncRead + where + R: AsyncWrite, + { + #[inline] + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + self.project().inner.poll_write(cx, buf) + } + + #[inline] + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().inner.poll_flush(cx) + } + + #[inline] + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().inner.poll_shutdown(cx) + } + } + + /// This is a forwarding impl to support duplex structs. + impl AsyncSeek for PartialAsyncRead + where + R: AsyncSeek, + { + #[inline] + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> { + self.project().inner.start_seek(position) + } + + #[inline] + fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().inner.poll_complete(cx) + } + } + + #[cfg(test)] + mod tests { + use super::*; + use itertools::Itertools; + use std::mem::MaybeUninit; + + // with_limited is pretty complex: test that it works properly. + #[test] + fn test_with_limited() { + const CAPACITY: usize = 256; + + let inputs = vec![ + // Columns are (filled, initialized). The capacity is always 256. + + // Fully filled, fully initialized buffer. + (256, 256), + // Partly filled, fully initialized buffer. + (64, 256), + // Unfilled, fully initialized buffer. + (0, 256), + // Fully filled, partly initialized buffer. + (128, 128), + // Partly filled, partly initialized buffer. + (64, 128), + // Unfilled, partly initialized buffer. + (0, 128), + // Unfilled, uninitialized buffer. + (0, 0), + ]; + // Test a series of limits for every possible case. + let limits = vec![0, 32, 64, 128, 192, 256, 384]; + + for ((filled, initialized), limit) in inputs.into_iter().cartesian_product(limits) { + // Create an uninitialized array of `MaybeUninit` for storage. The `assume_init` is + // safe because the type we are claiming to have initialized here is a + // bunch of `MaybeUninit`s, which do not require initialization. + let mut storage: [MaybeUninit; CAPACITY] = + unsafe { MaybeUninit::uninit().assume_init() }; + let mut buf = ReadBuf::uninit(&mut storage); + buf.initialize_unfilled_to(initialized); + buf.set_filled(filled); + + println!("*** limit = {}, original buf = {:?}", limit, buf); + + // --- + // Test that making no changes to the limited buffer causes no changes to the + // original buffer. + // --- + buf.with_limited(limit, |limited_buf| { + println!(" * do-nothing: limited buf = {:?}", limited_buf); + assert!( + limited_buf.capacity() <= limit, + "limit is applied to capacity" + ); + assert!( + limited_buf.initialized().len() <= limit, + "limit is applied to initialized len" + ); + assert!( + limited_buf.filled().len() <= limit, + "limit is applied to filled len" + ); + }); + + assert_eq!( + buf.filled().len(), + filled, + "do-nothing -> filled is the same as before" + ); + assert_eq!( + buf.initialized().len(), + initialized, + "do-nothing -> initialized is the same as before" + ); + + // --- + // Test that set_filled with a smaller value is reflected in the original buffer. + // --- + let new_filled = buf.with_limited(limit, |limited_buf| { + println!(" * halve-filled: limited buf = {:?}", limited_buf); + let new_filled = limited_buf.filled().len() / 2; + limited_buf.set_filled(new_filled); + println!(" * halve-filled: after = {:?}", limited_buf); + new_filled + }); + + match new_filled.cmp(&limit) { + std::cmp::Ordering::Less => { + assert_eq!( + buf.filled().len(), + new_filled, + "halve-filled, new filled < limit -> filled is updated" + ); + } + std::cmp::Ordering::Equal => { + assert_eq!(limit, 0, "halve-filled, new filled == limit -> limit = 0"); + assert_eq!( + buf.filled().len(), + filled, + "halve-filled, new filled == limit -> filled stays the same" + ); + } + std::cmp::Ordering::Greater => { + panic!("new_filled {} must be <= limit {}", new_filled, limit); + } + } + + assert_eq!( + buf.initialized().len(), + initialized, + "halve-filled -> initialized is same as before" + ); + + // --- + // Test that pushing a single byte is reflected in the original buffer. + // --- + if filled < limit.min(CAPACITY) { + // Reset the ReadBuf. + let mut storage: [MaybeUninit; CAPACITY] = + unsafe { MaybeUninit::uninit().assume_init() }; + let mut buf = ReadBuf::uninit(&mut storage); + buf.initialize_unfilled_to(initialized); + buf.set_filled(filled); + + buf.with_limited(limit, |limited_buf| { + println!(" * push-one-byte: limited buf = {:?}", limited_buf); + limited_buf.put_slice(&[42]); + println!(" * push-one-byte: after = {:?}", limited_buf); + }); + + assert_eq!( + buf.filled().len(), + filled + 1, + "push-one-byte, filled incremented by 1" + ); + assert_eq!( + buf.filled()[filled], + 42, + "push-one-byte, correct byte was pushed" + ); + if filled == initialized { + assert_eq!( + buf.initialized().len(), + initialized + 1, + "push-one-byte, filled == initialized -> initialized incremented by 1" + ); + } else { + assert_eq!( + buf.initialized().len(), + initialized, + "push-one-byte, filled < initialized -> initialized stays the same" + ); + } + } + + // --- + // Test that initializing unfilled bytes is reflected in the original buffer. + // --- + if initialized <= limit.min(CAPACITY) { + // Reset the ReadBuf. + let mut storage: [MaybeUninit; CAPACITY] = + unsafe { MaybeUninit::uninit().assume_init() }; + let mut buf = ReadBuf::uninit(&mut storage); + buf.initialize_unfilled_to(initialized); + buf.set_filled(filled); + + buf.with_limited(limit, |limited_buf| { + println!(" * initialize-unfilled: limited buf = {:?}", limited_buf); + limited_buf.initialize_unfilled(); + println!(" * initialize-unfilled: after = {:?}", limited_buf); + }); + + assert_eq!( + buf.filled().len(), + filled, + "initialize-unfilled, filled stays the same" + ); + assert_eq!( + buf.initialized().len(), + limit.min(CAPACITY), + "initialize-unfilled, initialized is capped at the limit" + ); + // Actually access the bytes and ensure this doesn't crash. + assert_eq!( + buf.initialized(), + vec![0; buf.initialized().len()], + "initialize-unfilled, bytes are correct" + ); + } + } + } + } +} + +impl fmt::Debug for PartialAsyncRead +where + R: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PartialAsyncRead") + .field("inner", &self.inner) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::fs::File; + + use crate::tests::assert_send; + + #[test] + fn test_sendable() { + assert_send::>(); + } +} diff --git a/src/async_write.rs b/src/async_write.rs new file mode 100644 index 0000000..acbc79b --- /dev/null +++ b/src/async_write.rs @@ -0,0 +1,371 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +//! This module contains an `AsyncWrite` wrapper that breaks writes up +//! according to a provided iterator. +//! +//! This is separate from `PartialWrite` because on `WouldBlock` errors, it +//! causes `futures` to try writing or flushing again. + +use crate::{futures_util::FuturesOps, PartialOp}; +use futures::{io, prelude::*}; +use pin_project::pin_project; +use std::{ + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +/// A wrapper that breaks inner `AsyncWrite` instances up according to the +/// provided iterator. +/// +/// Available with the `futures03` feature for `futures` traits, and with the `tokio1` feature for +/// `tokio` traits. +/// +/// # Examples +/// +/// This example uses `tokio`. +/// +/// ```rust +/// # #[cfg(feature = "tokio1")] +/// use partial_io::{PartialAsyncWrite, PartialOp}; +/// # #[cfg(feature = "tokio1")] +/// use std::io::{self, Cursor}; +/// # #[cfg(feature = "tokio1")] +/// use tokio::io::AsyncWriteExt; +/// +/// # #[cfg(feature = "tokio1")] +/// #[tokio::main] +/// async fn main() -> io::Result<()> { +/// let writer = Cursor::new(Vec::new()); +/// // Sequential calls to `poll_write()` and the other `poll_` methods simulate the following behavior: +/// let iter = vec![ +/// PartialOp::Err(io::ErrorKind::WouldBlock), // A not-ready state. +/// PartialOp::Limited(2), // Only allow 2 bytes to be written. +/// PartialOp::Err(io::ErrorKind::InvalidData), // Error from the underlying stream. +/// PartialOp::Unlimited, // Allow as many bytes to be written as possible. +/// ]; +/// let mut partial_writer = PartialAsyncWrite::new(writer, iter); +/// let in_data = vec![1, 2, 3, 4]; +/// +/// // This causes poll_write to be called twice, yielding after the first call (WouldBlock). +/// assert_eq!(partial_writer.write(&in_data).await?, 2); +/// let cursor_ref = partial_writer.get_ref(); +/// let out = cursor_ref.get_ref(); +/// assert_eq!(&out[..], &[1, 2]); +/// +/// // This next call returns an error. +/// assert_eq!( +/// partial_writer.write(&in_data[2..]).await.unwrap_err().kind(), +/// io::ErrorKind::InvalidData, +/// ); +/// +/// // And this one causes the last two bytes to be written. +/// assert_eq!(partial_writer.write(&in_data[2..]).await?, 2); +/// let cursor_ref = partial_writer.get_ref(); +/// let out = cursor_ref.get_ref(); +/// assert_eq!(&out[..], &[1, 2, 3, 4]); +/// +/// Ok(()) +/// } +/// +/// # #[cfg(not(feature = "tokio1"))] +/// # fn main() { +/// # assert!(true, "dummy test"); +/// # } +/// ``` +#[pin_project] +pub struct PartialAsyncWrite { + #[pin] + inner: W, + ops: FuturesOps, +} + +impl PartialAsyncWrite { + /// Creates a new `PartialAsyncWrite` wrapper over the writer with the specified `PartialOp`s. + pub fn new(inner: W, iter: I) -> Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + PartialAsyncWrite { + inner, + ops: FuturesOps::new(iter), + } + } + + /// Sets the `PartialOp`s for this writer. + pub fn set_ops(&mut self, iter: I) -> &mut Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + self.ops.replace(iter); + self + } + + /// Sets the `PartialOp`s for this writer in a pinned context. + pub fn pin_set_ops(self: Pin<&mut Self>, iter: I) -> Pin<&mut Self> + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + let mut this = self; + this.as_mut().project().ops.replace(iter); + this + } + + /// Returns a shared reference to the underlying writer. + pub fn get_ref(&self) -> &W { + &self.inner + } + + /// Returns a mutable reference to the underlying writer. + pub fn get_mut(&mut self) -> &mut W { + &mut self.inner + } + + /// Returns a pinned mutable reference to the underlying writer. + pub fn pin_get_mut(self: Pin<&mut Self>) -> Pin<&mut W> { + self.project().inner + } + + /// Consumes this wrapper, returning the underlying writer. + pub fn into_inner(self) -> W { + self.inner + } +} + +// --- +// Futures impls +// --- + +impl AsyncWrite for PartialAsyncWrite +where + W: AsyncWrite, +{ + fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl( + cx, + |cx, len| match len { + Some(len) => inner.poll_write(cx, &buf[..len]), + None => inner.poll_write(cx, buf), + }, + buf.len(), + "error during poll_write, generated by partial-io", + ) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl_no_limit( + cx, + |cx| inner.poll_flush(cx), + "error during poll_flush, generated by partial-io", + ) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl_no_limit( + cx, + |cx| inner.poll_close(cx), + "error during poll_close, generated by partial-io", + ) + } +} + +/// This is a forwarding impl to support duplex structs. +impl AsyncRead for PartialAsyncWrite +where + W: AsyncRead, +{ + #[inline] + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll> { + self.project().inner.poll_read(cx, buf) + } + + #[inline] + fn poll_read_vectored( + self: Pin<&mut Self>, + cx: &mut Context, + bufs: &mut [io::IoSliceMut], + ) -> Poll> { + self.project().inner.poll_read_vectored(cx, bufs) + } +} + +/// This is a forwarding impl to support duplex structs. +impl AsyncBufRead for PartialAsyncWrite +where + W: AsyncBufRead, +{ + #[inline] + fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().inner.poll_fill_buf(cx) + } + + #[inline] + fn consume(self: Pin<&mut Self>, amt: usize) { + self.project().inner.consume(amt) + } +} + +/// This is a forwarding impl to support duplex structs. +impl AsyncSeek for PartialAsyncWrite +where + W: AsyncSeek, +{ + #[inline] + fn poll_seek( + self: Pin<&mut Self>, + cx: &mut Context, + pos: io::SeekFrom, + ) -> Poll> { + self.project().inner.poll_seek(cx, pos) + } +} + +// --- +// Tokio impls +// --- + +#[cfg(feature = "tokio1")] +mod tokio_impl { + use super::PartialAsyncWrite; + use std::{ + io::{self, SeekFrom}, + pin::Pin, + task::{Context, Poll}, + }; + use tokio::io::{AsyncBufRead, AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; + + impl AsyncWrite for PartialAsyncWrite + where + W: AsyncWrite, + { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl( + cx, + |cx, len| match len { + Some(len) => inner.poll_write(cx, &buf[..len]), + None => inner.poll_write(cx, buf), + }, + buf.len(), + "error during poll_write, generated by partial-io", + ) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl_no_limit( + cx, + |cx| inner.poll_flush(cx), + "error during poll_flush, generated by partial-io", + ) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + let inner = this.inner; + + this.ops.poll_impl_no_limit( + cx, + |cx| inner.poll_shutdown(cx), + "error during poll_shutdown, generated by partial-io", + ) + } + } + + /// This is a forwarding impl to support duplex structs. + impl AsyncRead for PartialAsyncWrite + where + W: AsyncRead, + { + #[inline] + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.project().inner.poll_read(cx, buf) + } + } + + /// This is a forwarding impl to support duplex structs. + impl AsyncBufRead for PartialAsyncWrite + where + W: AsyncBufRead, + { + #[inline] + fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().inner.poll_fill_buf(cx) + } + + #[inline] + fn consume(self: Pin<&mut Self>, amt: usize) { + self.project().inner.consume(amt) + } + } + + /// This is a forwarding impl to support duplex structs. + impl AsyncSeek for PartialAsyncWrite + where + W: AsyncSeek, + { + #[inline] + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> { + self.project().inner.start_seek(position) + } + + #[inline] + fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().inner.poll_complete(cx) + } + } +} + +impl fmt::Debug for PartialAsyncWrite +where + W: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PartialAsyncWrite") + .field("inner", &self.inner) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::fs::File; + + use crate::tests::assert_send; + + #[test] + fn test_sendable() { + assert_send::>(); + } +} diff --git a/src/futures_util.rs b/src/futures_util.rs new file mode 100644 index 0000000..7b6fbe8 --- /dev/null +++ b/src/futures_util.rs @@ -0,0 +1,96 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +use crate::{make_ops, PartialOp}; +use std::{ + cmp, io, + task::{Context, Poll}, +}; + +pub(crate) struct FuturesOps { + ops: Box + Send>, +} + +impl FuturesOps { + /// Creates a new instance of `TokioOps`. + pub(crate) fn new(iter: I) -> Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + Self { + ops: make_ops(iter), + } + } + + /// Replaces ops with a new iterator. + pub(crate) fn replace(&mut self, iter: I) + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + self.ops = make_ops(iter) + } + + /// Helper for poll methods. + /// + /// `cb` is the callback that implements the actual logic. The second argument is `Some(n)` to + /// limit the number of bytes being written, or `None` for unlimited. + pub(crate) fn poll_impl( + &mut self, + cx: &mut Context, + cb: impl FnOnce(&mut Context, Option) -> Poll>, + remaining: usize, + err_str: &'static str, + ) -> Poll> { + loop { + match self.ops.next() { + Some(PartialOp::Limited(n)) => { + let len = cmp::min(n, remaining); + break cb(cx, Some(len)); + } + Some(PartialOp::Err(kind)) => { + if kind == io::ErrorKind::WouldBlock { + // Async* instances must convert WouldBlock errors to Poll::Pending and + // reschedule the task. + cx.waker().wake_by_ref(); + break Poll::Pending; + } else if kind == io::ErrorKind::Interrupted { + // Async* instances must retry on Interrupted errors. + continue; + } else { + break Poll::Ready(Err(io::Error::new(kind, err_str))); + } + } + Some(PartialOp::Unlimited) | None => break cb(cx, None), + } + } + } + + /// Helper for poll methods that ignore the length specified in `PartialOp::Limited`. + pub(crate) fn poll_impl_no_limit( + &mut self, + cx: &mut Context, + cb: impl FnOnce(&mut Context) -> Poll>, + err_str: &'static str, + ) -> Poll> { + loop { + match self.ops.next() { + Some(PartialOp::Err(kind)) => { + if kind == io::ErrorKind::WouldBlock { + // Async* instances must convert WouldBlock errors to Poll::Pending and + // reschedule the task. + cx.waker().wake_by_ref(); + break Poll::Pending; + } else if kind == io::ErrorKind::Interrupted { + // Async* instances must retry on interrupted errors. + continue; + } else { + break Poll::Ready(Err(io::Error::new(kind, err_str))); + } + } + _ => break cb(cx), + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..46bfac8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,151 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +#![cfg_attr(doc_cfg, feature(doc_auto_cfg))] + +//! Helpers for testing I/O behavior with partial, interrupted and blocking reads and writes. +//! +//! This library provides: +//! +//! * `PartialRead` and `PartialWrite`, which wrap existing `Read` and +//! `Write` implementations and allow specifying arbitrary behavior on the +//! next `read`, `write` or `flush` call. +//! * With the optional `futures03` and `tokio1` features, `PartialAsyncRead` and +//! `PartialAsyncWrite` to wrap existing `AsyncRead` and `AsyncWrite` +//! implementations. These implementations are task-aware, so they will know +//! how to pause and unpause tasks if they return a `WouldBlock` error. +//! * With the optional `proptest1` ([proptest]) and `quickcheck1` ([quickcheck]) features, +//! generation of random sequences of operations for property-based testing. See the +//! `proptest_types` and `quickcheck_types` documentation for more. +//! +//! # Motivation +//! +//! A `Read` or `Write` wrapper is conceptually simple but can be difficult to +//! get right, especially if the wrapper has an internal buffer. Common +//! issues include: +//! +//! * A partial read or write, even without an error, might leave the wrapper +//! in an invalid state ([example fix][1]). +//! +//! With the `AsyncRead` and `AsyncWrite` provided by `futures03` and `tokio1`: +//! +//! * A call to `read_to_end` or `write_all` within the wrapper might be partly +//! successful but then error out. These functions will return the error +//! without informing the caller of how much was read or written. Wrappers +//! with an internal buffer will want to advance their state corresponding +//! to the partial success, so they can't use `read_to_end` or `write_all` +//! ([example fix][2]). +//! * Instances must propagate `Poll::Pending` up, but that shouldn't leave +//! them in an invalid state. +//! +//! These situations can be hard to think about and hard to test. +//! +//! `partial-io` can help in two ways: +//! +//! 1. For a known bug involving any of these situations, `partial-io` can help +//! you write a test. +//! 2. With the `quickcheck1` feature enabled, `partial-io` can also help shake +//! out bugs in your wrapper. See `quickcheck_types` for more. +//! +//! # Examples +//! +//! ```rust +//! use std::io::{self, Cursor, Read}; +//! +//! use partial_io::{PartialOp, PartialRead}; +//! +//! let data = b"Hello, world!".to_vec(); +//! let cursor = Cursor::new(data); // Cursor> implements io::Read +//! let ops = vec![PartialOp::Limited(7), PartialOp::Err(io::ErrorKind::Interrupted)]; +//! let mut partial_read = PartialRead::new(cursor, ops); +//! +//! let mut out = vec![0; 256]; +//! +//! // The first read will read 7 bytes. +//! assert_eq!(partial_read.read(&mut out).unwrap(), 7); +//! assert_eq!(&out[..7], b"Hello, "); +//! // The second read will fail with ErrorKind::Interrupted. +//! assert_eq!(partial_read.read(&mut out[7..]).unwrap_err().kind(), io::ErrorKind::Interrupted); +//! // The iterator has run out of operations, so it no longer truncates reads. +//! assert_eq!(partial_read.read(&mut out[7..]).unwrap(), 6); +//! assert_eq!(&out[..13], b"Hello, world!"); +//! ``` +//! +//! For a real-world example, see the [tests in `zstd-rs`]. +//! +//! [proptest]: https://altsysrq.github.io/proptest-book/intro.html +//! [quickcheck]: https://docs.rs/quickcheck +//! [1]: https://github.com/gyscos/zstd-rs/commit/3123e418595f6badd5b06db2a14c4ff4555e7705 +//! [2]: https://github.com/gyscos/zstd-rs/commit/02dc9d9a3419618fc729542b45c96c32b0f178bb +//! [tests in `zstd-rs`]: https://github.com/gyscos/zstd-rs/blob/master/src/stream/mod.rs + +#[cfg(feature = "futures03")] +mod async_read; +#[cfg(feature = "futures03")] +mod async_write; +#[cfg(feature = "futures03")] +mod futures_util; +#[cfg(feature = "proptest1")] +pub mod proptest_types; +#[cfg(feature = "quickcheck1")] +pub mod quickcheck_types; +mod read; +mod write; + +use std::io; + +#[cfg(feature = "tokio1")] +pub use crate::async_read::tokio_impl::ReadBufExt; +#[cfg(feature = "futures03")] +pub use crate::async_read::PartialAsyncRead; +#[cfg(feature = "futures03")] +pub use crate::async_write::PartialAsyncWrite; +pub use crate::{read::PartialRead, write::PartialWrite}; + +/// What to do the next time an IO operation is performed. +/// +/// This is not the same as `io::Result>` because it contains +/// `io::ErrorKind` instances, not `io::Error` instances. This allows it to be +/// clonable. +#[derive(Clone, Debug)] +pub enum PartialOp { + /// Limit the next IO operation to a certain number of bytes. + /// + /// The wrapper will call into the inner `Read` or `Write` + /// instance. Depending on what the underlying operation does, this may + /// return an error or a fewer number of bytes. + /// + /// Some methods like `Write::flush` and `AsyncWrite::poll_flush` don't + /// have a limit. For these methods, `Limited(n)` behaves the same as + /// `Unlimited`. + Limited(usize), + + /// Do not limit the next IO operation. + /// + /// The wrapper will call into the inner `Read` or `Write` + /// instance. Depending on what the underlying operation does, this may + /// return an error or a limited number of bytes. + Unlimited, + + /// Return an error instead of calling into the underlying operation. + /// + /// For methods on `Async` traits: + /// * `ErrorKind::WouldBlock` is translated to `Poll::Pending` and the task + /// is scheduled to be woken up in the future. + /// * `ErrorKind::Interrupted` causes a retry. + Err(io::ErrorKind), +} + +#[inline] +fn make_ops(iter: I) -> Box + Send> +where + I: IntoIterator + 'static, + I::IntoIter: Send, +{ + Box::new(iter.into_iter().fuse()) +} + +#[cfg(test)] +mod tests { + pub fn assert_send() {} +} diff --git a/src/proptest_types.rs b/src/proptest_types.rs new file mode 100644 index 0000000..204690d --- /dev/null +++ b/src/proptest_types.rs @@ -0,0 +1,77 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +//! Proptest support for partial IO operations. +//! +//! This module allows sequences of [`PartialOp`]s to be randomly generated. These +//! sequences can then be fed into a [`PartialRead`](crate::PartialRead), +//! [`PartialWrite`](crate::PartialWrite), [`PartialAsyncRead`](crate::PartialAsyncRead) or +//! [`PartialAsyncWrite`](crate::PartialAsyncWrite). +//! +//! Once `proptest` has identified a failing test case, it will shrink the sequence of `PartialOp`s +//! and find a minimal test case. This minimal case can then be used to reproduce the issue. +//! +//! Basic implementations are provided for: +//! - generating errors some of the time +//! - generating [`PartialOp`] instances, given a way to generate errors. +//! +//! # Examples +//! +//! ```rust +//! use partial_io::proptest_types::{partial_op_strategy, interrupted_strategy}; +//! use proptest::{collection::vec, prelude::*}; +//! +//! proptest! { +//! #[test] +//! fn proptest_something(ops: vec(partial_op_strategy(interrupted_strategy(), 128), 0..128)) { +//! // Example buffer to read from, substitute with your own. +//! let reader = std::io::repeat(42); +//! let partial_reader = PartialRead::new(reader, ops); +//! // ... +//! +//! true +//! } +//! } +//! ``` +//! +//! For a detailed example, see `examples/buggy_write.rs` in this repository. + +use crate::PartialOp; +use proptest::{option::weighted, prelude::*}; +use std::io; + +/// Returns a strategy that generates `PartialOp` instances given a way to generate errors. +/// +/// To not generate any errors and only limit reads, pass in `Just(None)` as the error strategy. +pub fn partial_op_strategy( + error_strategy: impl Strategy>, + limit_bytes: usize, +) -> impl Strategy { + // Don't generate 0 because for writers it can mean that writes are no longer accepted. + (error_strategy, 1..=limit_bytes).prop_map(|(error_kind, limit)| match error_kind { + Some(kind) => PartialOp::Err(kind), + None => PartialOp::Limited(limit), + }) +} + +/// Returns a strategy that generates `Interrupted` errors 20% of the time. +pub fn interrupted_strategy() -> impl Strategy> { + weighted(0.2, Just(io::ErrorKind::Interrupted)) +} + +/// Returns a strategy that generates `WouldBlock` errors 20% of the time. +pub fn would_block_strategy() -> impl Strategy> { + weighted(0.2, Just(io::ErrorKind::WouldBlock)) +} + +/// Returns a strategy that generates `Interrupted` errors 10% of the time and `WouldBlock` errors +/// 10% of the time. +pub fn interrupted_would_block_strategy() -> impl Strategy> { + weighted( + 0.2, + prop_oneof![ + Just(io::ErrorKind::Interrupted), + Just(io::ErrorKind::WouldBlock), + ], + ) +} diff --git a/src/quickcheck_types.rs b/src/quickcheck_types.rs new file mode 100644 index 0000000..314d863 --- /dev/null +++ b/src/quickcheck_types.rs @@ -0,0 +1,194 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +//! `QuickCheck` support for partial IO operations. +//! +//! This module allows sequences of [`PartialOp`]s to be randomly generated. These +//! sequences can then be fed into a [`PartialRead`], [`PartialWrite`], +//! [`PartialAsyncRead`] or [`PartialAsyncWrite`]. +//! +//! Once `quickcheck` has identified a failing test case, it will shrink the +//! sequence of `PartialOp`s and find a minimal test case. This minimal case can +//! then be used to reproduce the issue. +//! +//! To generate random sequences of operations, write a `quickcheck` test with a +//! `PartialWithErrors` input, where `GE` implements [`GenError`]. Then pass +//! the sequence in as the second argument to the partial wrapper. +//! +//! Several implementations of `GenError` are provided. These can be used to +//! customize the sorts of errors generated. For even more customization, you +//! can write your own `GenError` implementation. +//! +//! # Examples +//! +//! ```rust +//! use partial_io::quickcheck_types::{GenInterrupted, PartialWithErrors}; +//! use quickcheck::quickcheck; +//! +//! quickcheck! { +//! fn test_something(seq: PartialWithErrors) -> bool { +//! // Example buffer to read from, substitute with your own. +//! let reader = std::io::repeat(42); +//! let partial_reader = PartialRead::new(reader, seq); +//! // ... +//! +//! true +//! } +//! } +//! ``` +//! +//! For a detailed example, see `examples/buggy_write.rs` in this repository. +//! +//! For a real-world example, see the [tests in `bzip2-rs`]. +//! +//! [`PartialOp`]: ../struct.PartialOp.html +//! [`PartialRead`]: ../struct.PartialRead.html +//! [`PartialWrite`]: ../struct.PartialWrite.html +//! [`PartialAsyncRead`]: ../struct.PartialAsyncRead.html +//! [`PartialAsyncWrite`]: ../struct.PartialAsyncWrite.html +//! [`GenError`]: trait.GenError.html +//! [tests in `bzip2-rs`]: https://github.com/alexcrichton/bzip2-rs/blob/master/src/write.rs + +use crate::PartialOp; +use quickcheck::{empty_shrinker, Arbitrary, Gen}; +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use std::{io, marker::PhantomData, ops::Deref}; + +/// Given a custom error generator, randomly generate a list of `PartialOp`s. +#[derive(Clone, Debug)] +pub struct PartialWithErrors { + items: Vec, + _marker: PhantomData, +} + +impl IntoIterator for PartialWithErrors { + type Item = PartialOp; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} + +impl Deref for PartialWithErrors { + type Target = [PartialOp]; + fn deref(&self) -> &Self::Target { + self.items.deref() + } +} + +/// Represents a way to generate `io::ErrorKind` instances. +/// +/// See [the module level documentation](index.html) for more. +pub trait GenError: Clone + Default + Send { + /// Optionally generate an `io::ErrorKind` instance. + fn gen_error(&mut self, g: &mut Gen) -> Option; +} + +/// Generate an `ErrorKind::Interrupted` error 20% of the time. +/// +/// See [the module level documentation](index.html) for more. +#[derive(Clone, Debug, Default)] +pub struct GenInterrupted; + +/// Generate an `ErrorKind::WouldBlock` error 20% of the time. +/// +/// See [the module level documentation](index.html) for more. +#[derive(Clone, Debug, Default)] +pub struct GenWouldBlock; + +/// Generate `Interrupted` and `WouldBlock` errors 10% of the time each. +/// +/// See [the module level documentation](index.html) for more. +#[derive(Clone, Debug, Default)] +pub struct GenInterruptedWouldBlock; + +macro_rules! impl_gen_error { + ($id: ident, [$($errors:expr),+]) => { + impl GenError for $id { + fn gen_error(&mut self, g: &mut Gen) -> Option { + // 20% chance to generate an error. + let mut rng = SmallRng::from_entropy(); + if rng.gen_ratio(1, 5) { + Some(g.choose(&[$($errors,)*]).unwrap().clone()) + } else { + None + } + } + } + } +} + +impl_gen_error!(GenInterrupted, [io::ErrorKind::Interrupted]); +impl_gen_error!(GenWouldBlock, [io::ErrorKind::WouldBlock]); +impl_gen_error!( + GenInterruptedWouldBlock, + [io::ErrorKind::Interrupted, io::ErrorKind::WouldBlock] +); + +/// Do not generate any errors. The only operations generated will be +/// `PartialOp::Limited` instances. +/// +/// See [the module level documentation](index.html) for more. +#[derive(Clone, Debug, Default)] +pub struct GenNoErrors; + +impl GenError for GenNoErrors { + fn gen_error(&mut self, _g: &mut Gen) -> Option { + None + } +} + +impl Arbitrary for PartialWithErrors +where + GE: GenError + 'static, +{ + fn arbitrary(g: &mut Gen) -> Self { + let size = g.size(); + // Generate a sequence of operations. A uniform distribution for this is + // fine because the goal is to shake bugs out relatively effectively. + let mut gen_error = GE::default(); + let items: Vec<_> = (0..size) + .map(|_| { + match gen_error.gen_error(g) { + Some(err) => PartialOp::Err(err), + // Don't generate 0 because for writers it can mean that + // writes are no longer accepted. + None => { + let mut rng = SmallRng::from_entropy(); + PartialOp::Limited(rng.gen_range(1..size)) + } + } + }) + .collect(); + PartialWithErrors { + items, + _marker: PhantomData, + } + } + + fn shrink(&self) -> Box> { + Box::new(self.items.clone().shrink().map(|items| PartialWithErrors { + items, + _marker: PhantomData, + })) + } +} + +impl Arbitrary for PartialOp { + fn arbitrary(_g: &mut Gen) -> Self { + // We only use this for shrink, so we don't need to implement this. + unimplemented!(); + } + + fn shrink(&self) -> Box> { + match *self { + // Skip 0 because for writers it can mean that writes are no longer + // accepted. + PartialOp::Limited(n) => { + Box::new(n.shrink().filter(|k| k != &0).map(PartialOp::Limited)) + } + _ => empty_shrinker(), + } + } +} diff --git a/src/read.rs b/src/read.rs new file mode 100644 index 0000000..f025553 --- /dev/null +++ b/src/read.rs @@ -0,0 +1,138 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +//! This module contains a reader wrapper that breaks its inputs up according to +//! a provided iterator. + +use std::{ + cmp, fmt, + io::{self, Read, Write}, +}; + +use crate::{make_ops, PartialOp}; + +/// A reader wrapper that breaks inner `Read` instances up according to the +/// provided iterator. +/// +/// # Examples +/// +/// ```rust +/// use std::io::{Cursor, Read}; +/// +/// use partial_io::{PartialOp, PartialRead}; +/// +/// let reader = Cursor::new(vec![1, 2, 3, 4]); +/// let iter = ::std::iter::repeat(PartialOp::Limited(1)); +/// let mut partial_reader = PartialRead::new(reader, iter); +/// let mut out = vec![0; 256]; +/// +/// let size = partial_reader.read(&mut out).unwrap(); +/// assert_eq!(size, 1); +/// assert_eq!(&out[..1], &[1]); +/// ``` +pub struct PartialRead { + inner: R, + ops: Box + Send>, +} + +impl PartialRead +where + R: Read, +{ + /// Creates a new `PartialRead` wrapper over the reader with the specified `PartialOp`s. + pub fn new(inner: R, iter: I) -> Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + PartialRead { + inner, + ops: make_ops(iter), + } + } + + /// Sets the `PartialOp`s for this reader. + pub fn set_ops(&mut self, iter: I) -> &mut Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + self.ops = make_ops(iter); + self + } + + /// Acquires a reference to the underlying reader. + pub fn get_ref(&self) -> &R { + &self.inner + } + + /// Acquires a mutable reference to the underlying reader. + pub fn get_mut(&mut self) -> &mut R { + &mut self.inner + } + + /// Consumes this wrapper, returning the underlying reader. + pub fn into_inner(self) -> R { + self.inner + } +} + +impl Read for PartialRead +where + R: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self.ops.next() { + Some(PartialOp::Limited(n)) => { + let len = cmp::min(n, buf.len()); + self.inner.read(&mut buf[..len]) + } + Some(PartialOp::Err(err)) => Err(io::Error::new( + err, + "error during read, generated by partial-io", + )), + Some(PartialOp::Unlimited) | None => self.inner.read(buf), + } + } +} + +// Forwarding impl to support duplex structs. +impl Write for PartialRead +where + R: Read + Write, +{ + #[inline] + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) + } + + #[inline] + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} + +impl fmt::Debug for PartialRead +where + R: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PartialRead") + .field("inner", &self.inner) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::fs::File; + + use crate::tests::assert_send; + + #[test] + fn test_sendable() { + assert_send::>(); + } +} diff --git a/src/write.rs b/src/write.rs new file mode 100644 index 0000000..c301ca1 --- /dev/null +++ b/src/write.rs @@ -0,0 +1,145 @@ +// Copyright (c) The partial-io Contributors +// SPDX-License-Identifier: MIT + +//! This module contains a writer wrapper that breaks writes up according to a +//! provided iterator. + +use std::{ + cmp, fmt, + io::{self, Read, Write}, +}; + +use crate::{make_ops, PartialOp}; + +/// A writer wrapper that breaks inner `Write` instances up according to the +/// provided iterator. +/// +/// # Examples +/// +/// ```rust +/// use std::io::Write; +/// +/// use partial_io::{PartialOp, PartialWrite}; +/// +/// let writer = Vec::new(); +/// let iter = ::std::iter::repeat(PartialOp::Limited(1)); +/// let mut partial_writer = PartialWrite::new(writer, iter); +/// let in_data = vec![1, 2, 3, 4]; +/// +/// let size = partial_writer.write(&in_data).unwrap(); +/// assert_eq!(size, 1); +/// assert_eq!(&partial_writer.get_ref()[..], &[1]); +/// ``` +pub struct PartialWrite { + inner: W, + ops: Box + Send>, +} + +impl PartialWrite +where + W: Write, +{ + /// Creates a new `PartialWrite` wrapper over the writer with the specified `PartialOp`s. + pub fn new(inner: W, iter: I) -> Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + PartialWrite { + inner, + // Use fuse here so that we don't keep calling the inner iterator + // once it's returned None. + ops: make_ops(iter), + } + } + + /// Sets the `PartialOp`s for this writer. + pub fn set_ops(&mut self, iter: I) -> &mut Self + where + I: IntoIterator + 'static, + I::IntoIter: Send, + { + self.ops = make_ops(iter); + self + } + + /// Acquires a reference to the underlying writer. + pub fn get_ref(&self) -> &W { + &self.inner + } + + /// Acquires a mutable reference to the underlying writer. + pub fn get_mut(&mut self) -> &mut W { + &mut self.inner + } + + /// Consumes this wrapper, returning the underlying writer. + pub fn into_inner(self) -> W { + self.inner + } +} + +impl Write for PartialWrite +where + W: Write, +{ + fn write(&mut self, buf: &[u8]) -> io::Result { + match self.ops.next() { + Some(PartialOp::Limited(n)) => { + let len = cmp::min(n, buf.len()); + self.inner.write(&buf[..len]) + } + Some(PartialOp::Err(err)) => Err(io::Error::new( + err, + "error during write, generated by partial-io", + )), + Some(PartialOp::Unlimited) | None => self.inner.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self.ops.next() { + Some(PartialOp::Err(err)) => Err(io::Error::new( + err, + "error during flush, generated by partial-io", + )), + _ => self.inner.flush(), + } + } +} + +// Forwarding impl to support duplex structs. +impl Read for PartialWrite +where + W: Read + Write, +{ + #[inline] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.inner.read(buf) + } +} + +impl fmt::Debug for PartialWrite +where + W: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PartialWrite") + .field("inner", &self.inner) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::fs::File; + + use crate::tests::assert_send; + + #[test] + fn test_sendable() { + assert_send::>(); + } +}