From ea7fff82a0af59b66eab2d22941cc373e58889c9 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Tue, 2 May 2023 07:44:04 +0900 Subject: [PATCH] Import partial-io 0.5.4 --- .cargo/config.toml | 2 + .cargo_vcs_info.json | 6 + .github/FUNDING.yml | 1 + .github/dependabot.yml | 11 + .github/workflows/ci.yml | 73 ++++ .github/workflows/docs.yml | 36 ++ .github/workflows/release.yml | 31 ++ .gitignore | 8 + CHANGELOG.md | 48 +++ CODE_OF_CONDUCT.md | 76 +++++ CONTRIBUTING.md | 25 ++ Cargo.lock | 569 +++++++++++++++++++++++++++++++ Cargo.toml | 114 +++++++ Cargo.toml.orig | 55 +++ LICENSE | 22 ++ README.md | 104 ++++++ README.tpl | 30 ++ examples/buggy_write.rs | 216 ++++++++++++ rustfmt.toml | 0 scripts/regenerate-readmes.sh | 14 + src/async_read.rs | 609 ++++++++++++++++++++++++++++++++++ src/async_write.rs | 371 +++++++++++++++++++++ src/futures_util.rs | 96 ++++++ src/lib.rs | 151 +++++++++ src/proptest_types.rs | 77 +++++ src/quickcheck_types.rs | 194 +++++++++++ src/read.rs | 138 ++++++++ src/write.rs | 145 ++++++++ 28 files changed, 3222 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .cargo_vcs_info.json create mode 100644 .github/FUNDING.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.tpl create mode 100644 examples/buggy_write.rs create mode 100644 rustfmt.toml create mode 100755 scripts/regenerate-readmes.sh create mode 100644 src/async_read.rs create mode 100644 src/async_write.rs create mode 100644 src/futures_util.rs create mode 100644 src/lib.rs create mode 100644 src/proptest_types.rs create mode 100644 src/quickcheck_types.rs create mode 100644 src/read.rs create mode 100644 src/write.rs 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::>(); + } +} -- 2.34.1