--- /dev/null
+[alias]
+xfmt = "fmt -- --config imports_granularity=Crate"
--- /dev/null
+{
+ "git": {
+ "sha1": "4d3bbd5e8a125423a7955462408d918ad7464a23"
+ },
+ "path_in_vcs": ""
+}
\ No newline at end of file
--- /dev/null
+github: sunshowers
--- /dev/null
+# 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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+# 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 }}
--- /dev/null
+# will have compiled files and executables
+/target/
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# IntelliJ settings
+/.idea
--- /dev/null
+# 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
+
+<!-- older releases are on the facebookincubator repo -->
+[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
--- /dev/null
+# 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 <conduct@sunshowers.io>. 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
--- /dev/null
+# 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.
--- /dev/null
+# 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"
--- /dev/null
+# 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 <rain@sunshowers.io>"]
+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",
+]
--- /dev/null
+[package]
+name = "partial-io"
+version = "0.5.4"
+edition = "2021"
+authors = ["Rain <rain@sunshowers.io>"]
+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"]
--- /dev/null
+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.
--- /dev/null
+# partial-io
+
+[](https://crates.io/crates/partial-io)
+[](https://docs.rs/partial-io/)
+[](https://sunshowers-code.github.io/partial-io/rustdoc/partial_io/)
+[](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<Vec<u8>> 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).
+
+<!--
+README.md is generated from README.tpl by cargo readme. To regenerate:
+
+cargo install cargo-readme
+cargo readme > README.md
+-->
--- /dev/null
+# {{crate}}
+
+[](https://crates.io/crates/partial-io)
+[](https://docs.rs/partial-io/)
+[](https://sunshowers-code.github.io/partial-io/rustdoc/partial_io/)
+[](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).
+
+<!--
+README.md is generated from README.tpl by cargo readme. To regenerate:
+
+cargo install cargo-readme
+cargo readme > README.md
+-->
--- /dev/null
+// 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<W> {
+ inner: W,
+ buf: Vec<u8>,
+ offset: usize,
+}
+
+impl<W: Write> BuggyWrite<W> {
+ 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<W: Write> Write for BuggyWrite<W> {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ // 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<Vec<u8>> = Lazy::new(|| "Hello".repeat(50).into_bytes());
+ pub(crate) static WORLD_STR: Lazy<Vec<u8>> = 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<I>(
+ partial_iter: I,
+ ) -> (
+ io::Result<usize>,
+ io::Result<usize>,
+ io::Result<()>,
+ Vec<u8>,
+ )
+ where
+ I: IntoIterator<Item = PartialOp> + '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<GenInterrupted>) -> TestResult);
+ }
+
+ fn quickcheck_buggy_write2(partial: PartialWithErrors<GenInterrupted>) -> 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");
+ }
+ }
+}
--- /dev/null
+#!/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
--- /dev/null
+// 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<R> {
+ #[pin]
+ inner: R,
+ ops: FuturesOps,
+}
+
+impl<R> PartialAsyncRead<R> {
+ /// Creates a new `PartialAsyncRead` wrapper over the reader with the specified `PartialOp`s.
+ pub fn new<I>(inner: R, iter: I) -> Self
+ where
+ I: IntoIterator<Item = PartialOp> + 'static,
+ I::IntoIter: Send,
+ {
+ PartialAsyncRead {
+ inner,
+ ops: FuturesOps::new(iter),
+ }
+ }
+
+ /// Sets the `PartialOp`s for this reader.
+ pub fn set_ops<I>(&mut self, iter: I) -> &mut Self
+ where
+ I: IntoIterator<Item = PartialOp> + '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<I>(self: Pin<&mut Self>, iter: I) -> Pin<&mut Self>
+ where
+ I: IntoIterator<Item = PartialOp> + '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<R> AsyncRead for PartialAsyncRead<R>
+where
+ R: AsyncRead,
+{
+ #[inline]
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ buf: &mut [u8],
+ ) -> Poll<io::Result<usize>> {
+ 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<R> AsyncBufRead for PartialAsyncRead<R>
+where
+ R: AsyncBufRead,
+{
+ fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<&[u8]>> {
+ 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<R> AsyncWrite for PartialAsyncRead<R>
+where
+ R: AsyncWrite,
+{
+ fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<io::Result<usize>> {
+ self.project().inner.poll_write(cx, buf)
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
+ self.project().inner.poll_flush(cx)
+ }
+
+ fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
+ self.project().inner.poll_close(cx)
+ }
+}
+
+/// This is a forwarding impl to support duplex structs.
+impl<R> AsyncSeek for PartialAsyncRead<R>
+where
+ R: AsyncSeek,
+{
+ #[inline]
+ fn poll_seek(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ pos: io::SeekFrom,
+ ) -> Poll<io::Result<u64>> {
+ 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<R> AsyncRead for PartialAsyncRead<R>
+ where
+ R: AsyncRead,
+ {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ 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<F, T>(&mut self, limit: usize, callback: F) -> T
+ where
+ F: FnOnce(&mut ReadBuf<'_>) -> T;
+ }
+
+ impl<'a> ReadBufExt for ReadBuf<'a> {
+ fn with_limited<F, T>(&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<R> AsyncBufRead for PartialAsyncRead<R>
+ where
+ R: AsyncBufRead,
+ {
+ fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
+ 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<R> AsyncWrite for PartialAsyncRead<R>
+ where
+ R: AsyncWrite,
+ {
+ #[inline]
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ self.project().inner.poll_write(cx, buf)
+ }
+
+ #[inline]
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
+ self.project().inner.poll_flush(cx)
+ }
+
+ #[inline]
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
+ self.project().inner.poll_shutdown(cx)
+ }
+ }
+
+ /// This is a forwarding impl to support duplex structs.
+ impl<R> AsyncSeek for PartialAsyncRead<R>
+ 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<io::Result<u64>> {
+ 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<u8>; 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<u8>; 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<u8>; 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<R> fmt::Debug for PartialAsyncRead<R>
+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::<PartialAsyncRead<File>>();
+ }
+}
--- /dev/null
+// 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<W> {
+ #[pin]
+ inner: W,
+ ops: FuturesOps,
+}
+
+impl<W> PartialAsyncWrite<W> {
+ /// Creates a new `PartialAsyncWrite` wrapper over the writer with the specified `PartialOp`s.
+ pub fn new<I>(inner: W, iter: I) -> Self
+ where
+ I: IntoIterator<Item = PartialOp> + 'static,
+ I::IntoIter: Send,
+ {
+ PartialAsyncWrite {
+ inner,
+ ops: FuturesOps::new(iter),
+ }
+ }
+
+ /// Sets the `PartialOp`s for this writer.
+ pub fn set_ops<I>(&mut self, iter: I) -> &mut Self
+ where
+ I: IntoIterator<Item = PartialOp> + '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<I>(self: Pin<&mut Self>, iter: I) -> Pin<&mut Self>
+ where
+ I: IntoIterator<Item = PartialOp> + '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<W> AsyncWrite for PartialAsyncWrite<W>
+where
+ W: AsyncWrite,
+{
+ fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<io::Result<usize>> {
+ 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<io::Result<()>> {
+ 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<io::Result<()>> {
+ 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<W> AsyncRead for PartialAsyncWrite<W>
+where
+ W: AsyncRead,
+{
+ #[inline]
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ buf: &mut [u8],
+ ) -> Poll<io::Result<usize>> {
+ self.project().inner.poll_read(cx, buf)
+ }
+
+ #[inline]
+ fn poll_read_vectored(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ bufs: &mut [io::IoSliceMut],
+ ) -> Poll<io::Result<usize>> {
+ self.project().inner.poll_read_vectored(cx, bufs)
+ }
+}
+
+/// This is a forwarding impl to support duplex structs.
+impl<W> AsyncBufRead for PartialAsyncWrite<W>
+where
+ W: AsyncBufRead,
+{
+ #[inline]
+ fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<&[u8]>> {
+ 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<W> AsyncSeek for PartialAsyncWrite<W>
+where
+ W: AsyncSeek,
+{
+ #[inline]
+ fn poll_seek(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ pos: io::SeekFrom,
+ ) -> Poll<io::Result<u64>> {
+ 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<W> AsyncWrite for PartialAsyncWrite<W>
+ where
+ W: AsyncWrite,
+ {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ 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<io::Result<()>> {
+ 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<io::Result<()>> {
+ 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<W> AsyncRead for PartialAsyncWrite<W>
+ where
+ W: AsyncRead,
+ {
+ #[inline]
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ self.project().inner.poll_read(cx, buf)
+ }
+ }
+
+ /// This is a forwarding impl to support duplex structs.
+ impl<W> AsyncBufRead for PartialAsyncWrite<W>
+ where
+ W: AsyncBufRead,
+ {
+ #[inline]
+ fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<&[u8]>> {
+ 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<W> AsyncSeek for PartialAsyncWrite<W>
+ 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<io::Result<u64>> {
+ self.project().inner.poll_complete(cx)
+ }
+ }
+}
+
+impl<W> fmt::Debug for PartialAsyncWrite<W>
+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::<PartialAsyncWrite<File>>();
+ }
+}
--- /dev/null
+// 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<dyn Iterator<Item = PartialOp> + Send>,
+}
+
+impl FuturesOps {
+ /// Creates a new instance of `TokioOps`.
+ pub(crate) fn new<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = PartialOp> + 'static,
+ I::IntoIter: Send,
+ {
+ Self {
+ ops: make_ops(iter),
+ }
+ }
+
+ /// Replaces ops with a new iterator.
+ pub(crate) fn replace<I>(&mut self, iter: I)
+ where
+ I: IntoIterator<Item = PartialOp> + '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<T>(
+ &mut self,
+ cx: &mut Context,
+ cb: impl FnOnce(&mut Context, Option<usize>) -> Poll<io::Result<T>>,
+ remaining: usize,
+ err_str: &'static str,
+ ) -> Poll<io::Result<T>> {
+ 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<T>(
+ &mut self,
+ cx: &mut Context,
+ cb: impl FnOnce(&mut Context) -> Poll<io::Result<T>>,
+ err_str: &'static str,
+ ) -> Poll<io::Result<T>> {
+ 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),
+ }
+ }
+ }
+}
--- /dev/null
+// 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<Vec<u8>> 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<Option<usize>>` 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<I>(iter: I) -> Box<dyn Iterator<Item = PartialOp> + Send>
+where
+ I: IntoIterator<Item = PartialOp> + 'static,
+ I::IntoIter: Send,
+{
+ Box::new(iter.into_iter().fuse())
+}
+
+#[cfg(test)]
+mod tests {
+ pub fn assert_send<S: Send>() {}
+}
--- /dev/null
+// 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<Value = Option<io::ErrorKind>>,
+ limit_bytes: usize,
+) -> impl Strategy<Value = PartialOp> {
+ // 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<Value = Option<io::ErrorKind>> {
+ 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<Value = Option<io::ErrorKind>> {
+ 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<Value = Option<io::ErrorKind>> {
+ weighted(
+ 0.2,
+ prop_oneof![
+ Just(io::ErrorKind::Interrupted),
+ Just(io::ErrorKind::WouldBlock),
+ ],
+ )
+}
--- /dev/null
+// 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<GE>` 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<GenInterrupted>) -> 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<GE> {
+ items: Vec<PartialOp>,
+ _marker: PhantomData<GE>,
+}
+
+impl<GE> IntoIterator for PartialWithErrors<GE> {
+ type Item = PartialOp;
+ type IntoIter = ::std::vec::IntoIter<PartialOp>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.items.into_iter()
+ }
+}
+
+impl<GE> Deref for PartialWithErrors<GE> {
+ 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<io::ErrorKind>;
+}
+
+/// 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<io::ErrorKind> {
+ // 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<io::ErrorKind> {
+ None
+ }
+}
+
+impl<GE> Arbitrary for PartialWithErrors<GE>
+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<dyn Iterator<Item = Self>> {
+ 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<dyn Iterator<Item = Self>> {
+ 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(),
+ }
+ }
+}
--- /dev/null
+// 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<R> {
+ inner: R,
+ ops: Box<dyn Iterator<Item = PartialOp> + Send>,
+}
+
+impl<R> PartialRead<R>
+where
+ R: Read,
+{
+ /// Creates a new `PartialRead` wrapper over the reader with the specified `PartialOp`s.
+ pub fn new<I>(inner: R, iter: I) -> Self
+ where
+ I: IntoIterator<Item = PartialOp> + 'static,
+ I::IntoIter: Send,
+ {
+ PartialRead {
+ inner,
+ ops: make_ops(iter),
+ }
+ }
+
+ /// Sets the `PartialOp`s for this reader.
+ pub fn set_ops<I>(&mut self, iter: I) -> &mut Self
+ where
+ I: IntoIterator<Item = PartialOp> + '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<R> Read for PartialRead<R>
+where
+ R: Read,
+{
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ 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<R> Write for PartialRead<R>
+where
+ R: Read + Write,
+{
+ #[inline]
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ self.inner.write(buf)
+ }
+
+ #[inline]
+ fn flush(&mut self) -> io::Result<()> {
+ self.inner.flush()
+ }
+}
+
+impl<R> fmt::Debug for PartialRead<R>
+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::<PartialRead<File>>();
+ }
+}
--- /dev/null
+// 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<W> {
+ inner: W,
+ ops: Box<dyn Iterator<Item = PartialOp> + Send>,
+}
+
+impl<W> PartialWrite<W>
+where
+ W: Write,
+{
+ /// Creates a new `PartialWrite` wrapper over the writer with the specified `PartialOp`s.
+ pub fn new<I>(inner: W, iter: I) -> Self
+ where
+ I: IntoIterator<Item = PartialOp> + '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<I>(&mut self, iter: I) -> &mut Self
+ where
+ I: IntoIterator<Item = PartialOp> + '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<W> Write for PartialWrite<W>
+where
+ W: Write,
+{
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ 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<W> Read for PartialWrite<W>
+where
+ W: Read + Write,
+{
+ #[inline]
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ self.inner.read(buf)
+ }
+}
+
+impl<W> fmt::Debug for PartialWrite<W>
+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::<PartialWrite<File>>();
+ }
+}