Import partial-io 0.5.4 upstream upstream/0.5.4
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 1 May 2023 22:44:04 +0000 (07:44 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 1 May 2023 22:44:04 +0000 (07:44 +0900)
28 files changed:
.cargo/config.toml [new file with mode: 0644]
.cargo_vcs_info.json [new file with mode: 0644]
.github/FUNDING.yml [new file with mode: 0644]
.github/dependabot.yml [new file with mode: 0644]
.github/workflows/ci.yml [new file with mode: 0644]
.github/workflows/docs.yml [new file with mode: 0644]
.github/workflows/release.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
CHANGELOG.md [new file with mode: 0644]
CODE_OF_CONDUCT.md [new file with mode: 0644]
CONTRIBUTING.md [new file with mode: 0644]
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
README.tpl [new file with mode: 0644]
examples/buggy_write.rs [new file with mode: 0644]
rustfmt.toml [new file with mode: 0644]
scripts/regenerate-readmes.sh [new file with mode: 0755]
src/async_read.rs [new file with mode: 0644]
src/async_write.rs [new file with mode: 0644]
src/futures_util.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/proptest_types.rs [new file with mode: 0644]
src/quickcheck_types.rs [new file with mode: 0644]
src/read.rs [new file with mode: 0644]
src/write.rs [new file with mode: 0644]

diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644 (file)
index 0000000..4580be0
--- /dev/null
@@ -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 (file)
index 0000000..01dac5e
--- /dev/null
@@ -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 (file)
index 0000000..5a8048a
--- /dev/null
@@ -0,0 +1 @@
+github: sunshowers
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644 (file)
index 0000000..eaa00e7
--- /dev/null
@@ -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 (file)
index 0000000..9afb6a7
--- /dev/null
@@ -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 (file)
index 0000000..0fb579a
--- /dev/null
@@ -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 (file)
index 0000000..744bd94
--- /dev/null
@@ -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 (file)
index 0000000..38db119
--- /dev/null
@@ -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 (file)
index 0000000..efcd9c7
--- /dev/null
@@ -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
+
+<!-- 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
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644 (file)
index 0000000..0a0cce3
--- /dev/null
@@ -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 <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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644 (file)
index 0000000..c18f322
--- /dev/null
@@ -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 (file)
index 0000000..241fb73
--- /dev/null
@@ -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 (file)
index 0000000..50342f4
--- /dev/null
@@ -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 <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",
+]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644 (file)
index 0000000..c112de3
--- /dev/null
@@ -0,0 +1,55 @@
+[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"]
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
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 (file)
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<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
+-->
diff --git a/README.tpl b/README.tpl
new file mode 100644 (file)
index 0000000..bb6d6db
--- /dev/null
@@ -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).
+
+<!--
+README.md is generated from README.tpl by cargo readme. To regenerate:
+
+cargo install cargo-readme
+cargo readme > README.md
+-->
diff --git a/examples/buggy_write.rs b/examples/buggy_write.rs
new file mode 100644 (file)
index 0000000..53296e2
--- /dev/null
@@ -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<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");
+        }
+    }
+}
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/scripts/regenerate-readmes.sh b/scripts/regenerate-readmes.sh
new file mode 100755 (executable)
index 0000000..0e0865e
--- /dev/null
@@ -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 (file)
index 0000000..3a58d3f
--- /dev/null
@@ -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<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>>();
+    }
+}
diff --git a/src/async_write.rs b/src/async_write.rs
new file mode 100644 (file)
index 0000000..acbc79b
--- /dev/null
@@ -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<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>>();
+    }
+}
diff --git a/src/futures_util.rs b/src/futures_util.rs
new file mode 100644 (file)
index 0000000..7b6fbe8
--- /dev/null
@@ -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<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),
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..46bfac8
--- /dev/null
@@ -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<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>() {}
+}
diff --git a/src/proptest_types.rs b/src/proptest_types.rs
new file mode 100644 (file)
index 0000000..204690d
--- /dev/null
@@ -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<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),
+        ],
+    )
+}
diff --git a/src/quickcheck_types.rs b/src/quickcheck_types.rs
new file mode 100644 (file)
index 0000000..314d863
--- /dev/null
@@ -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<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(),
+        }
+    }
+}
diff --git a/src/read.rs b/src/read.rs
new file mode 100644 (file)
index 0000000..f025553
--- /dev/null
@@ -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<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>>();
+    }
+}
diff --git a/src/write.rs b/src/write.rs
new file mode 100644 (file)
index 0000000..c301ca1
--- /dev/null
@@ -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<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>>();
+    }
+}