diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index a2345e184..000000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -[alias] -lint = "clippy --workspace --all-targets -- -Dclippy::todo" -lint-all = "clippy --workspace --all-features --all-targets -- -Dclippy::todo" - -# lib checking -ci-check-min = "hack --workspace check --no-default-features" -ci-check-default = "hack --workspace check" -ci-check-default-tests = "check --workspace --tests" -ci-check-all-feature-powerset="hack --workspace --feature-powerset --depth=4 --skip=__compress,experimental-io-uring check" -ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --depth=4 --skip=__compress check" diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 000000000..8c0cbc8bf --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,7 @@ +disallowed-names = [ + "e", # no single letter error bindings +] +disallowed-methods = [ + "std::cell::RefCell::default()", + "std::rc::Rc::default()", +] diff --git a/.cspell.yml b/.cspell.yml new file mode 100644 index 000000000..95a3e2f90 --- /dev/null +++ b/.cspell.yml @@ -0,0 +1,3 @@ +version: "0.2" +words: + - actix diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 1729d9a07..72cffbdc3 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -32,7 +32,7 @@ jobs: - name: Install nasm if: matrix.target.os == 'windows-latest' - uses: ilammy/setup-nasm@v1.5.1 + uses: ilammy/setup-nasm@v1.5.2 - name: Install OpenSSL if: matrix.target.os == 'windows-latest' @@ -44,20 +44,20 @@ jobs: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV - name: Install Rust (${{ matrix.version.name }}) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@v2.38.0 + uses: taiki-e/install-action@v2.49.50 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean - name: check minimal - run: cargo ci-check-min + run: just check-min - name: check default - run: cargo ci-check-default + run: just check-default - name: tests timeout-minutes: 60 @@ -76,16 +76,16 @@ jobs: - name: Free Disk Space run: ./scripts/free-disk-space.sh + - name: Setup mold linker + uses: rui314/setup-mold@v1 + - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 - - name: Install cargo-hack - uses: taiki-e/install-action@v2.38.0 + - name: Install just, cargo-hack + uses: taiki-e/install-action@v2.49.50 with: - tool: cargo-hack + tool: just,cargo-hack - - name: check feature combinations - run: cargo ci-check-all-feature-powerset - - - name: check feature combinations - run: cargo ci-check-all-feature-powerset-linux + - name: Check feature combinations + run: just check-feature-combinations diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b6f7b460..1c2ecdeea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: - name: Install nasm if: matrix.target.os == 'windows-latest' - uses: ilammy/setup-nasm@v1.5.1 + uses: ilammy/setup-nasm@v1.5.2 - name: Install OpenSSL if: matrix.target.os == 'windows-latest' @@ -59,12 +59,12 @@ jobs: uses: rui314/setup-mold@v1 - name: Install Rust (${{ matrix.version.name }}) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@v2.38.0 + uses: taiki-e/install-action@v2.49.50 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -73,10 +73,10 @@ jobs: run: just downgrade-for-msrv - name: check minimal - run: cargo ci-check-min + run: just check-min - name: check default - run: cargo ci-check-default + run: just check-default - name: tests timeout-minutes: 60 @@ -92,7 +92,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: toolchain: nightly @@ -108,12 +108,12 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: toolchain: nightly - name: Install just - uses: taiki-e/install-action@v2.38.0 + uses: taiki-e/install-action@v2.49.50 with: tool: just diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ca3115713..2d2aa1c5e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,13 +18,13 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: toolchain: nightly components: llvm-tools - name: Install just, cargo-llvm-cov, cargo-nextest - uses: taiki-e/install-action@v2.38.0 + uses: taiki-e/install-action@v2.49.50 with: tool: just,cargo-llvm-cov,cargo-nextest @@ -32,7 +32,7 @@ jobs: run: just test-coverage-codecov - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.4.1 + uses: codecov/codecov-action@v5.4.2 with: files: codecov.json fail_ci_if_error: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ca9d2bbeb..b1e34a57e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: toolchain: nightly components: rustfmt @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: components: clippy @@ -55,7 +55,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: toolchain: nightly components: rust-docs @@ -66,51 +66,25 @@ jobs: run: cargo +nightly doc --no-deps --workspace --all-features check-external-types: + if: false # rustdoc mismatch currently runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Rust (nightly-2024-05-01) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }}) + uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 with: - toolchain: nightly-2024-05-01 + toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} - name: Install just - uses: taiki-e/install-action@v2.38.0 + uses: taiki-e/install-action@v2.49.50 with: tool: just - name: Install cargo-check-external-types - uses: taiki-e/cache-cargo-install-action@v1.2.2 + uses: taiki-e/cache-cargo-install-action@v2.1.1 with: tool: cargo-check-external-types - name: check external types - run: just check-external-types-all +nightly-2024-05-01 - - public-api-diff: - runs-on: ubuntu-latest - steps: - - name: Checkout main branch - uses: actions/checkout@v4 - with: - ref: ${{ github.base_ref }} - - - name: Checkout PR branch - uses: actions/checkout@v4 - - - name: Install Rust (nightly-2024-06-07) - uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 - with: - toolchain: nightly-2024-06-07 - - - name: Install cargo-public-api - uses: taiki-e/install-action@v2.38.0 - with: - tool: cargo-public-api - - - name: Generate API diff - run: | - for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do - cargo public-api --manifest-path "$f" --simplified diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} - done + run: just check-external-types-all +${{ vars.RUST_VERSION_EXTERNAL_TYPES }} diff --git a/.gitignore b/.gitignore index 48ccccb92..516ee9919 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -Cargo.lock target/ guide/build/ /gh-pages diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..96cd56efb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3957 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-rt", + "bitflags 2.9.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-files" +version = "0.6.6" +dependencies = [ + "actix-http", + "actix-rt", + "actix-server", + "actix-service", + "actix-test", + "actix-utils", + "actix-web", + "bitflags 2.9.0", + "bytes", + "derive_more", + "env_logger", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tempfile", + "tokio-uring", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "3.10.0" +dependencies = [ + "actix-codec", + "actix-http-test", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web", + "async-stream", + "base64 0.22.1", + "bitflags 2.9.0", + "brotli", + "bytes", + "bytestring", + "criterion", + "derive_more", + "divan", + "encoding_rs", + "env_logger", + "flate2", + "foldhash", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "memchr", + "mime", + "once_cell", + "openssl", + "percent-encoding", + "pin-project-lite", + "rand 0.9.1", + "rcgen", + "regex", + "rustls 0.23.25", + "rustls-pemfile", + "rustversion", + "serde", + "serde_json", + "sha1", + "smallvec", + "static_assertions", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-http-test" +version = "3.2.0" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "awc", + "bytes", + "futures-core", + "http 0.2.12", + "log", + "openssl", + "serde", + "serde_json", + "serde_urlencoded", + "slab", + "socket2 0.5.9", + "tokio", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-multipart" +version = "0.7.2" +dependencies = [ + "actix-http", + "actix-multipart-derive", + "actix-multipart-rfc7578", + "actix-rt", + "actix-test", + "actix-utils", + "actix-web", + "assert_matches", + "awc", + "derive_more", + "env_logger", + "futures-core", + "futures-test", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "multer", + "rand 0.9.1", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", + "tokio-stream", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +dependencies = [ + "actix-multipart", + "actix-web", + "bytesize", + "darling", + "proc-macro2", + "quote", + "rustversion-msrv", + "syn", + "trybuild", +] + +[[package]] +name = "actix-multipart-rfc7578" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5e8f1e4c6baf42b8b78f3b2a85df02b54f10f5fdf54bf1f9b40211c130e427" +dependencies = [ + "actix-http", + "bytes", + "common-multipart-rfc7578", + "futures-core", + "thiserror", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +dependencies = [ + "bytestring", + "cfg-if", + "criterion", + "http 0.2.12", + "percent-encoding", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", + "tokio-uring", +] + +[[package]] +name = "actix-server" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.9", + "tokio", + "tokio-uring", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-test" +version = "0.1.5" +dependencies = [ + "actix-codec", + "actix-http", + "actix-http-test", + "actix-rt", + "actix-service", + "actix-utils", + "actix-web", + "awc", + "futures-core", + "futures-util", + "log", + "openssl", + "rustls 0.20.9", + "rustls 0.21.12", + "rustls 0.22.4", + "rustls 0.23.25", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "impl-more", + "openssl", + "pin-project-lite", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-openssl", + "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", + "tokio-rustls 0.25.0", + "tokio-rustls 0.26.2", + "tokio-util", + "tracing", + "webpki-roots 0.22.6", + "webpki-roots 0.25.4", + "webpki-roots 0.26.8", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.10.2" +dependencies = [ + "actix-codec", + "actix-files", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-test", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "awc", + "brotli", + "bytes", + "bytestring", + "cfg-if", + "const-str", + "cookie", + "core_affinity", + "criterion", + "derive_more", + "encoding_rs", + "env_logger", + "flate2", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "openssl", + "pin-project-lite", + "rand 0.9.1", + "rcgen", + "regex", + "regex-lite", + "rustls 0.23.25", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.9", + "static_assertions", + "time", + "tokio", + "tracing", + "url", + "zstd", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-rt", + "actix-test", + "actix-web", + "awc", + "bytes", + "bytestring", + "env_logger", + "futures-core", + "futures-util", + "mime", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +dependencies = [ + "actix-macros", + "actix-router", + "actix-rt", + "actix-test", + "actix-utils", + "actix-web", + "futures-core", + "proc-macro2", + "quote", + "rustversion-msrv", + "syn", + "trybuild", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "awc" +version = "3.6.0" +dependencies = [ + "actix-codec", + "actix-http", + "actix-http-test", + "actix-rt", + "actix-server", + "actix-service", + "actix-test", + "actix-tls", + "actix-utils", + "actix-web", + "base64 0.22.1", + "brotli", + "bytes", + "cfg-if", + "const-str", + "cookie", + "derive_more", + "env_logger", + "flate2", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "itoa", + "log", + "mime", + "openssl", + "percent-encoding", + "pin-project-lite", + "rand 0.9.1", + "rcgen", + "rustls 0.20.9", + "rustls 0.21.12", + "rustls 0.22.4", + "rustls 0.23.25", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions", + "tokio", + "trust-dns-resolver", + "zstd", +] + +[[package]] +name = "aws-lc-rs" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytesize" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +dependencies = [ + "anstyle", + "clap_lex", + "terminal_size", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "common-multipart-rfc7578" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f08d53b5e0c302c5830cfa7511ba0edc3f241c691a95c0d184dfb761e11a6cc2" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "mime", + "mime_guess", + "rand 0.8.5", + "thiserror", +] + +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", + "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_affinity" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" +dependencies = [ + "libc", + "num_cpus", + "winapi", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "divan" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009c56317fe2bd3b5eebe3aa888828c62ed1b085d26c1ef2079a60369795765e" +dependencies = [ + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4de9827ae754db91aedec0277381f5a2d8e2f801564c8d774acfe1ab1b045e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-test" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961fb6311645f46e2cdc2964a8bfae6743fd72315eaec181a71ae3eb2467113" +dependencies = [ + "futures-core", + "futures-executor", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "futures-util", + "pin-project", +] + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "io-uring" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595a0399f411a508feb2ec1e970a4a30c249351e30208960d58298de8660b0e5" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.9", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.0", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.3.1", + "httparse", + "memchr", + "mime", + "spin 0.9.8", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring 0.17.14", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "resolv-conf" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4" +dependencies = [ + "hostname", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.1", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +dependencies = [ + "aws-lc-rs", + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "rustversion-msrv" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ceb60223ee771fb5dfe462e29e5ee92bca9a7b9c555584f4d361045dae0e12" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "target-triple" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.5", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.5", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.9", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls 0.23.25", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-uring" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "748482e3e13584a34664a710168ad5068e8cb1d968aa4ffa887e83ca6dd27967" +dependencies = [ + "bytes", + "futures-util", + "io-uring", + "libc", + "slab", + "socket2 0.4.10", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trust-dns-proto" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.5", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + +[[package]] +name = "trybuild" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna 1.0.3", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + +[[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-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 19d5dd116..d5601f8f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.75" [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. @@ -51,3 +51,11 @@ awc = { path = "awc" } # actix-utils = { path = "../actix-net/actix-utils" } # actix-tls = { path = "../actix-net/actix-tls" } # actix-server = { path = "../actix-net/actix-server" } + +[workspace.lints.rust] +rust_2018_idioms = { level = "deny" } +future_incompatible = { level = "deny" } +nonstandard_style = { level = "deny" } + +[workspace.lints.clippy] +# clone_on_ref_ptr = { level = "deny" } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index e94f43907..afb2d5d20 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -2,6 +2,8 @@ ## Unreleased +- Minimum supported Rust version (MSRV) is now 1.75. + ## 0.6.6 - Update `tokio-uring` dependency to `0.4`. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 57cd4e913..b2bef2aa6 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -33,7 +33,7 @@ actix-web = { version = "4", default-features = false } bitflags = "2" bytes = "1" -derive_more = "0.99.5" +derive_more = { version = "2", features = ["display", "error", "from"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } http-range = "0.1.4" log = "0.4" @@ -54,3 +54,6 @@ actix-test = "0.1" actix-web = "4" env_logger = "0.11" tempfile = "3.2" + +[lints] +workspace = true diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index d614651fc..e762116e6 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -6,11 +6,11 @@ use derive_more::Display; pub enum FilesError { /// Path is not a directory. #[allow(dead_code)] - #[display(fmt = "path is not a directory. Unable to serve static files")] + #[display("path is not a directory. Unable to serve static files")] IsNotDirectory, /// Cannot render directory. - #[display(fmt = "unable to render directory without index file")] + #[display("unable to render directory without index file")] IsDirectory, } @@ -25,19 +25,19 @@ impl ResponseError for FilesError { #[non_exhaustive] pub enum UriSegmentError { /// Segment started with the wrapped invalid character. - #[display(fmt = "segment started with invalid character: ('{_0}')")] + #[display("segment started with invalid character: ('{_0}')")] BadStart(char), /// Segment contained the wrapped invalid character. - #[display(fmt = "segment contained invalid character ('{_0}')")] + #[display("segment contained invalid character ('{_0}')")] BadChar(char), /// Segment ended with the wrapped invalid character. - #[display(fmt = "segment ended with invalid character: ('{_0}')")] + #[display("segment ended with invalid character: ('{_0}')")] BadEnd(char), /// Path is not a valid UTF-8 string after percent-decoding. - #[display(fmt = "path is not a valid UTF-8 string after percent-decoding")] + #[display("path is not a valid UTF-8 string after percent-decoding")] NotValidUtf8, } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 167f996c0..551a14fa4 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -11,8 +11,7 @@ //! .service(Files::new("/static", ".").prefer_utf8(true)); //! ``` -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible, missing_docs, missing_debug_implementations)] +#![warn(missing_docs, missing_debug_implementations)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -307,11 +306,11 @@ mod tests { let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/javascript; charset=utf-8" + "text/javascript", ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.js\"" + "inline; filename=\"test.js\"", ); } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 3d3b36c40..393ad9244 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -79,7 +79,7 @@ impl FilesService { let (req, _) = req.into_parts(); - (self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req)) + (self.renderer)(&dir, &req).unwrap_or_else(|err| ServiceResponse::from_err(err, req)) } } diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 0947579a5..7ccb70a45 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -59,3 +59,6 @@ tokio = { version = "1.24.2", features = ["sync"] } [dev-dependencies] actix-http = "3" + +[lints] +workspace = true diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 554af9102..a359cec09 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -1,7 +1,5 @@ //! Various helpers for Actix applications to use during testing. -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -108,7 +106,7 @@ pub async fn test_server_with_addr>( builder.set_verify(SslVerifyMode::NONE); let _ = builder .set_alpn_protos(b"\x02h2\x08http/1.1") - .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); + .map_err(|err| log::error!("Can not set ALPN protocol: {err}")); Connector::new() .conn_lifetime(Duration::from_secs(0)) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 85ba03100..7324cba5a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,6 +2,28 @@ ## Unreleased +## 3.10.0 + +### Added + +- Add `header::CLEAR_SITE_DATA` constant. +- Add `Extensions::get_or_insert[_with]()` methods. +- Implement `From` for `Payload`. +- Implement `From>` for `Payload`. + +### Changed + +- Update `brotli` dependency to `7`. +- Minimum supported Rust version (MSRV) is now 1.75. + +## 3.9.0 + +### Added + +- Implement `FromIterator<(HeaderName, HeaderValue)>` for `HeaderMap`. + +## 3.8.0 + ### Added - Add `error::InvalidStatusCode` re-export. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 4dc0f0bd8..0e5c4b234 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.7.0" +version = "3.10.0" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -59,55 +59,59 @@ allowed_external_types = [ default = [] # HTTP/2 protocol support -http2 = ["h2"] +http2 = ["dep:h2"] # WebSocket protocol implementation ws = [ - "local-channel", - "base64", - "rand", - "sha1", + "dep:local-channel", + "dep:base64", + "dep:rand", + "dep:sha1", ] # TLS via OpenSSL -openssl = ["actix-tls/accept", "actix-tls/openssl"] +openssl = ["__tls", "actix-tls/accept", "actix-tls/openssl"] # TLS via Rustls v0.20 -rustls = ["rustls-0_20"] +rustls = ["__tls", "rustls-0_20"] # TLS via Rustls v0.20 -rustls-0_20 = ["actix-tls/accept", "actix-tls/rustls-0_20"] +rustls-0_20 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_20"] # TLS via Rustls v0.21 -rustls-0_21 = ["actix-tls/accept", "actix-tls/rustls-0_21"] +rustls-0_21 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_21"] # TLS via Rustls v0.22 -rustls-0_22 = ["actix-tls/accept", "actix-tls/rustls-0_22"] +rustls-0_22 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_22"] # TLS via Rustls v0.23 -rustls-0_23 = ["actix-tls/accept", "actix-tls/rustls-0_23"] +rustls-0_23 = ["__tls", "actix-tls/accept", "actix-tls/rustls-0_23"] # Compression codecs -compress-brotli = ["__compress", "brotli"] -compress-gzip = ["__compress", "flate2"] -compress-zstd = ["__compress", "zstd"] +compress-brotli = ["__compress", "dep:brotli"] +compress-gzip = ["__compress", "dep:flate2"] +compress-zstd = ["__compress", "dep:zstd"] # Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime. __compress = [] +# Internal (PRIVATE!) features used to aid checking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__tls = [] + [dependencies] actix-service = "2" actix-codec = "0.5" actix-utils = "3" actix-rt = { version = "2.2", default-features = false } -ahash = "0.8" bitflags = "2" bytes = "1" bytestring = "1" -derive_more = "0.99.5" +derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] } encoding_rs = "0.8" +foldhash = "0.1" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } http = "0.2.7" httparse = "1.5.1" @@ -128,14 +132,14 @@ h2 = { version = "0.3.26", optional = true } # websockets local-channel = { version = "0.1", optional = true } base64 = { version = "0.22", optional = true } -rand = { version = "0.8", optional = true } +rand = { version = "0.9", optional = true } sha1 = { version = "0.10", optional = true } # openssl/rustls actix-tls = { version = "3.4", default-features = false, optional = true } # compress-* -brotli = { version = "6", optional = true } +brotli = { version = "7", optional = true } flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.13", optional = true } @@ -151,18 +155,21 @@ divan = "0.1.8" env_logger = "0.11" futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } memchr = "2.4" -once_cell = "1.9" +once_cell = "1.21" rcgen = "0.13" regex = "1.3" rustversion = "1" rustls-pemfile = "2" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1", features = ["derive"] } serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.55" } tls-rustls_023 = { package = "rustls", version = "0.23" } tokio = { version = "1.24.2", features = ["net", "rt", "macros"] } +[lints] +workspace = true + [[example]] name = "ws" required-features = ["ws", "rustls-0_23"] diff --git a/actix-http/README.md b/actix-http/README.md index 0ba3fdcac..1e81cf33c 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -5,11 +5,11 @@ [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.7.0)](https://docs.rs/actix-http/3.7.0) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.10.0)](https://docs.rs/actix-http/3.10.0) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.7.0/status.svg)](https://deps.rs/crate/actix-http/3.7.0) +[![dependency status](https://deps.rs/crate/actix-http/3.10.0/status.svg)](https://deps.rs/crate/actix-http/3.10.0) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/examples/actix-web.rs b/actix-http/examples/actix-web.rs index 449e5899b..e07abfd97 100644 --- a/actix-http/examples/actix-web.rs +++ b/actix-http/examples/actix-web.rs @@ -1,10 +1,10 @@ use actix_http::HttpService; use actix_server::Server; use actix_service::map_config; -use actix_web::{dev::AppConfig, get, App}; +use actix_web::{dev::AppConfig, get, App, Responder}; #[get("/")] -async fn index() -> &'static str { +async fn index() -> impl Responder { "Hello, world. From Actix Web!" } diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index ae6f00cce..11fd2750e 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -23,7 +23,7 @@ async fn main() -> io::Result<()> { body.extend_from_slice(&item?); } - info!("request body: {:?}", body); + info!("request body: {body:?}"); let res = Response::build(StatusCode::OK) .insert_header(("x-head", HeaderValue::from_static("dummy value!"))) @@ -31,8 +31,7 @@ async fn main() -> io::Result<()> { Ok::<_, Error>(res) }) - // No TLS - .tcp() + .tcp() // No TLS })? .run() .await diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs index cf10beddf..afa3883a4 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -17,7 +17,7 @@ async fn main() -> io::Result<()> { ext.insert(42u32); }) .finish(|req: Request| async move { - info!("{:?}", req); + info!("{req:?}"); let mut res = Response::build(StatusCode::OK); res.insert_header(("x-head", HeaderValue::from_static("dummy value!"))); diff --git a/actix-http/examples/streaming-error.rs b/actix-http/examples/streaming-error.rs index 8c8a249cb..39f214fa1 100644 --- a/actix-http/examples/streaming-error.rs +++ b/actix-http/examples/streaming-error.rs @@ -22,16 +22,16 @@ async fn main() -> io::Result<()> { .bind("streaming-error", ("127.0.0.1", 8080), || { HttpService::build() .finish(|req| async move { - info!("{:?}", req); + info!("{req:?}"); let res = Response::ok(); Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! { yield Ok(Bytes::from("123")); yield Ok(Bytes::from("456")); - actix_rt::time::sleep(Duration::from_millis(1000)).await; + actix_rt::time::sleep(Duration::from_secs(1)).await; - yield Err(io::Error::new(io::ErrorKind::Other, "")); + yield Err(io::Error::new(io::ErrorKind::Other, "abc")); }))) }) .tcp() diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index fb86bc5ea..af83e4c3d 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -17,7 +17,6 @@ use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::{ready, Stream}; use tokio_util::codec::Encoder; -use tracing::{info, trace}; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -37,12 +36,12 @@ async fn main() -> io::Result<()> { } async fn handler(req: Request) -> Result>, Error> { - info!("handshaking"); + tracing::info!("handshaking"); let mut res = ws::handshake(req.head())?; // handshake will always fail under HTTP/2 - info!("responding"); + tracing::info!("responding"); res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new()))) } @@ -64,7 +63,7 @@ impl Stream for Heartbeat { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - trace!("poll"); + tracing::trace!("poll"); ready!(self.as_mut().interval.poll_tick(cx)); diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index 4574b2519..657ffe9c8 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -131,7 +131,7 @@ mod tests { assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); } #[derive(Debug, Display, Error)] - #[display(fmt = "stream error")] + #[display("stream error")] struct StreamErr; #[actix_rt::test] diff --git a/actix-http/src/body/utils.rs b/actix-http/src/body/utils.rs index d1449179f..b8bfa96cf 100644 --- a/actix-http/src/body/utils.rs +++ b/actix-http/src/body/utils.rs @@ -38,7 +38,7 @@ pub async fn to_bytes(body: B) -> Result { /// Error type returned from [`to_bytes_limited`] when body produced exceeds limit. #[derive(Debug, Display, Error)] -#[display(fmt = "limit exceeded while collecting body bytes")] +#[display("limit exceeded while collecting body bytes")] #[non_exhaustive] pub struct BodyLimitExceeded; diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 180927ac6..735dca679 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -415,11 +415,11 @@ fn new_brotli_compressor() -> Box> { #[non_exhaustive] pub enum EncoderError { /// Wrapped body stream error. - #[display(fmt = "body")] + #[display("body")] Body(Box), /// Generic I/O error. - #[display(fmt = "io")] + #[display("io")] Io(io::Error), } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 6f332118e..8ef2698a2 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -80,28 +80,28 @@ impl From for Response { #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] pub(crate) enum Kind { - #[display(fmt = "error processing HTTP")] + #[display("error processing HTTP")] Http, - #[display(fmt = "error parsing HTTP message")] + #[display("error parsing HTTP message")] Parse, - #[display(fmt = "request payload read error")] + #[display("request payload read error")] Payload, - #[display(fmt = "response body write error")] + #[display("response body write error")] Body, - #[display(fmt = "send response error")] + #[display("send response error")] SendResponse, - #[display(fmt = "error in WebSocket process")] + #[display("error in WebSocket process")] Ws, - #[display(fmt = "connection error")] + #[display("connection error")] Io, - #[display(fmt = "encoder error")] + #[display("encoder error")] Encoder, } @@ -160,44 +160,44 @@ impl From for Error { #[non_exhaustive] pub enum ParseError { /// An invalid `Method`, such as `GE.T`. - #[display(fmt = "invalid method specified")] + #[display("invalid method specified")] Method, /// An invalid `Uri`, such as `exam ple.domain`. - #[display(fmt = "URI error: {}", _0)] + #[display("URI error: {}", _0)] Uri(InvalidUri), /// An invalid `HttpVersion`, such as `HTP/1.1` - #[display(fmt = "invalid HTTP version specified")] + #[display("invalid HTTP version specified")] Version, /// An invalid `Header`. - #[display(fmt = "invalid Header provided")] + #[display("invalid Header provided")] Header, /// A message head is too large to be reasonable. - #[display(fmt = "message head is too large")] + #[display("message head is too large")] TooLarge, /// A message reached EOF, but is not complete. - #[display(fmt = "message is incomplete")] + #[display("message is incomplete")] Incomplete, /// An invalid `Status`, such as `1337 ELITE`. - #[display(fmt = "invalid status provided")] + #[display("invalid status provided")] Status, /// A timeout occurred waiting for an IO event. #[allow(dead_code)] - #[display(fmt = "timeout")] + #[display("timeout")] Timeout, /// An I/O error that occurred while trying to read or write to a network stream. - #[display(fmt = "I/O error: {}", _0)] + #[display("I/O error: {}", _0)] Io(io::Error), /// Parsing a field as string failed. - #[display(fmt = "UTF-8 error: {}", _0)] + #[display("UTF-8 error: {}", _0)] Utf8(Utf8Error), } @@ -256,28 +256,28 @@ impl From for Response { #[non_exhaustive] pub enum PayloadError { /// A payload reached EOF, but is not complete. - #[display(fmt = "payload reached EOF before completing: {:?}", _0)] + #[display("payload reached EOF before completing: {:?}", _0)] Incomplete(Option), /// Content encoding stream corruption. - #[display(fmt = "can not decode content-encoding")] + #[display("can not decode content-encoding")] EncodingCorrupted, /// Payload reached size limit. - #[display(fmt = "payload reached size limit")] + #[display("payload reached size limit")] Overflow, /// Payload length is unknown. - #[display(fmt = "payload length is unknown")] + #[display("payload length is unknown")] UnknownLength, /// HTTP/2 payload error. #[cfg(feature = "http2")] - #[display(fmt = "{}", _0)] + #[display("{}", _0)] Http2Payload(::h2::Error), /// Generic I/O error. - #[display(fmt = "{}", _0)] + #[display("{}", _0)] Io(io::Error), } @@ -326,44 +326,44 @@ impl From for Error { #[non_exhaustive] pub enum DispatchError { /// Service error. - #[display(fmt = "service error")] + #[display("service error")] Service(Response), /// Body streaming error. - #[display(fmt = "body error: {}", _0)] + #[display("body error: {}", _0)] Body(Box), /// Upgrade service error. - #[display(fmt = "upgrade error")] + #[display("upgrade error")] Upgrade, /// An `io::Error` that occurred while trying to read or write to a network stream. - #[display(fmt = "I/O error: {}", _0)] + #[display("I/O error: {}", _0)] Io(io::Error), /// Request parse error. - #[display(fmt = "request parse error: {}", _0)] + #[display("request parse error: {}", _0)] Parse(ParseError), /// HTTP/2 error. - #[display(fmt = "{}", _0)] + #[display("{}", _0)] #[cfg(feature = "http2")] H2(h2::Error), /// The first request did not complete within the specified timeout. - #[display(fmt = "request did not complete within the specified timeout")] + #[display("request did not complete within the specified timeout")] SlowRequestTimeout, /// Disconnect timeout. Makes sense for TLS streams. - #[display(fmt = "connection shutdown timeout")] + #[display("connection shutdown timeout")] DisconnectTimeout, /// Handler dropped payload before reading EOF. - #[display(fmt = "handler dropped payload before reading EOF")] + #[display("handler dropped payload before reading EOF")] HandlerDroppedPayload, /// Internal error. - #[display(fmt = "internal error")] + #[display("internal error")] InternalError, } @@ -389,11 +389,11 @@ impl StdError for DispatchError { #[non_exhaustive] pub enum ContentTypeError { /// Can not parse content type. - #[display(fmt = "could not parse content type")] + #[display("could not parse content type")] ParseError, /// Unknown content encoding. - #[display(fmt = "unknown content encoding")] + #[display("unknown content encoding")] UnknownEncoding, } diff --git a/actix-http/src/extensions.rs b/actix-http/src/extensions.rs index f2047a9ce..9c85caf37 100644 --- a/actix-http/src/extensions.rs +++ b/actix-http/src/extensions.rs @@ -31,7 +31,7 @@ impl Hasher for NoOpHasher { /// All entries into this map must be owned types (or static references). #[derive(Default)] pub struct Extensions { - /// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys. + // use no-op hasher with a std HashMap with for faster lookups on the small `TypeId` keys map: HashMap, BuildHasherDefault>, } @@ -104,6 +104,46 @@ impl Extensions { .and_then(|boxed| boxed.downcast_mut()) } + /// Inserts the given `value` into the extensions if it is not present, then returns a reference + /// to the value in the extensions. + /// + /// ``` + /// # use actix_http::Extensions; + /// let mut map = Extensions::new(); + /// assert_eq!(map.get::>(), None); + /// + /// map.get_or_insert(Vec::::new()).push(1); + /// assert_eq!(map.get::>(), Some(&vec![1])); + /// + /// map.get_or_insert(Vec::::new()).push(2); + /// assert_eq!(map.get::>(), Some(&vec![1,2])); + /// ``` + pub fn get_or_insert(&mut self, value: T) -> &mut T { + self.get_or_insert_with(|| value) + } + + /// Inserts a value computed from `f` into the extensions if the given `value` is not present, + /// then returns a reference to the value in the extensions. + /// + /// ``` + /// # use actix_http::Extensions; + /// let mut map = Extensions::new(); + /// assert_eq!(map.get::>(), None); + /// + /// map.get_or_insert_with(Vec::::new).push(1); + /// assert_eq!(map.get::>(), Some(&vec![1])); + /// + /// map.get_or_insert_with(Vec::::new).push(2); + /// assert_eq!(map.get::>(), Some(&vec![1,2])); + /// ``` + pub fn get_or_insert_with T>(&mut self, default: F) -> &mut T { + self.map + .entry(TypeId::of::()) + .or_insert_with(|| Box::new(default())) + .downcast_mut() + .expect("extensions map should now contain a T value") + } + /// Remove an item from the map of a given type. /// /// If an item of this type was already stored, it will be returned. diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index abe396ce2..77e34bcdc 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -313,7 +313,7 @@ impl MessageType for RequestHeadType { _ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported version")), } ) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) } } @@ -433,7 +433,7 @@ impl TransferEncoding { buf.extend_from_slice(b"0\r\n\r\n"); } else { writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len()) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; buf.reserve(msg.len() + 2); buf.extend_from_slice(msg); diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index f2f8a0e48..2cf76edb2 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -480,15 +480,15 @@ where let cfg = self.cfg.clone(); Box::pin(async move { - let expect = expect - .await - .map_err(|e| error!("Init http expect service error: {:?}", e))?; + let expect = expect.await.map_err(|err| { + tracing::error!("Initialization of HTTP expect service error: {err:?}"); + })?; let upgrade = match upgrade { Some(upgrade) => { - let upgrade = upgrade - .await - .map_err(|e| error!("Init http upgrade service error: {:?}", e))?; + let upgrade = upgrade.await.map_err(|err| { + tracing::error!("Initialization of HTTP upgrade service error: {err:?}"); + })?; Some(upgrade) } None => None, @@ -496,7 +496,7 @@ where let service = service .await - .map_err(|e| error!("Init http service error: {:?}", e))?; + .map_err(|err| error!("Initialization of HTTP service error: {err:?}"))?; Ok(H1ServiceHandler::new( cfg, @@ -541,6 +541,6 @@ where fn call(&self, (io, addr): (T, Option)) -> Self::Future { let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); - Dispatcher::new(io, self.flow.clone(), self.cfg.clone(), addr, conn_data) + Dispatcher::new(io, Rc::clone(&self.flow), self.cfg.clone(), addr, conn_data) } } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 636ac3161..debc73e59 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -434,7 +434,7 @@ where H2ServiceHandlerResponse { state: State::Handshake( - Some(self.flow.clone()), + Some(Rc::clone(&self.flow)), Some(self.cfg.clone()), addr, on_connect_data, diff --git a/actix-http/src/header/common.rs b/actix-http/src/header/common.rs index 6942dc26a..ebdd6708f 100644 --- a/actix-http/src/header/common.rs +++ b/actix-http/src/header/common.rs @@ -18,6 +18,14 @@ pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status"); // TODO(breaking): replace with http's version pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control"); +/// Response header field that sends a signal to the user agent that it ought to remove all data of +/// a certain set of types. +/// +/// See the [W3C Clear-Site-Data spec] for full semantics. +/// +/// [W3C Clear-Site-Data spec]: https://www.w3.org/TR/clear-site-data/#header +pub const CLEAR_SITE_DATA: HeaderName = HeaderName::from_static("clear-site-data"); + /// Response header that prevents a document from loading any cross-origin resources that don't /// explicitly grant the document permission (using [CORP] or [CORS]). /// diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index b86798a4c..a9a201e1a 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::hash_map, iter, ops}; -use ahash::AHashMap; +use foldhash::{HashMap as FoldHashMap, HashMapExt as _}; use http::header::{HeaderName, HeaderValue}; use smallvec::{smallvec, SmallVec}; @@ -13,8 +13,9 @@ use super::AsHeaderName; /// `HeaderMap` is a "multi-map" of [`HeaderName`] to one or more [`HeaderValue`]s. /// /// # Examples +/// /// ``` -/// use actix_http::header::{self, HeaderMap, HeaderValue}; +/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// /// let mut map = HeaderMap::new(); /// @@ -29,9 +30,24 @@ use super::AsHeaderName; /// /// assert!(!map.contains_key(header::ORIGIN)); /// ``` +/// +/// Construct a header map using the [`FromIterator`] implementation. Note that it uses the append +/// strategy, so duplicate header names are preserved. +/// +/// ``` +/// use actix_http::header::{self, HeaderMap, HeaderValue}; +/// +/// let headers = HeaderMap::from_iter([ +/// (header::CONTENT_TYPE, HeaderValue::from_static("text/plain")), +/// (header::COOKIE, HeaderValue::from_static("foo=1")), +/// (header::COOKIE, HeaderValue::from_static("bar=1")), +/// ]); +/// +/// assert_eq!(headers.len(), 3); +/// ``` #[derive(Debug, Clone, Default)] pub struct HeaderMap { - pub(crate) inner: AHashMap, + pub(crate) inner: FoldHashMap, } /// A bespoke non-empty list for HeaderMap values. @@ -100,7 +116,7 @@ impl HeaderMap { /// ``` pub fn with_capacity(capacity: usize) -> Self { HeaderMap { - inner: AHashMap::with_capacity(capacity), + inner: FoldHashMap::with_capacity(capacity), } } @@ -368,8 +384,8 @@ impl HeaderMap { /// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html")); /// assert!(!removed.is_empty()); /// ``` - pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed { - let value = self.inner.insert(key, Value::one(val)); + pub fn insert(&mut self, name: HeaderName, val: HeaderValue) -> Removed { + let value = self.inner.insert(name, Value::one(val)); Removed::new(value) } @@ -636,6 +652,16 @@ impl<'a> IntoIterator for &'a HeaderMap { } } +impl FromIterator<(HeaderName, HeaderValue)> for HeaderMap { + fn from_iter>(iter: T) -> Self { + iter.into_iter() + .fold(Self::new(), |mut map, (name, value)| { + map.append(name, value); + map + }) + } +} + /// Convert a `http::HeaderMap` to our `HeaderMap`. impl From for HeaderMap { fn from(mut map: http::HeaderMap) -> Self { @@ -804,7 +830,7 @@ impl<'a> Drain<'a> { } } -impl<'a> Iterator for Drain<'a> { +impl Iterator for Drain<'_> { type Item = (Option, HeaderValue); fn next(&mut self) -> Option { diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 79f91afef..b22c43f76 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -42,9 +42,9 @@ pub use self::{ as_name::AsHeaderName, // re-export list is explicit so that any updates to `http` do not conflict with this set common::{ - CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY, - CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST, - X_FORWARDED_PROTO, + CACHE_STATUS, CDN_CACHE_CONTROL, CLEAR_SITE_DATA, CROSS_ORIGIN_EMBEDDER_POLICY, + CROSS_ORIGIN_OPENER_POLICY, CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, + X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO, }, into_pair::TryIntoHeaderPair, into_value::TryIntoHeaderValue, diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index c3b4bc4c2..6c4cc9229 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -11,7 +11,7 @@ use crate::{ /// Error returned when a content encoding is unknown. #[derive(Debug, Display, Error)] -#[display(fmt = "unsupported content encoding")] +#[display("unsupported content encoding")] pub struct ContentEncodingParseError; /// Represents a supported content encoding. diff --git a/actix-http/src/header/shared/quality.rs b/actix-http/src/header/shared/quality.rs index c2276cf1b..c9b6c2ae6 100644 --- a/actix-http/src/header/shared/quality.rs +++ b/actix-http/src/header/shared/quality.rs @@ -125,7 +125,7 @@ pub fn itoa_fmt(mut wr: W, value: V) -> fmt::Re } #[derive(Debug, Clone, Display, Error)] -#[display(fmt = "quality out of bounds")] +#[display("quality out of bounds")] #[non_exhaustive] pub struct QualityOutOfBounds; diff --git a/actix-http/src/helpers.rs b/actix-http/src/helpers.rs index 7f28018e7..61175bdc9 100644 --- a/actix-http/src/helpers.rs +++ b/actix-http/src/helpers.rs @@ -61,7 +61,7 @@ pub fn write_content_length(n: u64, buf: &mut B, camel_case: bool) { /// perform a remaining length check before writing. pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); -impl<'a, B> io::Write for MutWriter<'a, B> +impl io::Write for MutWriter<'_, B> where B: BufMut, { diff --git a/actix-http/src/http_message.rs b/actix-http/src/http_message.rs index 3ba9ef752..2800f40ba 100644 --- a/actix-http/src/http_message.rs +++ b/actix-http/src/http_message.rs @@ -103,7 +103,7 @@ pub trait HttpMessage: Sized { } } -impl<'a, T> HttpMessage for &'a mut T +impl HttpMessage for &mut T where T: HttpMessage, { diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index f9697c4d5..734e6e1e1 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -6,10 +6,10 @@ //! | ------------------- | ------------------------------------------- | //! | `http2` | HTTP/2 support via [h2]. | //! | `openssl` | TLS support via [OpenSSL]. | -//! | `rustls` | TLS support via [rustls] 0.20. | -//! | `rustls-0_21` | TLS support via [rustls] 0.21. | -//! | `rustls-0_22` | TLS support via [rustls] 0.22. | -//! | `rustls-0_23` | TLS support via [rustls] 0.23. | +//! | `rustls-0_20` | TLS support via rustls 0.20. | +//! | `rustls-0_21` | TLS support via rustls 0.21. | +//! | `rustls-0_22` | TLS support via rustls 0.22. | +//! | `rustls-0_23` | TLS support via [rustls] 0.23. | //! | `compress-brotli` | Payload compression support: Brotli. | //! | `compress-gzip` | Payload compression support: Deflate, Gzip. | //! | `compress-zstd` | Payload compression support: Zstd. | @@ -20,8 +20,6 @@ //! [rustls]: https://crates.io/crates/rustls //! [trust-dns]: https://crates.io/crates/trust-dns -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![allow( clippy::type_complexity, clippy::too_many_arguments, @@ -61,13 +59,7 @@ pub mod ws; #[allow(deprecated)] pub use self::payload::PayloadStream; -#[cfg(any( - feature = "openssl", - feature = "rustls-0_20", - feature = "rustls-0_21", - feature = "rustls-0_22", - feature = "rustls-0_23", -))] +#[cfg(feature = "__tls")] pub use self::service::TlsAcceptorConfig; pub use self::{ builder::HttpServiceBuilder, diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 47b128fd0..d2241b229 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -66,7 +66,7 @@ impl ops::DerefMut for Message { impl Drop for Message { fn drop(&mut self) { - T::with_pool(|p| p.release(self.head.clone())) + T::with_pool(|p| p.release(Rc::clone(&self.head))) } } diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index 7d476c55f..d7a52417e 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -41,13 +41,31 @@ pin_project! { } impl From for Payload { + #[inline] fn from(payload: crate::h1::Payload) -> Self { Payload::H1 { payload } } } +impl From for Payload { + #[inline] + fn from(bytes: Bytes) -> Self { + let (_, mut pl) = crate::h1::Payload::create(true); + pl.unread_data(bytes); + self::Payload::from(pl) + } +} + +impl From> for Payload { + #[inline] + fn from(vec: Vec) -> Self { + Payload::from(Bytes::from(vec)) + } +} + #[cfg(feature = "http2")] impl From for Payload { + #[inline] fn from(payload: crate::h2::Payload) -> Self { Payload::H2 { payload } } @@ -55,6 +73,7 @@ impl From for Payload { #[cfg(feature = "http2")] impl From<::h2::RecvStream> for Payload { + #[inline] fn from(stream: ::h2::RecvStream) -> Self { Payload::H2 { payload: crate::h2::Payload::new(stream), @@ -63,13 +82,15 @@ impl From<::h2::RecvStream> for Payload { } impl From for Payload { + #[inline] fn from(payload: BoxedPayloadStream) -> Self { Payload::Stream { payload } } } impl Payload { - /// Takes current payload and replaces it with `None` value + /// Takes current payload and replaces it with `None` value. + #[must_use] pub fn take(&mut self) -> Payload { mem::replace(self, Payload::None) } diff --git a/actix-http/src/responses/builder.rs b/actix-http/src/responses/builder.rs index 91c69ba54..bb7d0f712 100644 --- a/actix-http/src/responses/builder.rs +++ b/actix-http/src/responses/builder.rs @@ -351,12 +351,9 @@ mod tests { assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); let resp = Response::build(StatusCode::OK) - .content_type(mime::APPLICATION_JAVASCRIPT_UTF_8) + .content_type(mime::TEXT_JAVASCRIPT) .body(Bytes::new()); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - "application/javascript; charset=utf-8" - ); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/javascript"); } #[test] diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index a58be93c7..3be099d9f 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -241,25 +241,13 @@ where } /// Configuration options used when accepting TLS connection. -#[cfg(any( - feature = "openssl", - feature = "rustls-0_20", - feature = "rustls-0_21", - feature = "rustls-0_22", - feature = "rustls-0_23", -))] +#[cfg(feature = "__tls")] #[derive(Debug, Default)] pub struct TlsAcceptorConfig { pub(crate) handshake_timeout: Option, } -#[cfg(any( - feature = "openssl", - feature = "rustls-0_20", - feature = "rustls-0_21", - feature = "rustls-0_22", - feature = "rustls-0_23", -))] +#[cfg(feature = "__tls")] impl TlsAcceptorConfig { /// Set TLS handshake timeout duration. pub fn handshake_timeout(self, dur: std::time::Duration) -> Self { @@ -787,23 +775,23 @@ where let cfg = self.cfg.clone(); Box::pin(async move { - let expect = expect - .await - .map_err(|e| error!("Init http expect service error: {:?}", e))?; + let expect = expect.await.map_err(|err| { + tracing::error!("Initialization of HTTP expect service error: {err:?}"); + })?; let upgrade = match upgrade { Some(upgrade) => { - let upgrade = upgrade - .await - .map_err(|e| error!("Init http upgrade service error: {:?}", e))?; + let upgrade = upgrade.await.map_err(|err| { + tracing::error!("Initialization of HTTP upgrade service error: {err:?}"); + })?; Some(upgrade) } None => None, }; - let service = service - .await - .map_err(|e| error!("Init http service error: {:?}", e))?; + let service = service.await.map_err(|err| { + tracing::error!("Initialization of HTTP service error: {err:?}"); + })?; Ok(HttpServiceHandler::new( cfg, @@ -922,7 +910,7 @@ where handshake: Some(( crate::h2::handshake_with_timeout(io, &self.cfg), self.cfg.clone(), - self.flow.clone(), + Rc::clone(&self.flow), conn_data, peer_addr, )), @@ -938,7 +926,7 @@ where state: State::H1 { dispatcher: h1::Dispatcher::new( io, - self.flow.clone(), + Rc::clone(&self.flow), self.cfg.clone(), peer_addr, conn_data, diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 3815e64c6..dfa9a86c9 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -159,8 +159,8 @@ impl TestBuffer { #[allow(dead_code)] pub(crate) fn clone(&self) -> Self { Self { - read_buf: self.read_buf.clone(), - write_buf: self.write_buf.clone(), + read_buf: Rc::clone(&self.read_buf), + write_buf: Rc::clone(&self.write_buf), err: self.err.clone(), } } diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 1354d5ae1..7d0a300b7 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -114,14 +114,14 @@ mod inner { { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - DispatcherError::Service(ref e) => { - write!(fmt, "DispatcherError::Service({:?})", e) + DispatcherError::Service(ref err) => { + write!(fmt, "DispatcherError::Service({err:?})") } - DispatcherError::Encoder(ref e) => { - write!(fmt, "DispatcherError::Encoder({:?})", e) + DispatcherError::Encoder(ref err) => { + write!(fmt, "DispatcherError::Encoder({err:?})") } - DispatcherError::Decoder(ref e) => { - write!(fmt, "DispatcherError::Decoder({:?})", e) + DispatcherError::Decoder(ref err) => { + write!(fmt, "DispatcherError::Decoder({err:?})") } } } @@ -136,9 +136,9 @@ mod inner { { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - DispatcherError::Service(ref e) => write!(fmt, "{}", e), - DispatcherError::Encoder(ref e) => write!(fmt, "{:?}", e), - DispatcherError::Decoder(ref e) => write!(fmt, "{:?}", e), + DispatcherError::Service(ref err) => write!(fmt, "{err}"), + DispatcherError::Encoder(ref err) => write!(fmt, "{err:?}"), + DispatcherError::Decoder(ref err) => write!(fmt, "{err:?}"), } } } diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 3ed53b70a..c2ae010c2 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -27,43 +27,43 @@ pub use self::{ #[derive(Debug, Display, Error, From)] pub enum ProtocolError { /// Received an unmasked frame from client. - #[display(fmt = "received an unmasked frame from client")] + #[display("received an unmasked frame from client")] UnmaskedFrame, /// Received a masked frame from server. - #[display(fmt = "received a masked frame from server")] + #[display("received a masked frame from server")] MaskedFrame, /// Encountered invalid opcode. - #[display(fmt = "invalid opcode ({})", _0)] + #[display("invalid opcode ({})", _0)] InvalidOpcode(#[error(not(source))] u8), /// Invalid control frame length - #[display(fmt = "invalid control frame length ({})", _0)] + #[display("invalid control frame length ({})", _0)] InvalidLength(#[error(not(source))] usize), /// Bad opcode. - #[display(fmt = "bad opcode")] + #[display("bad opcode")] BadOpCode, /// A payload reached size limit. - #[display(fmt = "payload reached size limit")] + #[display("payload reached size limit")] Overflow, /// Continuation has not started. - #[display(fmt = "continuation has not started")] + #[display("continuation has not started")] ContinuationNotStarted, /// Received new continuation but it is already started. - #[display(fmt = "received new continuation but it has already started")] + #[display("received new continuation but it has already started")] ContinuationStarted, /// Unknown continuation fragment. - #[display(fmt = "unknown continuation fragment: {}", _0)] + #[display("unknown continuation fragment: {}", _0)] ContinuationFragment(#[error(not(source))] OpCode), /// I/O error. - #[display(fmt = "I/O error: {}", _0)] + #[display("I/O error: {}", _0)] Io(io::Error), } @@ -71,27 +71,27 @@ pub enum ProtocolError { #[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] pub enum HandshakeError { /// Only get method is allowed. - #[display(fmt = "method not allowed")] + #[display("method not allowed")] GetMethodRequired, /// Upgrade header if not set to WebSocket. - #[display(fmt = "WebSocket upgrade is expected")] + #[display("WebSocket upgrade is expected")] NoWebsocketUpgrade, /// Connection header is not set to upgrade. - #[display(fmt = "connection upgrade is expected")] + #[display("connection upgrade is expected")] NoConnectionUpgrade, /// WebSocket version header is not set. - #[display(fmt = "WebSocket version header is required")] + #[display("WebSocket version header is required")] NoVersionHeader, /// Unsupported WebSocket version. - #[display(fmt = "unsupported WebSocket version")] + #[display("unsupported WebSocket version")] UnsupportedVersion, /// WebSocket key is not set or wrong. - #[display(fmt = "unknown WebSocket key")] + #[display("unknown WebSocket key")] BadWebsocketKey, } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 5888527f1..2d940984d 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -94,7 +94,7 @@ async fn with_query_parameter() { } #[derive(Debug, Display, Error)] -#[display(fmt = "expect failed")] +#[display("expect failed")] struct ExpectFailed; impl From for Response { diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 4dd22b585..83456b0cb 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -398,7 +398,7 @@ async fn h2_response_http_error_handling() { } #[derive(Debug, Display, Error)] -#[display(fmt = "error")] +#[display("error")] struct BadRequest; impl From for Response { diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 3ca0d94c2..43e47c0a4 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -480,7 +480,7 @@ async fn h2_response_http_error_handling() { } #[derive(Debug, Display, Error)] -#[display(fmt = "error")] +#[display("error")] struct BadRequest; impl From for Response { diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 4ba64a53c..aafcde19a 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -16,6 +16,7 @@ use actix_utils::future::{err, ok, ready}; use bytes::Bytes; use derive_more::{Display, Error}; use futures_util::{stream::once, FutureExt as _, StreamExt as _}; +use rand::Rng as _; use regex::Regex; #[actix_rt::test] @@ -62,7 +63,7 @@ async fn h1_2() { } #[derive(Debug, Display, Error)] -#[display(fmt = "expect failed")] +#[display("expect failed")] struct ExpectFailed; impl From for Response { @@ -164,7 +165,10 @@ async fn chunked_payload() { for chunk_size in chunk_sizes.iter() { let mut bytes = Vec::new(); - let random_bytes: Vec = (0..*chunk_size).map(|_| rand::random::()).collect(); + let random_bytes = rand::rng() + .sample_iter(rand::distr::StandardUniform) + .take(*chunk_size) + .collect::>(); bytes.extend(format!("{:X}\r\n", chunk_size).as_bytes()); bytes.extend(&random_bytes[..]); @@ -723,7 +727,7 @@ async fn h1_response_http_error_handling() { } #[derive(Debug, Display, Error)] -#[display(fmt = "error")] +#[display("error")] struct BadRequest; impl From for Response { diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 9a78074c4..da16ab5f5 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -37,16 +37,16 @@ impl WsService { #[derive(Debug, Display, Error, From)] enum WsServiceError { - #[display(fmt = "HTTP error")] + #[display("HTTP error")] Http(actix_http::Error), - #[display(fmt = "WS handshake error")] + #[display("WS handshake error")] Ws(actix_http::ws::HandshakeError), - #[display(fmt = "I/O error")] + #[display("I/O error")] Io(std::io::Error), - #[display(fmt = "dispatcher error")] + #[display("dispatcher error")] Dispatcher, } diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md index 1b44ba4b7..d0c759297 100644 --- a/actix-multipart-derive/CHANGES.md +++ b/actix-multipart-derive/CHANGES.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.7.0 + - Minimum supported Rust version (MSRV) is now 1.72. ## 0.6.1 diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml index e978864a3..d4b228020 100644 --- a/actix-multipart-derive/Cargo.toml +++ b/actix-multipart-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart-derive" -version = "0.6.1" +version = "0.7.0" authors = ["Jacob Halsey "] description = "Multipart form derive macro for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] @@ -18,14 +18,17 @@ all-features = true proc-macro = true [dependencies] +bytesize = "2" darling = "0.20" -parse-size = "1" proc-macro2 = "1" quote = "1" syn = "2" [dev-dependencies] -actix-multipart = "0.6" +actix-multipart = "0.7" actix-web = "4" -rustversion = "1" +rustversion-msrv = "0.100" trybuild = "1" + +[lints] +workspace = true diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md index ec0afffdd..bf75613ed 100644 --- a/actix-multipart-derive/README.md +++ b/actix-multipart-derive/README.md @@ -5,11 +5,11 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive) -[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.6.1)](https://docs.rs/actix-multipart-derive/0.6.1) +[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.7.0)](https://docs.rs/actix-multipart-derive/0.7.0) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart-derive.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.6.1/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.6.1) +[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.7.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.7.0) [![Download](https://img.shields.io/crates/d/actix-multipart-derive.svg)](https://crates.io/crates/actix-multipart-derive) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-multipart-derive/src/lib.rs b/actix-multipart-derive/src/lib.rs index 9552ad2d9..4df9b78aa 100644 --- a/actix-multipart-derive/src/lib.rs +++ b/actix-multipart-derive/src/lib.rs @@ -2,16 +2,15 @@ //! //! See [`macro@MultipartForm`] for usage examples. -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(clippy::disallowed_names)] // false positives in some macro expansions use std::collections::HashSet; +use bytesize::ByteSize; use darling::{FromDeriveInput, FromField, FromMeta}; -use parse_size::parse_size; use proc_macro::TokenStream; use proc_macro2::Ident; use quote::quote; @@ -37,6 +36,7 @@ struct MultipartFormAttrs { duplicate_field: DuplicateField, } +#[allow(clippy::disallowed_names)] // false positive in macro expansion #[derive(FromField, Default)] #[darling(attributes(multipart), default)] struct FieldAttrs { @@ -103,7 +103,7 @@ struct ParsedField<'t> { /// # Field Limits /// /// You can use the `#[multipart(limit = "")]` attribute to set field level limits. The limit -/// string is parsed using [parse_size]. +/// string is parsed using [`bytesize`]. /// /// Note: the form is also subject to the global limits configured using `MultipartFormConfig`. /// @@ -138,7 +138,7 @@ struct ParsedField<'t> { /// `#[multipart(duplicate_field = "")]` attribute: /// /// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted. -/// - "deny": A `MultipartError::UnsupportedField` error response is returned. +/// - "deny": A `MultipartError::UnknownField` error response is returned. /// - "replace": Each field is processed, but only the last one is persisted. /// /// Note that `Vec` fields will ignore this option. @@ -150,7 +150,7 @@ struct ParsedField<'t> { /// struct Form { } /// ``` /// -/// [parse_size]: https://docs.rs/parse-size/1/parse_size +/// [`bytesize`]: https://docs.rs/bytesize/2 #[proc_macro_derive(MultipartForm, attributes(multipart))] pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input: syn::DeriveInput = parse_macro_input!(input); @@ -191,8 +191,8 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?; let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string()); - let limit = match attrs.limit.map(|limit| match parse_size(&limit) { - Ok(size) => Ok(usize::try_from(size).unwrap()), + let limit = match attrs.limit.map(|limit| match limit.parse::() { + Ok(ByteSize(size)) => Ok(usize::try_from(size).unwrap()), Err(err) => Err(syn::Error::new( field.ident.as_ref().unwrap().span(), format!("Could not parse size limit `{}`: {}", limit, err), @@ -229,7 +229,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS // Return value when a field name is not supported by the form let unknown_field_result = if attrs.deny_unknown_fields { quote!(::std::result::Result::Err( - ::actix_multipart::MultipartError::UnsupportedField(field.name().to_string()) + ::actix_multipart::MultipartError::UnknownField(field.name().unwrap().to_string()) )) } else { quote!(::std::result::Result::Ok(())) @@ -292,7 +292,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS limits: &'t mut ::actix_multipart::form::Limits, state: &'t mut ::actix_multipart::form::State, ) -> ::std::pin::Pin<::std::boxed::Box> + 't>> { - match field.name() { + match field.name().unwrap() { #handle_field_impl _ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)), } diff --git a/actix-multipart-derive/tests/trybuild.rs b/actix-multipart-derive/tests/trybuild.rs index 6b25d78df..7bd140324 100644 --- a/actix-multipart-derive/tests/trybuild.rs +++ b/actix-multipart-derive/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.72)] // MSRV +#[rustversion_msrv::msrv] #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr index fc02a78c4..6633086c0 100644 --- a/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr +++ b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr @@ -1,16 +1,16 @@ -error: Could not parse size limit `2 bytes`: invalid digit found in string +error: Could not parse size limit `2 bytes`: couldn't parse "bytes" into a known SI unit, couldn't parse unit of "bytes" --> tests/trybuild/size-limit-parse-fail.rs:6:5 | 6 | description: Text, | ^^^^^^^^^^^ -error: Could not parse size limit `2 megabytes`: invalid digit found in string +error: Could not parse size limit `2 megabytes`: couldn't parse "megabytes" into a known SI unit, couldn't parse unit of "megabytes" --> tests/trybuild/size-limit-parse-fail.rs:12:5 | 12 | description: Text, | ^^^^^^^^^^^ -error: Could not parse size limit `four meters`: invalid digit found in string +error: Could not parse size limit `four meters`: couldn't parse "four meters" into a ByteSize, cannot parse float from empty string --> tests/trybuild/size-limit-parse-fail.rs:18:5 | 18 | description: Text, diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index a91edf9c8..a030fac44 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -2,6 +2,28 @@ ## Unreleased +- Minimum supported Rust version (MSRV) is now 1.75. + +## 0.7.2 + +- Fix re-exported version of `actix-multipart-derive`. + +## 0.7.1 + +- Expose `LimitExceeded` error type. + +## 0.7.0 + +- Add `MultipartError::ContentTypeIncompatible` variant. +- Add `MultipartError::ContentDispositionNameMissing` variant. +- Add `Field::bytes()` method. +- Rename `MultipartError::{NoContentDisposition => ContentDispositionMissing}` variant. +- Rename `MultipartError::{NoContentType => ContentTypeMissing}` variant. +- Rename `MultipartError::{ParseContentType => ContentTypeParse}` variant. +- Rename `MultipartError::{Boundary => BoundaryMissing}` variant. +- Rename `MultipartError::{UnsupportedField => UnknownField}` variant. +- Remove top-level re-exports of `test` utilities. + ## 0.6.2 - Add testing utilities under new module `test`. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 5e9b78d84..f86550f72 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,16 +1,17 @@ [package] name = "actix-multipart" -version = "0.6.2" +version = "0.7.2" authors = [ "Nikolay Kim ", "Jacob Halsey ", + "Rob Ede ", ] -description = "Multipart form support for Actix Web" -keywords = ["http", "web", "framework", "async", "futures"] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" -license = "MIT OR Apache-2.0" -edition = "2021" +description = "Multipart request & form support for Actix Web" +keywords = ["http", "actix", "web", "multipart", "form"] +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] @@ -37,12 +38,11 @@ derive = ["actix-multipart-derive"] tempfile = ["dep:tempfile", "tokio/fs"] [dependencies] -actix-multipart-derive = { version = "=0.6.1", optional = true } +actix-multipart-derive = { version = "=0.7.0", optional = true } actix-utils = "3" actix-web = { version = "4", default-features = false } -bytes = "1" -derive_more = "0.99.5" +derive_more = { version = "2", features = ["display", "error", "from"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } httparse = "1.3" @@ -50,7 +50,7 @@ local-waker = "0.1" log = "0.4" memchr = "2.5" mime = "0.3" -rand = "0.8" +rand = "0.9" serde = "1" serde_json = "1" serde_plain = "1" @@ -59,12 +59,18 @@ tokio = { version = "1.24.2", features = ["sync", "io-util"] } [dev-dependencies] actix-http = "3" -actix-multipart-rfc7578 = "0.10" +actix-multipart-rfc7578 = "0.11" actix-rt = "2.2" actix-test = "0.1" actix-web = "4" +assert_matches = "1" awc = "3" +env_logger = "0.11" futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } +futures-test = "0.3" multer = "3" tokio = { version = "1.24.2", features = ["sync"] } tokio-stream = "0.1" + +[lints] +workspace = true diff --git a/actix-multipart/README.md b/actix-multipart/README.md index d61347f32..ec2e94bd8 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.2)](https://docs.rs/actix-multipart/0.6.2) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.6.2/status.svg)](https://deps.rs/crate/actix-multipart/0.6.2) +[![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) @@ -15,14 +15,18 @@ -Multipart form support for Actix Web. +Multipart request & form support for Actix Web. + +The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level extractor which supports reading [multipart fields](Field), in the order they are sent by the client. + +Due to additional requirements for `multipart/form-data` requests, the higher level [`MultipartForm`] extractor and derive macro only supports this media type. ## Examples ```rust use actix_web::{post, App, HttpServer, Responder}; -use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; +use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -34,7 +38,7 @@ struct Metadata { struct UploadForm { #[multipart(limit = "100MB")] file: TempFile, - json: MPJson, + json: MpJson, } #[post("/videos")] @@ -54,15 +58,17 @@ async fn main() -> std::io::Result<()> { } ``` - +cURL request: -[More available in the examples repo →](https://github.com/actix/examples/tree/master/forms/multipart) - -Curl request : - -```bash +```sh curl -v --request POST \ --url http://localhost:8080/videos \ -F 'json={"name": "Cargo.lock"};type=application/json' \ -F file=@./Cargo.lock ``` + +[`MultipartForm`]: struct@form::MultipartForm + + + +[More available in the examples repo →](https://github.com/actix/examples/tree/master/forms/multipart) diff --git a/actix-multipart/examples/form.rs b/actix-multipart/examples/form.rs new file mode 100644 index 000000000..a90aeff96 --- /dev/null +++ b/actix-multipart/examples/form.rs @@ -0,0 +1,36 @@ +use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; +use actix_web::{middleware::Logger, post, App, HttpServer, Responder}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Metadata { + name: String, +} + +#[derive(Debug, MultipartForm)] +struct UploadForm { + #[multipart(limit = "100MB")] + file: TempFile, + json: MpJson, +} + +#[post("/videos")] +async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder { + format!( + "Uploaded file {}, with size: {}\ntemporary file ({}) was deleted\n", + form.json.name, + form.file.size, + form.file.file.path().display(), + ) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + HttpServer::new(move || App::new().service(post_video).wrap(Logger::default())) + .workers(2) + .bind(("127.0.0.1", 8080))? + .run() + .await +} diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index 77b5a559f..cb4f06d39 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -10,78 +10,96 @@ use derive_more::{Display, Error, From}; /// A set of errors that can occur during parsing multipart streams. #[derive(Debug, Display, From, Error)] #[non_exhaustive] -pub enum MultipartError { - /// Content-Disposition header is not found or is not equal to "form-data". +pub enum Error { + /// Could not find Content-Type header. + #[display("Could not find Content-Type header")] + ContentTypeMissing, + + /// Could not parse Content-Type header. + #[display("Could not parse Content-Type header")] + ContentTypeParse, + + /// Parsed Content-Type did not have "multipart" top-level media type. /// - /// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a - /// Content-Disposition header must always be present and equal to "form-data". - #[display(fmt = "No Content-Disposition `form-data` header")] - NoContentDisposition, + /// Also raised when extracting a [`MultipartForm`] from a request that does not have the + /// "multipart/form-data" media type. + /// + /// [`MultipartForm`]: struct@crate::form::MultipartForm + #[display("Parsed Content-Type did not have 'multipart' top-level media type")] + ContentTypeIncompatible, - /// Content-Type header is not found - #[display(fmt = "No Content-Type header found")] - NoContentType, + /// Multipart boundary is not found. + #[display("Multipart boundary is not found")] + BoundaryMissing, - /// Can not parse Content-Type header - #[display(fmt = "Can not parse Content-Type header")] - ParseContentType, + /// Content-Disposition header was not found or not of disposition type "form-data" when parsing + /// a "form-data" field. + /// + /// As per [RFC 7578 §4.2], a "multipart/form-data" field's Content-Disposition header must + /// always be present and have a disposition type of "form-data". + /// + /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + #[display("Content-Disposition header was not found when parsing a \"form-data\" field")] + ContentDispositionMissing, - /// Multipart boundary is not found - #[display(fmt = "Multipart boundary is not found")] - Boundary, + /// Content-Disposition name parameter was not found when parsing a "form-data" field. + /// + /// As per [RFC 7578 §4.2], a "multipart/form-data" field's Content-Disposition header must + /// always include a "name" parameter. + /// + /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + #[display("Content-Disposition header was not found when parsing a \"form-data\" field")] + ContentDispositionNameMissing, - /// Nested multipart is not supported - #[display(fmt = "Nested multipart is not supported")] + /// Nested multipart is not supported. + #[display("Nested multipart is not supported")] Nested, - /// Multipart stream is incomplete - #[display(fmt = "Multipart stream is incomplete")] + /// Multipart stream is incomplete. + #[display("Multipart stream is incomplete")] Incomplete, - /// Error during field parsing - #[display(fmt = "{}", _0)] + /// Field parsing failed. + #[display("Error during field parsing")] Parse(ParseError), - /// Payload error - #[display(fmt = "{}", _0)] + /// HTTP payload error. + #[display("Payload error")] Payload(PayloadError), - /// Not consumed - #[display(fmt = "Multipart stream is not consumed")] + /// Stream is not consumed. + #[display("Stream is not consumed")] NotConsumed, - /// An error from a field handler in a form - #[display( - fmt = "An error occurred processing field `{}`: {}", - field_name, - source - )] + /// Form field handler raised error. + #[display("An error occurred processing field: {name}")] Field { - field_name: String, + name: String, source: actix_web::Error, }, - /// Duplicate field - #[display(fmt = "Duplicate field found for: `{}`", _0)] + /// Duplicate field found (for structure that opted-in to denying duplicate fields). + #[display("Duplicate field found: {_0}")] #[from(ignore)] DuplicateField(#[error(not(source))] String), - /// Missing field - #[display(fmt = "Field with name `{}` is required", _0)] + /// Required field is missing. + #[display("Required field is missing: {_0}")] #[from(ignore)] MissingField(#[error(not(source))] String), - /// Unknown field - #[display(fmt = "Unsupported field `{}`", _0)] + /// Unknown field (for structure that opted-in to denying unknown fields). + #[display("Unknown field: {_0}")] #[from(ignore)] - UnsupportedField(#[error(not(source))] String), + UnknownField(#[error(not(source))] String), } -/// Return `BadRequest` for `MultipartError` -impl ResponseError for MultipartError { +/// Return `BadRequest` for `MultipartError`. +impl ResponseError for Error { fn status_code(&self) -> StatusCode { match &self { - MultipartError::Field { source, .. } => source.as_response_error().status_code(), + Error::Field { source, .. } => source.as_response_error().status_code(), + Error::ContentTypeIncompatible => StatusCode::UNSUPPORTED_MEDIA_TYPE, _ => StatusCode::BAD_REQUEST, } } @@ -93,7 +111,7 @@ mod tests { #[test] fn test_multipart_error() { - let resp = MultipartError::Boundary.error_response(); + let resp = Error::BoundaryMissing.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index 56ed69ae4..31999228e 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -1,21 +1,20 @@ -//! Multipart payload support - use actix_utils::future::{ready, Ready}; use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; -use crate::server::Multipart; +use crate::multipart::Multipart; -/// Get request's payload as multipart stream. +/// Extract request's payload as multipart stream. /// -/// Content-type: multipart/form-data; +/// Content-type: multipart/*; /// /// # Examples +/// /// ``` -/// use actix_web::{web, HttpResponse, Error}; +/// use actix_web::{web, HttpResponse}; /// use actix_multipart::Multipart; /// use futures_util::StreamExt as _; /// -/// async fn index(mut payload: Multipart) -> Result { +/// async fn index(mut payload: Multipart) -> actix_web::Result { /// // iterate over multipart stream /// while let Some(item) = payload.next().await { /// let mut field = item?; @@ -26,7 +25,7 @@ use crate::server::Multipart; /// } /// } /// -/// Ok(HttpResponse::Ok().into()) +/// Ok(HttpResponse::Ok().finish()) /// } /// ``` impl FromRequest for Multipart { @@ -35,9 +34,6 @@ impl FromRequest for Multipart { #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - ready(Ok(match Multipart::boundary(req.headers()) { - Ok(boundary) => Multipart::from_boundary(boundary, payload.take()), - Err(err) => Multipart::from_error(err), - })) + ready(Ok(Multipart::from_req(req, payload))) } } diff --git a/actix-multipart/src/field.rs b/actix-multipart/src/field.rs new file mode 100644 index 000000000..0bbb8a657 --- /dev/null +++ b/actix-multipart/src/field.rs @@ -0,0 +1,501 @@ +use std::{ + cell::RefCell, + cmp, fmt, + future::poll_fn, + mem, + pin::Pin, + rc::Rc, + task::{ready, Context, Poll}, +}; + +use actix_web::{ + error::PayloadError, + http::header::{self, ContentDisposition, HeaderMap}, + web::{Bytes, BytesMut}, +}; +use derive_more::{Display, Error}; +use futures_core::Stream; +use mime::Mime; + +use crate::{ + error::Error, + payload::{PayloadBuffer, PayloadRef}, + safety::Safety, +}; + +/// Error type returned from [`Field::bytes()`] when field data is larger than limit. +#[derive(Debug, Display, Error)] +#[display("size limit exceeded while collecting field data")] +#[non_exhaustive] +pub struct LimitExceeded; + +/// A single field in a multipart stream. +pub struct Field { + /// Field's Content-Type. + content_type: Option, + + /// Field's Content-Disposition. + content_disposition: Option, + + /// Form field name. + /// + /// A non-optional storage for form field names to avoid unwraps in `form` module. Will be an + /// empty string in non-form contexts. + /// + // INVARIANT: always non-empty when request content-type is multipart/form-data. + pub(crate) form_field_name: String, + + /// Field's header map. + headers: HeaderMap, + + safety: Safety, + inner: Rc>, +} + +impl Field { + pub(crate) fn new( + content_type: Option, + content_disposition: Option, + form_field_name: Option, + headers: HeaderMap, + safety: Safety, + inner: Rc>, + ) -> Self { + Field { + content_type, + content_disposition, + form_field_name: form_field_name.unwrap_or_default(), + headers, + inner, + safety, + } + } + + /// Returns a reference to the field's header map. + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Returns a reference to the field's content (mime) type, if it is supplied by the client. + /// + /// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not + /// present, it should default to "text/plain". Note it is the responsibility of the client to + /// provide the appropriate content type, there is no attempt to validate this by the server. + pub fn content_type(&self) -> Option<&Mime> { + self.content_type.as_ref() + } + + /// Returns this field's parsed Content-Disposition header, if set. + /// + /// # Validation + /// + /// Per [RFC 7578 §4.2], the parts of a multipart/form-data payload MUST contain a + /// Content-Disposition header field where the disposition type is `form-data` and MUST also + /// contain an additional parameter of `name` with its value being the original field name from + /// the form. This requirement is enforced during extraction for multipart/form-data requests, + /// but not other kinds of multipart requests (such as multipart/related). + /// + /// As such, it is safe to `.unwrap()` calls `.content_disposition()` if you've verified. + /// + /// The [`name()`](Self::name) method is also provided as a convenience for obtaining the + /// aforementioned name parameter. + /// + /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + pub fn content_disposition(&self) -> Option<&ContentDisposition> { + self.content_disposition.as_ref() + } + + /// Returns the field's name, if set. + /// + /// See [`content_disposition()`](Self::content_disposition) regarding guarantees on presence of + /// the "name" field. + pub fn name(&self) -> Option<&str> { + self.content_disposition()?.get_name() + } + + /// Collects the raw field data, up to `limit` bytes. + /// + /// # Errors + /// + /// Any errors produced by the data stream are returned as `Ok(Err(Error))` immediately. + /// + /// If the buffered data size would exceed `limit`, an `Err(LimitExceeded)` is returned. Note + /// that, in this case, the full data stream is exhausted before returning the error so that + /// subsequent fields can still be read. To better defend against malicious/infinite requests, + /// it is advisable to also put a timeout on this call. + pub async fn bytes(&mut self, limit: usize) -> Result, LimitExceeded> { + /// Sensible default (2kB) for initial, bounded allocation when collecting body bytes. + const INITIAL_ALLOC_BYTES: usize = 2 * 1024; + + let mut exceeded_limit = false; + let mut buf = BytesMut::with_capacity(INITIAL_ALLOC_BYTES); + + let mut field = Pin::new(self); + + match poll_fn(|cx| loop { + match ready!(field.as_mut().poll_next(cx)) { + // if already over limit, discard chunk to advance multipart request + Some(Ok(_chunk)) if exceeded_limit => {} + + // if limit is exceeded set flag to true and continue + Some(Ok(chunk)) if buf.len() + chunk.len() > limit => { + exceeded_limit = true; + // eagerly de-allocate field data buffer + let _ = mem::take(&mut buf); + } + + Some(Ok(chunk)) => buf.extend_from_slice(&chunk), + + None => return Poll::Ready(Ok(())), + Some(Err(err)) => return Poll::Ready(Err(err)), + } + }) + .await + { + // propagate error returned from body poll + Err(err) => Ok(Err(err)), + + // limit was exceeded while reading body + Ok(()) if exceeded_limit => Err(LimitExceeded), + + // otherwise return body buffer + Ok(()) => Ok(Ok(buf.freeze())), + } + } +} + +impl Stream for Field { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + let mut inner = this.inner.borrow_mut(); + + if let Some(mut buffer) = inner + .payload + .as_ref() + .expect("Field should not be polled after completion") + .get_mut(&this.safety) + { + // check safety and poll read payload to buffer. + buffer.poll_stream(cx)?; + } else if !this.safety.is_clean() { + // safety violation + return Poll::Ready(Some(Err(Error::NotConsumed))); + } else { + return Poll::Pending; + } + + inner.poll(&this.safety) + } +} + +impl fmt::Debug for Field { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ct) = &self.content_type { + writeln!(f, "\nField: {}", ct)?; + } else { + writeln!(f, "\nField:")?; + } + writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; + writeln!(f, " headers:")?; + for (key, val) in self.headers.iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + +pub(crate) struct InnerField { + /// Payload is initialized as Some and is `take`n when the field stream finishes. + payload: Option, + + /// Field boundary (without "--" prefix). + boundary: String, + + /// True if request payload has been exhausted. + eof: bool, + + /// Field data's stated size according to it's Content-Length header. + length: Option, +} + +impl InnerField { + pub(crate) fn new_in_rc( + payload: PayloadRef, + boundary: String, + headers: &HeaderMap, + ) -> Result>, PayloadError> { + Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this))) + } + + pub(crate) fn new( + payload: PayloadRef, + boundary: String, + headers: &HeaderMap, + ) -> Result { + let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { + match len.to_str().ok().and_then(|len| len.parse::().ok()) { + Some(len) => Some(len), + None => return Err(PayloadError::Incomplete(None)), + } + } else { + None + }; + + Ok(InnerField { + boundary, + payload: Some(payload), + eof: false, + length: len, + }) + } + + /// Reads body part content chunk of the specified size. + /// + /// The body part must has `Content-Length` header with proper value. + pub(crate) fn read_len( + payload: &mut PayloadBuffer, + size: &mut u64, + ) -> Poll>> { + if *size == 0 { + Poll::Ready(None) + } else { + match payload.read_max(*size)? { + Some(mut chunk) => { + let len = cmp::min(chunk.len() as u64, *size); + *size -= len; + let ch = chunk.split_to(len as usize); + if !chunk.is_empty() { + payload.unprocessed(chunk); + } + Poll::Ready(Some(Ok(ch))) + } + None => { + if payload.eof && (*size != 0) { + Poll::Ready(Some(Err(Error::Incomplete))) + } else { + Poll::Pending + } + } + } + } + } + + /// Reads content chunk of body part with unknown length. + /// + /// The `Content-Length` header for body part is not necessary. + pub(crate) fn read_stream( + payload: &mut PayloadBuffer, + boundary: &str, + ) -> Poll>> { + let mut pos = 0; + + let len = payload.buf.len(); + + if len == 0 { + return if payload.eof { + Poll::Ready(Some(Err(Error::Incomplete))) + } else { + Poll::Pending + }; + } + + // check boundary + if len > 4 && payload.buf[0] == b'\r' { + let b_len = if payload.buf.starts_with(b"\r\n") && &payload.buf[2..4] == b"--" { + Some(4) + } else if &payload.buf[1..3] == b"--" { + Some(3) + } else { + None + }; + + if let Some(b_len) = b_len { + let b_size = boundary.len() + b_len; + if len < b_size { + return Poll::Pending; + } else if &payload.buf[b_len..b_size] == boundary.as_bytes() { + // found boundary + return Poll::Ready(None); + } + } + } + + loop { + return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") { + let cur = pos + idx; + + // check if we have enough data for boundary detection + if cur + 4 > len { + if cur > 0 { + Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze()))) + } else { + Poll::Pending + } + } else { + // check boundary + if (&payload.buf[cur..cur + 2] == b"\r\n" + && &payload.buf[cur + 2..cur + 4] == b"--") + || (&payload.buf[cur..=cur] == b"\r" + && &payload.buf[cur + 1..cur + 3] == b"--") + { + if cur != 0 { + // return buffer + Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze()))) + } else { + pos = cur + 1; + continue; + } + } else { + // not boundary + pos = cur + 1; + continue; + } + } + } else { + Poll::Ready(Some(Ok(payload.buf.split().freeze()))) + }; + } + } + + pub(crate) fn poll(&mut self, safety: &Safety) -> Poll>> { + if self.payload.is_none() { + return Poll::Ready(None); + } + + let Some(mut payload) = self + .payload + .as_ref() + .expect("Field should not be polled after completion") + .get_mut(safety) + else { + return Poll::Pending; + }; + + if !self.eof { + let res = if let Some(ref mut len) = self.length { + Self::read_len(&mut payload, len) + } else { + Self::read_stream(&mut payload, &self.boundary) + }; + + match ready!(res) { + Some(Ok(bytes)) => return Poll::Ready(Some(Ok(bytes))), + Some(Err(err)) => return Poll::Ready(Some(Err(err))), + None => self.eof = true, + } + } + + let result = match payload.readline() { + Ok(None) => Poll::Pending, + Ok(Some(line)) => { + if line.as_ref() != b"\r\n" { + log::warn!("multipart field did not read all the data or it is malformed"); + } + Poll::Ready(None) + } + Err(err) => Poll::Ready(Some(Err(err))), + }; + + drop(payload); + + if let Poll::Ready(None) = result { + // drop payload buffer and make future un-poll-able + let _ = self.payload.take(); + } + + result + } +} + +#[cfg(test)] +mod tests { + use futures_util::{stream, StreamExt as _}; + + use super::*; + use crate::Multipart; + + // TODO: use test utility when multi-file support is introduced + fn create_double_request_with_header() -> (Bytes, HeaderMap) { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\ + \r\n\ + one+one+one\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\ + \r\n\ + two+two+two\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + (bytes, headers) + } + + #[actix_rt::test] + async fn bytes_unlimited() { + let (body, headers) = create_double_request_with_header(); + + let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)])); + + let field = multipart + .next() + .await + .expect("multipart should have two fields") + .expect("multipart body should be well formatted") + .bytes(usize::MAX) + .await + .expect("field data should not be size limited") + .expect("reading field data should not error"); + assert_eq!(field, "one+one+one"); + + let field = multipart + .next() + .await + .expect("multipart should have two fields") + .expect("multipart body should be well formatted") + .bytes(usize::MAX) + .await + .expect("field data should not be size limited") + .expect("reading field data should not error"); + assert_eq!(field, "two+two+two"); + } + + #[actix_rt::test] + async fn bytes_limited() { + let (body, headers) = create_double_request_with_header(); + + let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)])); + + multipart + .next() + .await + .expect("multipart should have two fields") + .expect("multipart body should be well formatted") + .bytes(8) // smaller than data size + .await + .expect_err("field data should be size limited"); + + // next field still readable + let field = multipart + .next() + .await + .expect("multipart should have two fields") + .expect("multipart body should be well formatted") + .bytes(usize::MAX) + .await + .expect("field data should not be size limited") + .expect("reading field data should not error"); + assert_eq!(field, "two+two+two"); + } +} diff --git a/actix-multipart/src/form/bytes.rs b/actix-multipart/src/form/bytes.rs index 3c5e2eb10..51b0cf7d9 100644 --- a/actix-multipart/src/form/bytes.rs +++ b/actix-multipart/src/form/bytes.rs @@ -1,7 +1,6 @@ //! Reads a field into memory. -use actix_web::HttpRequest; -use bytes::BytesMut; +use actix_web::{web::BytesMut, HttpRequest}; use futures_core::future::LocalBoxFuture; use futures_util::TryStreamExt as _; use mime::Mime; @@ -15,7 +14,7 @@ use crate::{ #[derive(Debug)] pub struct Bytes { /// The data. - pub data: bytes::Bytes, + pub data: actix_web::web::Bytes, /// The value of the `Content-Type` header. pub content_type: Option, @@ -41,8 +40,9 @@ impl<'t> FieldReader<'t> for Bytes { content_type: field.content_type().map(ToOwned::to_owned), file_name: field .content_disposition() + .expect("multipart form fields should have a content-disposition header") .get_filename() - .map(str::to_owned), + .map(ToOwned::to_owned), }) }) } diff --git a/actix-multipart/src/form/json.rs b/actix-multipart/src/form/json.rs index bb4e03bf6..70874e5de 100644 --- a/actix-multipart/src/form/json.rs +++ b/actix-multipart/src/form/json.rs @@ -32,7 +32,6 @@ where fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { Box::pin(async move { let config = JsonConfig::from_req(req); - let field_name = field.name().to_owned(); if config.validate_content_type { let valid = if let Some(mime) = field.content_type() { @@ -43,17 +42,19 @@ where if !valid { return Err(MultipartError::Field { - field_name, + name: field.form_field_name, source: config.map_error(req, JsonFieldError::ContentType), }); } } + let form_field_name = field.form_field_name.clone(); + let bytes = Bytes::read_field(req, field, limits).await?; Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err( |err| MultipartError::Field { - field_name, + name: form_field_name, source: config.map_error(req, JsonFieldError::Deserialize(err)), }, )?)) @@ -65,11 +66,11 @@ where #[non_exhaustive] pub enum JsonFieldError { /// Deserialize error. - #[display(fmt = "Json deserialize error: {}", _0)] + #[display("Json deserialize error: {}", _0)] Deserialize(serde_json::Error), /// Content type error. - #[display(fmt = "Content type error")] + #[display("Content type error")] ContentType, } @@ -133,8 +134,7 @@ impl Default for JsonConfig { mod tests { use std::collections::HashMap; - use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; - use bytes::Bytes; + use actix_web::{http::StatusCode, web, web::Bytes, App, HttpResponse, Responder}; use crate::form::{ json::{Json, JsonConfig}, diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs index 68cdefec5..693a45e8e 100644 --- a/actix-multipart/src/form/mod.rs +++ b/actix-multipart/src/form/mod.rs @@ -1,4 +1,4 @@ -//! Process and extract typed data from a multipart stream. +//! Extract and process typed data from fields of a `multipart/form-data` request. use std::{ any::Any, @@ -80,13 +80,13 @@ where state: &'t mut State, duplicate_field: DuplicateField, ) -> Self::Future { - if state.contains_key(field.name()) { + if state.contains_key(&field.form_field_name) { match duplicate_field { DuplicateField::Ignore => return Box::pin(ready(Ok(()))), DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( - field.name().to_owned(), + field.form_field_name, )))) } @@ -95,7 +95,7 @@ where } Box::pin(async move { - let field_name = field.name().to_owned(); + let field_name = field.form_field_name.clone(); let t = T::read_field(req, field, limits).await?; state.insert(field_name, Box::new(t)); Ok(()) @@ -123,10 +123,8 @@ where Box::pin(async move { // Note: Vec GroupReader always allows duplicates - let field_name = field.name().to_owned(); - let vec = state - .entry(field_name) + .entry(field.form_field_name.clone()) .or_insert_with(|| Box::>::default()) .downcast_mut::>() .unwrap(); @@ -159,13 +157,13 @@ where state: &'t mut State, duplicate_field: DuplicateField, ) -> Self::Future { - if state.contains_key(field.name()) { + if state.contains_key(&field.form_field_name) { match duplicate_field { DuplicateField::Ignore => return Box::pin(ready(Ok(()))), DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( - field.name().to_owned(), + field.form_field_name, )))) } @@ -174,7 +172,7 @@ where } Box::pin(async move { - let field_name = field.name().to_owned(); + let field_name = field.form_field_name.clone(); let t = T::read_field(req, field, limits).await?; state.insert(field_name, Box::new(t)); Ok(()) @@ -281,6 +279,9 @@ impl Limits { /// [`MultipartCollect`] trait. You should use the [`macro@MultipartForm`] macro to derive this /// for your struct. /// +/// Note that this extractor rejects requests with any other Content-Type such as `multipart/mixed`, +/// `multipart/related`, or non-multipart media types. +/// /// Add a [`MultipartFormConfig`] to your app data to configure extraction. #[derive(Deref, DerefMut)] pub struct MultipartForm(pub T); @@ -294,14 +295,24 @@ impl MultipartForm { impl FromRequest for MultipartForm where - T: MultipartCollect, + T: MultipartCollect + 'static, { type Error = Error; type Future = LocalBoxFuture<'static, Result>; #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { - let mut payload = Multipart::new(req.headers(), payload.take()); + let mut multipart = Multipart::from_req(req, payload); + + let content_type = match multipart.content_type_or_bail() { + Ok(content_type) => content_type, + Err(err) => return Box::pin(ready(Err(err.into()))), + }; + + if content_type.subtype() != mime::FORM_DATA { + // this extractor only supports multipart/form-data + return Box::pin(ready(Err(MultipartError::ContentTypeIncompatible.into()))); + }; let config = MultipartFormConfig::from_req(req); let mut limits = Limits::new(config.total_limit, config.memory_limit); @@ -313,14 +324,20 @@ where Box::pin( async move { let mut state = State::default(); - // We need to ensure field limits are shared for all instances of this field name + + // ensure limits are shared for all fields with this name let mut field_limits = HashMap::>::new(); - while let Some(field) = payload.try_next().await? { + while let Some(field) = multipart.try_next().await? { + debug_assert!( + !field.form_field_name.is_empty(), + "multipart form fields should have names", + ); + // Retrieve the limit for this field let entry = field_limits - .entry(field.name().to_owned()) - .or_insert_with(|| T::limit(field.name())); + .entry(field.form_field_name.clone()) + .or_insert_with(|| T::limit(&field.form_field_name)); limits.field_limit_remaining.clone_from(entry); @@ -329,6 +346,7 @@ where // Update the stored limit *entry = limits.field_limit_remaining; } + let inner = T::from_state(state)?; Ok(MultipartForm(inner)) } @@ -752,6 +770,41 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } + #[actix_rt::test] + async fn non_multipart_form_data() { + #[derive(MultipartForm)] + struct TestNonMultipartFormData { + #[allow(unused)] + #[multipart(limit = "30B")] + foo: Text, + } + + async fn non_multipart_form_data_route( + _form: MultipartForm, + ) -> String { + unreachable!("request is sent with multipart/mixed"); + } + + let srv = actix_test::start(|| { + App::new().route("/", web::post().to(non_multipart_form_data_route)) + }); + + let mut form = multipart::Form::default(); + form.add_text("foo", "foo"); + + // mangle content-type, keeping the boundary + let ct = form.content_type().replacen("/form-data", "/mixed", 1); + + let res = Client::default() + .post(srv.url("/")) + .content_type(ct) + .send_body(multipart::Body::from(form)) + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + } + #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: Connect(Disconnected)")] #[actix_web::test] async fn field_try_next_panic() { diff --git a/actix-multipart/src/form/tempfile.rs b/actix-multipart/src/form/tempfile.rs index 9371a026b..218b91bff 100644 --- a/actix-multipart/src/form/tempfile.rs +++ b/actix-multipart/src/form/tempfile.rs @@ -42,38 +42,36 @@ impl<'t> FieldReader<'t> for TempFile { fn read_field(req: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future { Box::pin(async move { let config = TempFileConfig::from_req(req); - let field_name = field.name().to_owned(); let mut size = 0; - let file = config - .create_tempfile() - .map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?; + let file = config.create_tempfile().map_err(|err| { + config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) + })?; - let mut file_async = - tokio::fs::File::from_std(file.reopen().map_err(|err| { - config.map_error(req, &field_name, TempFileError::FileIo(err)) - })?); + let mut file_async = tokio::fs::File::from_std(file.reopen().map_err(|err| { + config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) + })?); while let Some(chunk) = field.try_next().await? { limits.try_consume_limits(chunk.len(), false)?; size += chunk.len(); file_async.write_all(chunk.as_ref()).await.map_err(|err| { - config.map_error(req, &field_name, TempFileError::FileIo(err)) + config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) })?; } - file_async - .flush() - .await - .map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?; + file_async.flush().await.map_err(|err| { + config.map_error(req, &field.form_field_name, TempFileError::FileIo(err)) + })?; Ok(TempFile { file, content_type: field.content_type().map(ToOwned::to_owned), file_name: field .content_disposition() + .expect("multipart form fields should have a content-disposition header") .get_filename() - .map(str::to_owned), + .map(ToOwned::to_owned), size, }) }) @@ -84,7 +82,7 @@ impl<'t> FieldReader<'t> for TempFile { #[non_exhaustive] pub enum TempFileError { /// File I/O Error - #[display(fmt = "File I/O error: {}", _0)] + #[display("File I/O error: {}", _0)] FileIo(std::io::Error), } @@ -137,7 +135,7 @@ impl TempFileConfig { }; MultipartError::Field { - field_name: field_name.to_owned(), + name: field_name.to_owned(), source, } } diff --git a/actix-multipart/src/form/text.rs b/actix-multipart/src/form/text.rs index 83e211524..b56a20353 100644 --- a/actix-multipart/src/form/text.rs +++ b/actix-multipart/src/form/text.rs @@ -36,7 +36,6 @@ where fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { Box::pin(async move { let config = TextConfig::from_req(req); - let field_name = field.name().to_owned(); if config.validate_content_type { let valid = if let Some(mime) = field.content_type() { @@ -49,22 +48,24 @@ where if !valid { return Err(MultipartError::Field { - field_name, + name: field.form_field_name, source: config.map_error(req, TextError::ContentType), }); } } + let form_field_name = field.form_field_name.clone(); + let bytes = Bytes::read_field(req, field, limits).await?; let text = str::from_utf8(&bytes.data).map_err(|err| MultipartError::Field { - field_name: field_name.clone(), + name: form_field_name.clone(), source: config.map_error(req, TextError::Utf8Error(err)), })?; Ok(Text(serde_plain::from_str(text).map_err(|err| { MultipartError::Field { - field_name, + name: form_field_name, source: config.map_error(req, TextError::Deserialize(err)), } })?)) @@ -76,15 +77,15 @@ where #[non_exhaustive] pub enum TextError { /// UTF-8 decoding error. - #[display(fmt = "UTF-8 decoding error: {}", _0)] + #[display("UTF-8 decoding error: {}", _0)] Utf8Error(str::Utf8Error), /// Deserialize error. - #[display(fmt = "Plain text deserialize error: {}", _0)] + #[display("Plain text deserialize error: {}", _0)] Deserialize(serde_plain::Error), /// Content type error. - #[display(fmt = "Content type error")] + #[display("Content type error")] ContentType, } diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 51b06db38..8eea35f2e 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,11 +1,19 @@ -//! Multipart form support for Actix Web. +//! Multipart request & form support for Actix Web. +//! +//! The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including +//! `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level +//! extractor which supports reading [multipart fields](Field), in the order they are sent by the +//! client. +//! +//! Due to additional requirements for `multipart/form-data` requests, the higher level +//! [`MultipartForm`] extractor and derive macro only supports this media type. //! //! # Examples //! //! ```no_run //! use actix_web::{post, App, HttpServer, Responder}; //! -//! use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; +//! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; //! use serde::Deserialize; //! //! #[derive(Debug, Deserialize)] @@ -17,7 +25,7 @@ //! struct UploadForm { //! #[multipart(limit = "100MB")] //! file: TempFile, -//! json: MPJson, +//! json: MpJson, //! } //! //! #[post("/videos")] @@ -36,10 +44,18 @@ //! .await //! } //! ``` +//! +//! cURL request: +//! +//! ```sh +//! curl -v --request POST \ +//! --url http://localhost:8080/videos \ +//! -F 'json={"name": "Cargo.lock"};type=application/json' \ +//! -F file=@./Cargo.lock +//! ``` +//! +//! [`MultipartForm`]: struct@form::MultipartForm -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] -#![allow(clippy::borrow_interior_mutable_const)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -50,14 +66,15 @@ extern crate self as actix_multipart; mod error; mod extractor; +pub(crate) mod field; pub mod form; -mod server; +mod multipart; +pub(crate) mod payload; +pub(crate) mod safety; pub mod test; pub use self::{ - error::MultipartError, - server::{Field, Multipart}, - test::{ - create_form_data_payload_and_headers, create_form_data_payload_and_headers_with_boundary, - }, + error::Error as MultipartError, + field::{Field, LimitExceeded}, + multipart::Multipart, }; diff --git a/actix-multipart/src/multipart.rs b/actix-multipart/src/multipart.rs new file mode 100644 index 000000000..e38fbde9e --- /dev/null +++ b/actix-multipart/src/multipart.rs @@ -0,0 +1,883 @@ +//! Multipart response payload support. + +use std::{ + cell::RefCell, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use actix_web::{ + dev, + error::{ParseError, PayloadError}, + http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}, + web::Bytes, + HttpRequest, +}; +use futures_core::stream::Stream; +use mime::Mime; + +use crate::{ + error::Error, + field::InnerField, + payload::{PayloadBuffer, PayloadRef}, + safety::Safety, + Field, +}; + +const MAX_HEADERS: usize = 32; + +/// The server-side implementation of `multipart/form-data` requests. +/// +/// This will parse the incoming stream into `MultipartItem` instances via its `Stream` +/// implementation. `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` is +/// used for nested multipart streams. +pub struct Multipart { + flow: Flow, + safety: Safety, +} + +enum Flow { + InFlight(Inner), + + /// Error container is Some until an error is returned out of the flow. + Error(Option), +} + +impl Multipart { + /// Creates multipart instance from parts. + pub fn new(headers: &HeaderMap, stream: S) -> Self + where + S: Stream> + 'static, + { + match Self::find_ct_and_boundary(headers) { + Ok((ct, boundary)) => Self::from_ct_and_boundary(ct, boundary, stream), + Err(err) => Self::from_error(err), + } + } + + /// Creates multipart instance from parts. + pub(crate) fn from_req(req: &HttpRequest, payload: &mut dev::Payload) -> Self { + match Self::find_ct_and_boundary(req.headers()) { + Ok((ct, boundary)) => Self::from_ct_and_boundary(ct, boundary, payload.take()), + Err(err) => Self::from_error(err), + } + } + + /// Extract Content-Type and boundary info from headers. + pub(crate) fn find_ct_and_boundary(headers: &HeaderMap) -> Result<(Mime, String), Error> { + let content_type = headers + .get(&header::CONTENT_TYPE) + .ok_or(Error::ContentTypeMissing)? + .to_str() + .ok() + .and_then(|content_type| content_type.parse::().ok()) + .ok_or(Error::ContentTypeParse)?; + + if content_type.type_() != mime::MULTIPART { + return Err(Error::ContentTypeIncompatible); + } + + let boundary = content_type + .get_param(mime::BOUNDARY) + .ok_or(Error::BoundaryMissing)? + .as_str() + .to_owned(); + + Ok((content_type, boundary)) + } + + /// Constructs a new multipart reader from given Content-Type, boundary, and stream. + pub(crate) fn from_ct_and_boundary(ct: Mime, boundary: String, stream: S) -> Multipart + where + S: Stream> + 'static, + { + Multipart { + safety: Safety::new(), + flow: Flow::InFlight(Inner { + payload: PayloadRef::new(PayloadBuffer::new(stream)), + content_type: ct, + boundary, + state: State::FirstBoundary, + item: Item::None, + }), + } + } + + /// Constructs a new multipart reader from given `MultipartError`. + pub(crate) fn from_error(err: Error) -> Multipart { + Multipart { + flow: Flow::Error(Some(err)), + safety: Safety::new(), + } + } + + /// Return requests parsed Content-Type or raise the stored error. + pub(crate) fn content_type_or_bail(&mut self) -> Result { + match self.flow { + Flow::InFlight(ref inner) => Ok(inner.content_type.clone()), + Flow::Error(ref mut err) => Err(err + .take() + .expect("error should not be taken after it was returned")), + } + } +} + +impl Stream for Multipart { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + + match this.flow { + Flow::InFlight(ref mut inner) => { + if let Some(mut buffer) = inner.payload.get_mut(&this.safety) { + // check safety and poll read payload to buffer. + buffer.poll_stream(cx)?; + } else if !this.safety.is_clean() { + // safety violation + return Poll::Ready(Some(Err(Error::NotConsumed))); + } else { + return Poll::Pending; + } + + inner.poll(&this.safety, cx) + } + + Flow::Error(ref mut err) => Poll::Ready(Some(Err(err + .take() + .expect("Multipart polled after finish")))), + } + } +} + +#[derive(PartialEq, Debug)] +enum State { + /// Skip data until first boundary. + FirstBoundary, + + /// Reading boundary. + Boundary, + + /// Reading Headers. + Headers, + + /// Stream EOF. + Eof, +} + +enum Item { + None, + Field(Rc>), +} + +struct Inner { + /// Request's payload stream & buffer. + payload: PayloadRef, + + /// Request's Content-Type. + /// + /// Guaranteed to have "multipart" top-level media type, i.e., `multipart/*`. + content_type: Mime, + + /// Field boundary. + boundary: String, + + state: State, + item: Item, +} + +impl Inner { + fn read_field_headers(payload: &mut PayloadBuffer) -> Result, Error> { + match payload.read_until(b"\r\n\r\n")? { + None => { + if payload.eof { + Err(Error::Incomplete) + } else { + Ok(None) + } + } + + Some(bytes) => { + let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; + + match httparse::parse_headers(&bytes, &mut hdrs).map_err(ParseError::from)? { + httparse::Status::Complete((_, hdrs)) => { + // convert headers + let mut headers = HeaderMap::with_capacity(hdrs.len()); + + for h in hdrs { + let name = + HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?; + let value = + HeaderValue::try_from(h.value).map_err(|_| ParseError::Header)?; + headers.append(name, value); + } + + Ok(Some(headers)) + } + + httparse::Status::Partial => Err(ParseError::Header.into()), + } + } + } + } + + /// Reads a field boundary from the payload buffer (and discards it). + /// + /// Reads "in-between" and "final" boundaries. E.g. for boundary = "foo": + /// + /// ```plain + /// --foo <-- in-between fields + /// --foo-- <-- end of request body, should be followed by EOF + /// ``` + /// + /// Returns: + /// + /// - `Ok(Some(true))` - final field boundary read (EOF) + /// - `Ok(Some(false))` - field boundary read + /// - `Ok(None)` - boundary not found, more data needs reading + /// - `Err(BoundaryMissing)` - multipart boundary is missing + fn read_boundary(payload: &mut PayloadBuffer, boundary: &str) -> Result, Error> { + // TODO: need to read epilogue + let chunk = match payload.readline_or_eof()? { + // TODO: this might be okay as a let Some() else return Ok(None) + None => return Ok(payload.eof.then_some(true)), + Some(chunk) => chunk, + }; + + const BOUNDARY_MARKER: &[u8] = b"--"; + const LINE_BREAK: &[u8] = b"\r\n"; + + let boundary_len = boundary.len(); + + if chunk.len() < boundary_len + 2 + 2 + || !chunk.starts_with(BOUNDARY_MARKER) + || &chunk[2..boundary_len + 2] != boundary.as_bytes() + { + return Err(Error::BoundaryMissing); + } + + // chunk facts: + // - long enough to contain boundary + 2 markers or 1 marker and line-break + // - starts with boundary marker + // - chunk contains correct boundary + + if &chunk[boundary_len + 2..] == LINE_BREAK { + // boundary is followed by line-break, indicating more fields to come + return Ok(Some(false)); + } + + // boundary is followed by marker + if &chunk[boundary_len + 2..boundary_len + 4] == BOUNDARY_MARKER + && ( + // chunk is exactly boundary len + 2 markers + chunk.len() == boundary_len + 2 + 2 + // final boundary is allowed to end with a line-break + || &chunk[boundary_len + 4..] == LINE_BREAK + ) + { + return Ok(Some(true)); + } + + Err(Error::BoundaryMissing) + } + + fn skip_until_boundary( + payload: &mut PayloadBuffer, + boundary: &str, + ) -> Result, Error> { + let mut eof = false; + + loop { + match payload.readline()? { + Some(chunk) => { + if chunk.is_empty() { + return Err(Error::BoundaryMissing); + } + if chunk.len() < boundary.len() { + continue; + } + if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() { + break; + } else { + if chunk.len() < boundary.len() + 2 { + continue; + } + let b: &[u8] = boundary.as_ref(); + if &chunk[..boundary.len()] == b + && &chunk[boundary.len()..boundary.len() + 2] == b"--" + { + eof = true; + break; + } + } + } + None => { + return if payload.eof { + Err(Error::Incomplete) + } else { + Ok(None) + }; + } + } + } + Ok(Some(eof)) + } + + fn poll(&mut self, safety: &Safety, cx: &Context<'_>) -> Poll>> { + if self.state == State::Eof { + Poll::Ready(None) + } else { + // release field + loop { + // Nested multipart streams of fields has to be consumed + // before switching to next + if safety.current() { + let stop = match self.item { + Item::Field(ref mut field) => match field.borrow_mut().poll(safety) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Some(Ok(_))) => continue, + Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), + Poll::Ready(None) => true, + }, + Item::None => false, + }; + if stop { + self.item = Item::None; + } + if let Item::None = self.item { + break; + } + } + } + + let field_headers = if let Some(mut payload) = self.payload.get_mut(safety) { + match self.state { + // read until first boundary + State::FirstBoundary => { + match Inner::skip_until_boundary(&mut payload, &self.boundary)? { + None => return Poll::Pending, + Some(eof) => { + if eof { + self.state = State::Eof; + return Poll::Ready(None); + } else { + self.state = State::Headers; + } + } + } + } + + // read boundary + State::Boundary => match Inner::read_boundary(&mut payload, &self.boundary)? { + None => return Poll::Pending, + Some(eof) => { + if eof { + self.state = State::Eof; + return Poll::Ready(None); + } else { + self.state = State::Headers; + } + } + }, + + _ => {} + } + + // read field headers for next field + if self.state == State::Headers { + if let Some(headers) = Inner::read_field_headers(&mut payload)? { + self.state = State::Boundary; + headers + } else { + return Poll::Pending; + } + } else { + unreachable!() + } + } else { + log::debug!("NotReady: field is in flight"); + return Poll::Pending; + }; + + let field_content_disposition = field_headers + .get(&header::CONTENT_DISPOSITION) + .and_then(|cd| ContentDisposition::from_raw(cd).ok()) + .filter(|content_disposition| { + matches!( + content_disposition.disposition, + header::DispositionType::FormData, + ) + }); + + let form_field_name = if self.content_type.subtype() == mime::FORM_DATA { + // According to RFC 7578 §4.2, which relates to "multipart/form-data" requests + // specifically, fields must have a Content-Disposition header, its disposition + // type must be set as "form-data", and it must have a name parameter. + + let Some(cd) = &field_content_disposition else { + return Poll::Ready(Some(Err(Error::ContentDispositionMissing))); + }; + + let Some(field_name) = cd.get_name() else { + return Poll::Ready(Some(Err(Error::ContentDispositionNameMissing))); + }; + + Some(field_name.to_owned()) + } else { + None + }; + + // TODO: check out other multipart/* RFCs for specific requirements + + let field_content_type: Option = field_headers + .get(&header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .and_then(|ct| ct.parse().ok()); + + self.state = State::Boundary; + + // nested multipart stream is not supported + if let Some(mime) = &field_content_type { + if mime.type_() == mime::MULTIPART { + return Poll::Ready(Some(Err(Error::Nested))); + } + } + + let field_inner = + InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &field_headers)?; + + self.item = Item::Field(Rc::clone(&field_inner)); + + Poll::Ready(Some(Ok(Field::new( + field_content_type, + field_content_disposition, + form_field_name, + field_headers, + safety.clone(cx), + field_inner, + )))) + } + } +} + +impl Drop for Inner { + fn drop(&mut self) { + // InnerMultipartItem::Field has to be dropped first because of Safety. + self.item = Item::None; + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use actix_http::h1; + use actix_web::{ + http::header::{DispositionParam, DispositionType}, + rt, + test::TestRequest, + web::{BufMut as _, BytesMut}, + FromRequest, + }; + use assert_matches::assert_matches; + use futures_test::stream::StreamTestExt as _; + use futures_util::{stream, StreamExt as _}; + use tokio::sync::mpsc; + use tokio_stream::wrappers::UnboundedReceiverStream; + + use super::*; + + const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0"; + + #[actix_rt::test] + async fn test_boundary() { + let headers = HeaderMap::new(); + match Multipart::find_ct_and_boundary(&headers) { + Err(Error::ContentTypeMissing) => {} + _ => unreachable!("should not happen"), + } + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("test"), + ); + + match Multipart::find_ct_and_boundary(&headers) { + Err(Error::ContentTypeParse) => {} + _ => unreachable!("should not happen"), + } + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("multipart/mixed"), + ); + match Multipart::find_ct_and_boundary(&headers) { + Err(Error::BoundaryMissing) => {} + _ => unreachable!("should not happen"), + } + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"", + ), + ); + + assert_eq!( + Multipart::find_ct_and_boundary(&headers).unwrap().1, + "5c02368e880e436dab70ed54e1c58209", + ); + } + + fn create_stream() -> ( + mpsc::UnboundedSender>, + impl Stream>, + ) { + let (tx, rx) = mpsc::unbounded_channel(); + + ( + tx, + UnboundedReceiverStream::new(rx).map(|res| res.map_err(|_| panic!())), + ) + } + + fn create_simple_request_with_header() -> (Bytes, HeaderMap) { + let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary( + BOUNDARY, + "file", + Some("fn.txt".to_owned()), + Some(mime::TEXT_PLAIN_UTF_8), + Bytes::from_static(b"data"), + ); + + let mut buf = BytesMut::with_capacity(body.len() + 14); + + // add junk before form to test pre-boundary data rejection + buf.put("testasdadsad\r\n".as_bytes()); + + buf.put(body); + + (buf.freeze(), headers) + } + + // TODO: use test utility when multi-file support is introduced + fn create_double_request_with_header() -> (Bytes, HeaderMap) { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + data\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + (bytes, headers) + } + + #[actix_rt::test] + async fn test_multipart_no_end_crlf() { + let (sender, payload) = create_stream(); + let (mut bytes, headers) = create_double_request_with_header(); + let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf + + sender.send(Ok(bytes_stripped)).unwrap(); + drop(sender); // eof + + let mut multipart = Multipart::new(&headers, payload); + + match multipart.next().await.unwrap() { + Ok(_) => {} + _ => unreachable!(), + } + + match multipart.next().await.unwrap() { + Ok(_) => {} + _ => unreachable!(), + } + + match multipart.next().await { + None => {} + _ => unreachable!(), + } + } + + #[actix_rt::test] + async fn test_multipart() { + let (sender, payload) = create_stream(); + let (bytes, headers) = create_double_request_with_header(); + + sender.send(Ok(bytes)).unwrap(); + + let mut multipart = Multipart::new(&headers, payload); + match multipart.next().await { + Some(Ok(mut field)) => { + let cd = field.content_disposition().unwrap(); + assert_eq!(cd.disposition, DispositionType::FormData); + assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); + + assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); + assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); + + match field.next().await.unwrap() { + Ok(chunk) => assert_eq!(chunk, "test"), + _ => unreachable!(), + } + match field.next().await { + None => {} + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match multipart.next().await.unwrap() { + Ok(mut field) => { + assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); + assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); + + match field.next().await { + Some(Ok(chunk)) => assert_eq!(chunk, "data"), + _ => unreachable!(), + } + match field.next().await { + None => {} + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match multipart.next().await { + None => {} + _ => unreachable!(), + } + } + + // Loops, collecting all bytes until end-of-field + async fn get_whole_field(field: &mut Field) -> BytesMut { + let mut b = BytesMut::new(); + loop { + match field.next().await { + Some(Ok(chunk)) => b.extend_from_slice(&chunk), + None => return b, + _ => unreachable!(), + } + } + } + + #[actix_rt::test] + async fn test_stream() { + let (bytes, headers) = create_double_request_with_header(); + let payload = stream::iter(bytes) + .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) + .interleave_pending(); + + let mut multipart = Multipart::new(&headers, payload); + match multipart.next().await.unwrap() { + Ok(mut field) => { + let cd = field.content_disposition().unwrap(); + assert_eq!(cd.disposition, DispositionType::FormData); + assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); + + assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); + assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); + + assert_eq!(get_whole_field(&mut field).await, "test"); + } + _ => unreachable!(), + } + + match multipart.next().await { + Some(Ok(mut field)) => { + assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); + assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); + + assert_eq!(get_whole_field(&mut field).await, "data"); + } + _ => unreachable!(), + } + + match multipart.next().await { + None => {} + _ => unreachable!(), + } + } + + #[actix_rt::test] + async fn test_multipart_from_error() { + let err = Error::ContentTypeMissing; + let mut multipart = Multipart::from_error(err); + assert!(multipart.next().await.unwrap().is_err()) + } + + #[actix_rt::test] + async fn test_multipart_from_boundary() { + let (_, payload) = create_stream(); + let (_, headers) = create_simple_request_with_header(); + let (ct, boundary) = Multipart::find_ct_and_boundary(&headers).unwrap(); + let _ = Multipart::from_ct_and_boundary(ct, boundary, payload); + } + + #[actix_rt::test] + async fn test_multipart_payload_consumption() { + // with sample payload and HttpRequest with no headers + let (_, inner_payload) = h1::Payload::create(false); + let mut payload = actix_web::dev::Payload::from(inner_payload); + let req = TestRequest::default().to_http_request(); + + // multipart should generate an error + let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap(); + assert!(mp.next().await.unwrap().is_err()); + + // and should not consume the payload + match payload { + actix_web::dev::Payload::H1 { .. } => {} //expected + _ => unreachable!(), + } + } + + #[actix_rt::test] + async fn no_content_disposition_form_data() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\ + Content-Length: 4\r\n\ + \r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = stream::iter(bytes) + .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) + .interleave_pending(); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert_matches!( + res.expect_err( + "according to RFC 7578, form-data fields require a content-disposition header" + ), + Error::ContentDispositionMissing + ); + } + + #[actix_rt::test] + async fn no_content_disposition_non_form_data() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\ + Content-Length: 4\r\n\ + \r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = stream::iter(bytes) + .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) + .interleave_pending(); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + res.unwrap(); + } + + #[actix_rt::test] + async fn no_name_in_form_data_content_disposition() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\ + Content-Length: 4\r\n\ + \r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = stream::iter(bytes) + .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) + .interleave_pending(); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert_matches!( + res.expect_err("according to RFC 7578, form-data fields require a name attribute"), + Error::ContentDispositionNameMissing + ); + } + + #[actix_rt::test] + async fn test_drop_multipart_dont_hang() { + let (sender, payload) = create_stream(); + let (bytes, headers) = create_simple_request_with_header(); + sender.send(Ok(bytes)).unwrap(); + drop(sender); // eof + + let mut multipart = Multipart::new(&headers, payload); + let mut field = multipart.next().await.unwrap().unwrap(); + + drop(multipart); + + // should fail immediately + match field.next().await { + Some(Err(Error::NotConsumed)) => {} + _ => panic!(), + }; + } + + #[actix_rt::test] + async fn test_drop_field_awaken_multipart() { + let (sender, payload) = create_stream(); + let (bytes, headers) = create_double_request_with_header(); + sender.send(Ok(bytes)).unwrap(); + drop(sender); // eof + + let mut multipart = Multipart::new(&headers, payload); + let mut field = multipart.next().await.unwrap().unwrap(); + + let task = rt::spawn(async move { + rt::time::sleep(Duration::from_millis(500)).await; + assert_eq!(field.next().await.unwrap().unwrap(), "test"); + drop(field); + }); + + // dropping field should awaken current task + let _ = multipart.next().await.unwrap().unwrap(); + task.await.unwrap(); + } +} diff --git a/actix-multipart/src/payload.rs b/actix-multipart/src/payload.rs new file mode 100644 index 000000000..858634bc0 --- /dev/null +++ b/actix-multipart/src/payload.rs @@ -0,0 +1,255 @@ +use std::{ + cell::{RefCell, RefMut}, + cmp, mem, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use actix_web::{ + error::PayloadError, + web::{Bytes, BytesMut}, +}; +use futures_core::stream::{LocalBoxStream, Stream}; + +use crate::{error::Error, safety::Safety}; + +pub(crate) struct PayloadRef { + payload: Rc>, +} + +impl PayloadRef { + pub(crate) fn new(payload: PayloadBuffer) -> PayloadRef { + PayloadRef { + payload: Rc::new(RefCell::new(payload)), + } + } + + pub(crate) fn get_mut(&self, safety: &Safety) -> Option> { + if safety.current() { + Some(self.payload.borrow_mut()) + } else { + None + } + } +} + +impl Clone for PayloadRef { + fn clone(&self) -> PayloadRef { + PayloadRef { + payload: Rc::clone(&self.payload), + } + } +} + +/// Payload buffer. +pub(crate) struct PayloadBuffer { + pub(crate) stream: LocalBoxStream<'static, Result>, + pub(crate) buf: BytesMut, + /// EOF flag. If true, no more payload reads will be attempted. + pub(crate) eof: bool, +} + +impl PayloadBuffer { + /// Constructs new payload buffer. + pub(crate) fn new(stream: S) -> Self + where + S: Stream> + 'static, + { + PayloadBuffer { + stream: Box::pin(stream), + buf: BytesMut::with_capacity(1_024), // pre-allocate 1KiB + eof: false, + } + } + + pub(crate) fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { + loop { + match Pin::new(&mut self.stream).poll_next(cx) { + Poll::Ready(Some(Ok(data))) => { + self.buf.extend_from_slice(&data); + // try to read more data + continue; + } + Poll::Ready(Some(Err(err))) => return Err(err), + Poll::Ready(None) => { + self.eof = true; + return Ok(()); + } + Poll::Pending => return Ok(()), + } + } + } + + /// Reads exact number of bytes. + #[cfg(test)] + pub(crate) fn read_exact(&mut self, size: usize) -> Option { + if size <= self.buf.len() { + Some(self.buf.split_to(size).freeze()) + } else { + None + } + } + + pub(crate) fn read_max(&mut self, size: u64) -> Result, Error> { + if !self.buf.is_empty() { + let size = cmp::min(self.buf.len() as u64, size) as usize; + Ok(Some(self.buf.split_to(size).freeze())) + } else if self.eof { + Err(Error::Incomplete) + } else { + Ok(None) + } + } + + /// Reads until specified ending. + /// + /// Returns: + /// + /// - `Ok(Some(chunk))` - `needle` is found, with chunk ending after needle + /// - `Err(Incomplete)` - `needle` is not found and we're at EOF + /// - `Ok(None)` - `needle` is not found otherwise + pub(crate) fn read_until(&mut self, needle: &[u8]) -> Result, Error> { + match memchr::memmem::find(&self.buf, needle) { + // buffer exhausted and EOF without finding needle + None if self.eof => Err(Error::Incomplete), + + // needle not yet found + None => Ok(None), + + // needle found, split chunk out of buf + Some(idx) => Ok(Some(self.buf.split_to(idx + needle.len()).freeze())), + } + } + + /// Reads bytes until new line delimiter (`\n`, `0x0A`). + /// + /// Returns: + /// + /// - `Ok(Some(chunk))` - `needle` is found, with chunk ending after needle + /// - `Err(Incomplete)` - `needle` is not found and we're at EOF + /// - `Ok(None)` - `needle` is not found otherwise + #[inline] + pub(crate) fn readline(&mut self) -> Result, Error> { + self.read_until(b"\n") + } + + /// Reads bytes until new line delimiter or until EOF. + #[inline] + pub(crate) fn readline_or_eof(&mut self) -> Result, Error> { + match self.readline() { + Err(Error::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), + line => line, + } + } + + /// Puts unprocessed data back to the buffer. + pub(crate) fn unprocessed(&mut self, data: Bytes) { + // TODO: use BytesMut::from when it's released, see https://github.com/tokio-rs/bytes/pull/710 + let buf = BytesMut::from(&data[..]); + let buf = mem::replace(&mut self.buf, buf); + self.buf.extend_from_slice(&buf); + } +} + +#[cfg(test)] +mod tests { + use actix_http::h1; + use futures_util::future::lazy; + + use super::*; + + #[actix_rt::test] + async fn basic() { + let (_, payload) = h1::Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(payload.buf.len(), 0); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + assert_eq!(None, payload.read_max(1).unwrap()); + } + + #[actix_rt::test] + async fn eof() { + let (mut sender, payload) = h1::Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(None, payload.read_max(4).unwrap()); + sender.feed_data(Bytes::from("data")); + sender.feed_eof(); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + + assert_eq!(Some(Bytes::from("data")), payload.read_max(4).unwrap()); + assert_eq!(payload.buf.len(), 0); + assert!(payload.read_max(1).is_err()); + assert!(payload.eof); + } + + #[actix_rt::test] + async fn err() { + let (mut sender, payload) = h1::Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + assert_eq!(None, payload.read_max(1).unwrap()); + sender.set_error(PayloadError::Incomplete(None)); + lazy(|cx| payload.poll_stream(cx)).await.err().unwrap(); + } + + #[actix_rt::test] + async fn read_max() { + let (mut sender, payload) = h1::Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + sender.feed_data(Bytes::from("line1")); + sender.feed_data(Bytes::from("line2")); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + assert_eq!(payload.buf.len(), 10); + + assert_eq!(Some(Bytes::from("line1")), payload.read_max(5).unwrap()); + assert_eq!(payload.buf.len(), 5); + + assert_eq!(Some(Bytes::from("line2")), payload.read_max(5).unwrap()); + assert_eq!(payload.buf.len(), 0); + } + + #[actix_rt::test] + async fn read_exactly() { + let (mut sender, payload) = h1::Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(None, payload.read_exact(2)); + + sender.feed_data(Bytes::from("line1")); + sender.feed_data(Bytes::from("line2")); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + + assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2)); + assert_eq!(payload.buf.len(), 8); + + assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4)); + assert_eq!(payload.buf.len(), 4); + } + + #[actix_rt::test] + async fn read_until() { + let (mut sender, payload) = h1::Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(None, payload.read_until(b"ne").unwrap()); + + sender.feed_data(Bytes::from("line1")); + sender.feed_data(Bytes::from("line2")); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + + assert_eq!( + Some(Bytes::from("line")), + payload.read_until(b"ne").unwrap() + ); + assert_eq!(payload.buf.len(), 6); + + assert_eq!( + Some(Bytes::from("1line2")), + payload.read_until(b"2").unwrap() + ); + assert_eq!(payload.buf.len(), 0); + } +} diff --git a/actix-multipart/src/safety.rs b/actix-multipart/src/safety.rs new file mode 100644 index 000000000..db6b3b18b --- /dev/null +++ b/actix-multipart/src/safety.rs @@ -0,0 +1,60 @@ +use std::{cell::Cell, marker::PhantomData, rc::Rc, task}; + +use local_waker::LocalWaker; + +/// Counter. It tracks of number of clones of payloads and give access to payload only to top most. +/// +/// - When dropped, parent task is awakened. This is to support the case where `Field` is dropped in +/// a separate task than `Multipart`. +/// - Assumes that parent owners don't move to different tasks; only the top-most is allowed to. +/// - If dropped and is not top most owner, is_clean flag is set to false. +#[derive(Debug)] +pub(crate) struct Safety { + task: LocalWaker, + level: usize, + payload: Rc>, + clean: Rc>, +} + +impl Safety { + pub(crate) fn new() -> Safety { + let payload = Rc::new(PhantomData); + Safety { + task: LocalWaker::new(), + level: Rc::strong_count(&payload), + clean: Rc::new(Cell::new(true)), + payload, + } + } + + pub(crate) fn current(&self) -> bool { + Rc::strong_count(&self.payload) == self.level && self.clean.get() + } + + pub(crate) fn is_clean(&self) -> bool { + self.clean.get() + } + + pub(crate) fn clone(&self, cx: &task::Context<'_>) -> Safety { + let payload = Rc::clone(&self.payload); + let s = Safety { + task: LocalWaker::new(), + level: Rc::strong_count(&payload), + clean: self.clean.clone(), + payload, + }; + s.task.register(cx.waker()); + s + } +} + +impl Drop for Safety { + fn drop(&mut self) { + if Rc::strong_count(&self.payload) != self.level { + // Multipart dropped leaving a Field + self.clean.set(false); + } + + self.task.wake(); + } +} diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs deleted file mode 100644 index 0256aa7bf..000000000 --- a/actix-multipart/src/server.rs +++ /dev/null @@ -1,1374 +0,0 @@ -//! Multipart response payload support. - -use std::{ - cell::{Cell, RefCell, RefMut}, - cmp, fmt, - marker::PhantomData, - pin::Pin, - rc::Rc, - task::{Context, Poll}, -}; - -use actix_web::{ - error::{ParseError, PayloadError}, - http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}, -}; -use bytes::{Bytes, BytesMut}; -use futures_core::stream::{LocalBoxStream, Stream}; -use local_waker::LocalWaker; - -use crate::error::MultipartError; - -const MAX_HEADERS: usize = 32; - -/// The server-side implementation of `multipart/form-data` requests. -/// -/// This will parse the incoming stream into `MultipartItem` instances via its -/// Stream implementation. -/// `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` -/// is used for nested multipart streams. -pub struct Multipart { - safety: Safety, - error: Option, - inner: Option, -} - -enum InnerMultipartItem { - None, - Field(Rc>), -} - -#[derive(PartialEq, Debug)] -enum InnerState { - /// Stream eof - Eof, - - /// Skip data until first boundary - FirstBoundary, - - /// Reading boundary - Boundary, - - /// Reading Headers, - Headers, -} - -struct InnerMultipart { - payload: PayloadRef, - boundary: String, - state: InnerState, - item: InnerMultipartItem, -} - -impl Multipart { - /// Create multipart instance for boundary. - pub fn new(headers: &HeaderMap, stream: S) -> Multipart - where - S: Stream> + 'static, - { - match Self::boundary(headers) { - Ok(boundary) => Multipart::from_boundary(boundary, stream), - Err(err) => Multipart::from_error(err), - } - } - - /// Extract boundary info from headers. - pub(crate) fn boundary(headers: &HeaderMap) -> Result { - headers - .get(&header::CONTENT_TYPE) - .ok_or(MultipartError::NoContentType)? - .to_str() - .ok() - .and_then(|content_type| content_type.parse::().ok()) - .ok_or(MultipartError::ParseContentType)? - .get_param(mime::BOUNDARY) - .map(|boundary| boundary.as_str().to_owned()) - .ok_or(MultipartError::Boundary) - } - - /// Create multipart instance for given boundary and stream - pub(crate) fn from_boundary(boundary: String, stream: S) -> Multipart - where - S: Stream> + 'static, - { - Multipart { - error: None, - safety: Safety::new(), - inner: Some(InnerMultipart { - boundary, - payload: PayloadRef::new(PayloadBuffer::new(stream)), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - }), - } - } - - /// Create Multipart instance from MultipartError - pub(crate) fn from_error(err: MultipartError) -> Multipart { - Multipart { - error: Some(err), - safety: Safety::new(), - inner: None, - } - } -} - -impl Stream for Multipart { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - - match this.inner.as_mut() { - Some(inner) => { - if let Some(mut buffer) = inner.payload.get_mut(&this.safety) { - // check safety and poll read payload to buffer. - buffer.poll_stream(cx)?; - } else if !this.safety.is_clean() { - // safety violation - return Poll::Ready(Some(Err(MultipartError::NotConsumed))); - } else { - return Poll::Pending; - } - - inner.poll(&this.safety, cx) - } - None => Poll::Ready(Some(Err(this - .error - .take() - .expect("Multipart polled after finish")))), - } - } -} - -impl InnerMultipart { - fn read_headers(payload: &mut PayloadBuffer) -> Result, MultipartError> { - match payload.read_until(b"\r\n\r\n")? { - None => { - if payload.eof { - Err(MultipartError::Incomplete) - } else { - Ok(None) - } - } - Some(bytes) => { - let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; - match httparse::parse_headers(&bytes, &mut hdrs) { - Ok(httparse::Status::Complete((_, hdrs))) => { - // convert headers - let mut headers = HeaderMap::with_capacity(hdrs.len()); - - for h in hdrs { - let name = - HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?; - let value = - HeaderValue::try_from(h.value).map_err(|_| ParseError::Header)?; - headers.append(name, value); - } - - Ok(Some(headers)) - } - Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), - Err(err) => Err(ParseError::from(err).into()), - } - } - } - } - - fn read_boundary( - payload: &mut PayloadBuffer, - boundary: &str, - ) -> Result, MultipartError> { - // TODO: need to read epilogue - match payload.readline_or_eof()? { - None => { - if payload.eof { - Ok(Some(true)) - } else { - Ok(None) - } - } - Some(chunk) => { - if chunk.len() < boundary.len() + 4 - || &chunk[..2] != b"--" - || &chunk[2..boundary.len() + 2] != boundary.as_bytes() - { - Err(MultipartError::Boundary) - } else if &chunk[boundary.len() + 2..] == b"\r\n" { - Ok(Some(false)) - } else if &chunk[boundary.len() + 2..boundary.len() + 4] == b"--" - && (chunk.len() == boundary.len() + 4 - || &chunk[boundary.len() + 4..] == b"\r\n") - { - Ok(Some(true)) - } else { - Err(MultipartError::Boundary) - } - } - } - } - - fn skip_until_boundary( - payload: &mut PayloadBuffer, - boundary: &str, - ) -> Result, MultipartError> { - let mut eof = false; - loop { - match payload.readline()? { - Some(chunk) => { - if chunk.is_empty() { - return Err(MultipartError::Boundary); - } - if chunk.len() < boundary.len() { - continue; - } - if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() { - break; - } else { - if chunk.len() < boundary.len() + 2 { - continue; - } - let b: &[u8] = boundary.as_ref(); - if &chunk[..boundary.len()] == b - && &chunk[boundary.len()..boundary.len() + 2] == b"--" - { - eof = true; - break; - } - } - } - None => { - return if payload.eof { - Err(MultipartError::Incomplete) - } else { - Ok(None) - }; - } - } - } - Ok(Some(eof)) - } - - fn poll( - &mut self, - safety: &Safety, - cx: &Context<'_>, - ) -> Poll>> { - if self.state == InnerState::Eof { - Poll::Ready(None) - } else { - // release field - loop { - // Nested multipart streams of fields has to be consumed - // before switching to next - if safety.current() { - let stop = match self.item { - InnerMultipartItem::Field(ref mut field) => { - match field.borrow_mut().poll(safety) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Some(Ok(_))) => continue, - Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), - Poll::Ready(None) => true, - } - } - InnerMultipartItem::None => false, - }; - if stop { - self.item = InnerMultipartItem::None; - } - if let InnerMultipartItem::None = self.item { - break; - } - } - } - - let headers = if let Some(mut payload) = self.payload.get_mut(safety) { - match self.state { - // read until first boundary - InnerState::FirstBoundary => { - match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)? { - Some(eof) => { - if eof { - self.state = InnerState::Eof; - return Poll::Ready(None); - } else { - self.state = InnerState::Headers; - } - } - None => return Poll::Pending, - } - } - // read boundary - InnerState::Boundary => { - match InnerMultipart::read_boundary(&mut payload, &self.boundary)? { - None => return Poll::Pending, - Some(eof) => { - if eof { - self.state = InnerState::Eof; - return Poll::Ready(None); - } else { - self.state = InnerState::Headers; - } - } - } - } - _ => {} - } - - // read field headers for next field - if self.state == InnerState::Headers { - if let Some(headers) = InnerMultipart::read_headers(&mut payload)? { - self.state = InnerState::Boundary; - headers - } else { - return Poll::Pending; - } - } else { - unreachable!() - } - } else { - log::debug!("NotReady: field is in flight"); - return Poll::Pending; - }; - - // According to RFC 7578 §4.2, a Content-Disposition header must always be present and - // set to "form-data". - - let content_disposition = headers - .get(&header::CONTENT_DISPOSITION) - .and_then(|cd| ContentDisposition::from_raw(cd).ok()) - .filter(|content_disposition| { - let is_form_data = - content_disposition.disposition == header::DispositionType::FormData; - - let has_field_name = content_disposition - .parameters - .iter() - .any(|param| matches!(param, header::DispositionParam::Name(_))); - - is_form_data && has_field_name - }); - - let cd = if let Some(content_disposition) = content_disposition { - content_disposition - } else { - return Poll::Ready(Some(Err(MultipartError::NoContentDisposition))); - }; - - let ct: Option = headers - .get(&header::CONTENT_TYPE) - .and_then(|ct| ct.to_str().ok()) - .and_then(|ct| ct.parse().ok()); - - self.state = InnerState::Boundary; - - // nested multipart stream is not supported - if let Some(mime) = &ct { - if mime.type_() == mime::MULTIPART { - return Poll::Ready(Some(Err(MultipartError::Nested))); - } - } - - let field = - InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?; - - self.item = InnerMultipartItem::Field(Rc::clone(&field)); - - Poll::Ready(Some(Ok(Field::new( - safety.clone(cx), - headers, - ct, - cd, - field, - )))) - } - } -} - -impl Drop for InnerMultipart { - fn drop(&mut self) { - // InnerMultipartItem::Field has to be dropped first because of Safety. - self.item = InnerMultipartItem::None; - } -} - -/// A single field in a multipart stream -pub struct Field { - ct: Option, - cd: ContentDisposition, - headers: HeaderMap, - inner: Rc>, - safety: Safety, -} - -impl Field { - fn new( - safety: Safety, - headers: HeaderMap, - ct: Option, - cd: ContentDisposition, - inner: Rc>, - ) -> Self { - Field { - ct, - cd, - headers, - inner, - safety, - } - } - - /// Returns a reference to the field's header map. - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Returns a reference to the field's content (mime) type, if it is supplied by the client. - /// - /// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not - /// present, it should default to "text/plain". Note it is the responsibility of the client to - /// provide the appropriate content type, there is no attempt to validate this by the server. - pub fn content_type(&self) -> Option<&mime::Mime> { - self.ct.as_ref() - } - - /// Returns the field's Content-Disposition. - /// - /// Per [RFC 7578 §4.2]: "Each part MUST contain a Content-Disposition header field where the - /// disposition type is `form-data`. The Content-Disposition header field MUST also contain an - /// additional parameter of `name`; the value of the `name` parameter is the original field name - /// from the form." - /// - /// This crate validates that it exists before returning a `Field`. As such, it is safe to - /// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as - /// a convenience. - /// - /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 - pub fn content_disposition(&self) -> &ContentDisposition { - &self.cd - } - - /// Returns the field's name. - /// - /// See [content_disposition](Self::content_disposition) regarding guarantees about existence of - /// the name field. - pub fn name(&self) -> &str { - self.content_disposition() - .get_name() - .expect("field name should be guaranteed to exist in multipart form-data") - } -} - -impl Stream for Field { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - let mut inner = this.inner.borrow_mut(); - if let Some(mut buffer) = inner - .payload - .as_ref() - .expect("Field should not be polled after completion") - .get_mut(&this.safety) - { - // check safety and poll read payload to buffer. - buffer.poll_stream(cx)?; - } else if !this.safety.is_clean() { - // safety violation - return Poll::Ready(Some(Err(MultipartError::NotConsumed))); - } else { - return Poll::Pending; - } - - inner.poll(&this.safety) - } -} - -impl fmt::Debug for Field { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(ct) = &self.ct { - writeln!(f, "\nField: {}", ct)?; - } else { - writeln!(f, "\nField:")?; - } - writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -struct InnerField { - /// Payload is initialized as Some and is `take`n when the field stream finishes. - payload: Option, - boundary: String, - eof: bool, - length: Option, -} - -impl InnerField { - fn new_in_rc( - payload: PayloadRef, - boundary: String, - headers: &HeaderMap, - ) -> Result>, PayloadError> { - Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this))) - } - - fn new( - payload: PayloadRef, - boundary: String, - headers: &HeaderMap, - ) -> Result { - let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { - match len.to_str().ok().and_then(|len| len.parse::().ok()) { - Some(len) => Some(len), - None => return Err(PayloadError::Incomplete(None)), - } - } else { - None - }; - - Ok(InnerField { - boundary, - payload: Some(payload), - eof: false, - length: len, - }) - } - - /// Reads body part content chunk of the specified size. - /// The body part must has `Content-Length` header with proper value. - fn read_len( - payload: &mut PayloadBuffer, - size: &mut u64, - ) -> Poll>> { - if *size == 0 { - Poll::Ready(None) - } else { - match payload.read_max(*size)? { - Some(mut chunk) => { - let len = cmp::min(chunk.len() as u64, *size); - *size -= len; - let ch = chunk.split_to(len as usize); - if !chunk.is_empty() { - payload.unprocessed(chunk); - } - Poll::Ready(Some(Ok(ch))) - } - None => { - if payload.eof && (*size != 0) { - Poll::Ready(Some(Err(MultipartError::Incomplete))) - } else { - Poll::Pending - } - } - } - } - } - - /// Reads content chunk of body part with unknown length. - /// The `Content-Length` header for body part is not necessary. - fn read_stream( - payload: &mut PayloadBuffer, - boundary: &str, - ) -> Poll>> { - let mut pos = 0; - - let len = payload.buf.len(); - if len == 0 { - return if payload.eof { - Poll::Ready(Some(Err(MultipartError::Incomplete))) - } else { - Poll::Pending - }; - } - - // check boundary - if len > 4 && payload.buf[0] == b'\r' { - let b_len = if &payload.buf[..2] == b"\r\n" && &payload.buf[2..4] == b"--" { - Some(4) - } else if &payload.buf[1..3] == b"--" { - Some(3) - } else { - None - }; - - if let Some(b_len) = b_len { - let b_size = boundary.len() + b_len; - if len < b_size { - return Poll::Pending; - } else if &payload.buf[b_len..b_size] == boundary.as_bytes() { - // found boundary - return Poll::Ready(None); - } - } - } - - loop { - return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") { - let cur = pos + idx; - - // check if we have enough data for boundary detection - if cur + 4 > len { - if cur > 0 { - Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze()))) - } else { - Poll::Pending - } - } else { - // check boundary - if (&payload.buf[cur..cur + 2] == b"\r\n" - && &payload.buf[cur + 2..cur + 4] == b"--") - || (&payload.buf[cur..=cur] == b"\r" - && &payload.buf[cur + 1..cur + 3] == b"--") - { - if cur != 0 { - // return buffer - Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze()))) - } else { - pos = cur + 1; - continue; - } - } else { - // not boundary - pos = cur + 1; - continue; - } - } - } else { - Poll::Ready(Some(Ok(payload.buf.split().freeze()))) - }; - } - } - - fn poll(&mut self, s: &Safety) -> Poll>> { - if self.payload.is_none() { - return Poll::Ready(None); - } - - let result = if let Some(mut payload) = self - .payload - .as_ref() - .expect("Field should not be polled after completion") - .get_mut(s) - { - if !self.eof { - let res = if let Some(ref mut len) = self.length { - InnerField::read_len(&mut payload, len) - } else { - InnerField::read_stream(&mut payload, &self.boundary) - }; - - match res { - Poll::Pending => return Poll::Pending, - Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))), - Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), - Poll::Ready(None) => self.eof = true, - } - } - - match payload.readline() { - Ok(None) => Poll::Pending, - Ok(Some(line)) => { - if line.as_ref() != b"\r\n" { - log::warn!("multipart field did not read all the data or it is malformed"); - } - Poll::Ready(None) - } - Err(err) => Poll::Ready(Some(Err(err))), - } - } else { - Poll::Pending - }; - - if let Poll::Ready(None) = result { - // drop payload buffer and make future un-poll-able - let _ = self.payload.take(); - } - - result - } -} - -struct PayloadRef { - payload: Rc>, -} - -impl PayloadRef { - fn new(payload: PayloadBuffer) -> PayloadRef { - PayloadRef { - payload: Rc::new(payload.into()), - } - } - - fn get_mut(&self, s: &Safety) -> Option> { - if s.current() { - Some(self.payload.borrow_mut()) - } else { - None - } - } -} - -impl Clone for PayloadRef { - fn clone(&self) -> PayloadRef { - PayloadRef { - payload: Rc::clone(&self.payload), - } - } -} - -/// Counter. It tracks of number of clones of payloads and give access to payload only to top most. -/// * When dropped, parent task is awakened. This is to support the case where Field is -/// dropped in a separate task than Multipart. -/// * Assumes that parent owners don't move to different tasks; only the top-most is allowed to. -/// * If dropped and is not top most owner, is_clean flag is set to false. -#[derive(Debug)] -struct Safety { - task: LocalWaker, - level: usize, - payload: Rc>, - clean: Rc>, -} - -impl Safety { - fn new() -> Safety { - let payload = Rc::new(PhantomData); - Safety { - task: LocalWaker::new(), - level: Rc::strong_count(&payload), - clean: Rc::new(Cell::new(true)), - payload, - } - } - - fn current(&self) -> bool { - Rc::strong_count(&self.payload) == self.level && self.clean.get() - } - - fn is_clean(&self) -> bool { - self.clean.get() - } - - fn clone(&self, cx: &Context<'_>) -> Safety { - let payload = Rc::clone(&self.payload); - let s = Safety { - task: LocalWaker::new(), - level: Rc::strong_count(&payload), - clean: self.clean.clone(), - payload, - }; - s.task.register(cx.waker()); - s - } -} - -impl Drop for Safety { - fn drop(&mut self) { - if Rc::strong_count(&self.payload) != self.level { - // Multipart dropped leaving a Field - self.clean.set(false); - } - - self.task.wake(); - } -} - -/// Payload buffer. -struct PayloadBuffer { - eof: bool, - buf: BytesMut, - stream: LocalBoxStream<'static, Result>, -} - -impl PayloadBuffer { - /// Constructs new `PayloadBuffer` instance. - fn new(stream: S) -> Self - where - S: Stream> + 'static, - { - PayloadBuffer { - eof: false, - buf: BytesMut::new(), - stream: Box::pin(stream), - } - } - - fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { - loop { - match Pin::new(&mut self.stream).poll_next(cx) { - Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data), - Poll::Ready(Some(Err(err))) => return Err(err), - Poll::Ready(None) => { - self.eof = true; - return Ok(()); - } - Poll::Pending => return Ok(()), - } - } - } - - /// Read exact number of bytes - #[cfg(test)] - fn read_exact(&mut self, size: usize) -> Option { - if size <= self.buf.len() { - Some(self.buf.split_to(size).freeze()) - } else { - None - } - } - - fn read_max(&mut self, size: u64) -> Result, MultipartError> { - if !self.buf.is_empty() { - let size = std::cmp::min(self.buf.len() as u64, size) as usize; - Ok(Some(self.buf.split_to(size).freeze())) - } else if self.eof { - Err(MultipartError::Incomplete) - } else { - Ok(None) - } - } - - /// Read until specified ending - fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { - let res = memchr::memmem::find(&self.buf, line) - .map(|idx| self.buf.split_to(idx + line.len()).freeze()); - - if res.is_none() && self.eof { - Err(MultipartError::Incomplete) - } else { - Ok(res) - } - } - - /// Read bytes until new line delimiter - fn readline(&mut self) -> Result, MultipartError> { - self.read_until(b"\n") - } - - /// Read bytes until new line delimiter or eof - fn readline_or_eof(&mut self) -> Result, MultipartError> { - match self.readline() { - Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), - line => line, - } - } - - /// Put unprocessed data back to the buffer - fn unprocessed(&mut self, data: Bytes) { - let buf = BytesMut::from(data.as_ref()); - let buf = std::mem::replace(&mut self.buf, buf); - self.buf.extend_from_slice(&buf); - } -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use actix_http::h1; - use actix_web::{ - http::header::{DispositionParam, DispositionType}, - rt, - test::TestRequest, - FromRequest, - }; - use bytes::BufMut as _; - use futures_util::{future::lazy, StreamExt as _}; - use tokio::sync::mpsc; - use tokio_stream::wrappers::UnboundedReceiverStream; - - use super::*; - - const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0"; - - #[actix_rt::test] - async fn test_boundary() { - let headers = HeaderMap::new(); - match Multipart::boundary(&headers) { - Err(MultipartError::NoContentType) => {} - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("test"), - ); - - match Multipart::boundary(&headers) { - Err(MultipartError::ParseContentType) => {} - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("multipart/mixed"), - ); - match Multipart::boundary(&headers) { - Err(MultipartError::Boundary) => {} - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static( - "multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"", - ), - ); - - assert_eq!( - Multipart::boundary(&headers).unwrap(), - "5c02368e880e436dab70ed54e1c58209" - ); - } - - fn create_stream() -> ( - mpsc::UnboundedSender>, - impl Stream>, - ) { - let (tx, rx) = mpsc::unbounded_channel(); - - ( - tx, - UnboundedReceiverStream::new(rx).map(|res| res.map_err(|_| panic!())), - ) - } - - // Stream that returns from a Bytes, one char at a time and Pending every other poll() - struct SlowStream { - bytes: Bytes, - pos: usize, - ready: bool, - } - - impl SlowStream { - fn new(bytes: Bytes) -> SlowStream { - SlowStream { - bytes, - pos: 0, - ready: false, - } - } - } - - impl Stream for SlowStream { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - if !this.ready { - this.ready = true; - cx.waker().wake_by_ref(); - return Poll::Pending; - } - - if this.pos == this.bytes.len() { - return Poll::Ready(None); - } - - let res = Poll::Ready(Some(Ok(this.bytes.slice(this.pos..(this.pos + 1))))); - this.pos += 1; - this.ready = false; - res - } - } - - fn create_simple_request_with_header() -> (Bytes, HeaderMap) { - let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary( - BOUNDARY, - "file", - Some("fn.txt".to_owned()), - Some(mime::TEXT_PLAIN_UTF_8), - Bytes::from_static(b"data"), - ); - - let mut buf = BytesMut::with_capacity(body.len() + 14); - - // add junk before form to test pre-boundary data rejection - buf.put("testasdadsad\r\n".as_bytes()); - - buf.put(body); - - (buf.freeze(), headers) - } - - // TODO: use test utility when multi-file support is introduced - fn create_double_request_with_header() -> (Bytes, HeaderMap) { - let bytes = Bytes::from( - "testasdadsad\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - test\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - data\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", - ); - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static( - "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", - ), - ); - (bytes, headers) - } - - #[actix_rt::test] - async fn test_multipart_no_end_crlf() { - let (sender, payload) = create_stream(); - let (mut bytes, headers) = create_double_request_with_header(); - let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf - - sender.send(Ok(bytes_stripped)).unwrap(); - drop(sender); // eof - - let mut multipart = Multipart::new(&headers, payload); - - match multipart.next().await.unwrap() { - Ok(_) => {} - _ => unreachable!(), - } - - match multipart.next().await.unwrap() { - Ok(_) => {} - _ => unreachable!(), - } - - match multipart.next().await { - None => {} - _ => unreachable!(), - } - } - - #[actix_rt::test] - async fn test_multipart() { - let (sender, payload) = create_stream(); - let (bytes, headers) = create_double_request_with_header(); - - sender.send(Ok(bytes)).unwrap(); - - let mut multipart = Multipart::new(&headers, payload); - match multipart.next().await { - Some(Ok(mut field)) => { - let cd = field.content_disposition(); - assert_eq!(cd.disposition, DispositionType::FormData); - assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); - - assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); - assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); - - match field.next().await.unwrap() { - Ok(chunk) => assert_eq!(chunk, "test"), - _ => unreachable!(), - } - match field.next().await { - None => {} - _ => unreachable!(), - } - } - _ => unreachable!(), - } - - match multipart.next().await.unwrap() { - Ok(mut field) => { - assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); - assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); - - match field.next().await { - Some(Ok(chunk)) => assert_eq!(chunk, "data"), - _ => unreachable!(), - } - match field.next().await { - None => {} - _ => unreachable!(), - } - } - _ => unreachable!(), - } - - match multipart.next().await { - None => {} - _ => unreachable!(), - } - } - - // Loops, collecting all bytes until end-of-field - async fn get_whole_field(field: &mut Field) -> BytesMut { - let mut b = BytesMut::new(); - loop { - match field.next().await { - Some(Ok(chunk)) => b.extend_from_slice(&chunk), - None => return b, - _ => unreachable!(), - } - } - } - - #[actix_rt::test] - async fn test_stream() { - let (bytes, headers) = create_double_request_with_header(); - let payload = SlowStream::new(bytes); - - let mut multipart = Multipart::new(&headers, payload); - match multipart.next().await.unwrap() { - Ok(mut field) => { - let cd = field.content_disposition(); - assert_eq!(cd.disposition, DispositionType::FormData); - assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); - - assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); - assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); - - assert_eq!(get_whole_field(&mut field).await, "test"); - } - _ => unreachable!(), - } - - match multipart.next().await { - Some(Ok(mut field)) => { - assert_eq!(field.content_type().unwrap().type_(), mime::TEXT); - assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN); - - assert_eq!(get_whole_field(&mut field).await, "data"); - } - _ => unreachable!(), - } - - match multipart.next().await { - None => {} - _ => unreachable!(), - } - } - - #[actix_rt::test] - async fn test_basic() { - let (_, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(payload.buf.len(), 0); - lazy(|cx| payload.poll_stream(cx)).await.unwrap(); - assert_eq!(None, payload.read_max(1).unwrap()); - } - - #[actix_rt::test] - async fn test_eof() { - let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(None, payload.read_max(4).unwrap()); - sender.feed_data(Bytes::from("data")); - sender.feed_eof(); - lazy(|cx| payload.poll_stream(cx)).await.unwrap(); - - assert_eq!(Some(Bytes::from("data")), payload.read_max(4).unwrap()); - assert_eq!(payload.buf.len(), 0); - assert!(payload.read_max(1).is_err()); - assert!(payload.eof); - } - - #[actix_rt::test] - async fn test_err() { - let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - assert_eq!(None, payload.read_max(1).unwrap()); - sender.set_error(PayloadError::Incomplete(None)); - lazy(|cx| payload.poll_stream(cx)).await.err().unwrap(); - } - - #[actix_rt::test] - async fn test_readmax() { - let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - lazy(|cx| payload.poll_stream(cx)).await.unwrap(); - assert_eq!(payload.buf.len(), 10); - - assert_eq!(Some(Bytes::from("line1")), payload.read_max(5).unwrap()); - assert_eq!(payload.buf.len(), 5); - - assert_eq!(Some(Bytes::from("line2")), payload.read_max(5).unwrap()); - assert_eq!(payload.buf.len(), 0); - } - - #[actix_rt::test] - async fn test_readexactly() { - let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(None, payload.read_exact(2)); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - lazy(|cx| payload.poll_stream(cx)).await.unwrap(); - - assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2)); - assert_eq!(payload.buf.len(), 8); - - assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4)); - assert_eq!(payload.buf.len(), 4); - } - - #[actix_rt::test] - async fn test_readuntil() { - let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(None, payload.read_until(b"ne").unwrap()); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - lazy(|cx| payload.poll_stream(cx)).await.unwrap(); - - assert_eq!( - Some(Bytes::from("line")), - payload.read_until(b"ne").unwrap() - ); - assert_eq!(payload.buf.len(), 6); - - assert_eq!( - Some(Bytes::from("1line2")), - payload.read_until(b"2").unwrap() - ); - assert_eq!(payload.buf.len(), 0); - } - - #[actix_rt::test] - async fn test_multipart_from_error() { - let err = MultipartError::NoContentType; - let mut multipart = Multipart::from_error(err); - assert!(multipart.next().await.unwrap().is_err()) - } - - #[actix_rt::test] - async fn test_multipart_from_boundary() { - let (_, payload) = create_stream(); - let (_, headers) = create_simple_request_with_header(); - let boundary = Multipart::boundary(&headers); - assert!(boundary.is_ok()); - let _ = Multipart::from_boundary(boundary.unwrap(), payload); - } - - #[actix_rt::test] - async fn test_multipart_payload_consumption() { - // with sample payload and HttpRequest with no headers - let (_, inner_payload) = h1::Payload::create(false); - let mut payload = actix_web::dev::Payload::from(inner_payload); - let req = TestRequest::default().to_http_request(); - - // multipart should generate an error - let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap(); - assert!(mp.next().await.unwrap().is_err()); - - // and should not consume the payload - match payload { - actix_web::dev::Payload::H1 { .. } => {} //expected - _ => unreachable!(), - } - } - - #[actix_rt::test] - async fn no_content_disposition() { - let bytes = Bytes::from( - "testasdadsad\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - test\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", - ); - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static( - "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", - ), - ); - let payload = SlowStream::new(bytes); - - let mut multipart = Multipart::new(&headers, payload); - let res = multipart.next().await.unwrap(); - assert!(res.is_err()); - assert!(matches!( - res.unwrap_err(), - MultipartError::NoContentDisposition, - )); - } - - #[actix_rt::test] - async fn no_name_in_content_disposition() { - let bytes = Bytes::from( - "testasdadsad\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Disposition: form-data; filename=\"fn.txt\"\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - test\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", - ); - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static( - "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", - ), - ); - let payload = SlowStream::new(bytes); - - let mut multipart = Multipart::new(&headers, payload); - let res = multipart.next().await.unwrap(); - assert!(res.is_err()); - assert!(matches!( - res.unwrap_err(), - MultipartError::NoContentDisposition, - )); - } - - #[actix_rt::test] - async fn test_drop_multipart_dont_hang() { - let (sender, payload) = create_stream(); - let (bytes, headers) = create_simple_request_with_header(); - sender.send(Ok(bytes)).unwrap(); - drop(sender); // eof - - let mut multipart = Multipart::new(&headers, payload); - let mut field = multipart.next().await.unwrap().unwrap(); - - drop(multipart); - - // should fail immediately - match field.next().await { - Some(Err(MultipartError::NotConsumed)) => {} - _ => panic!(), - }; - } - - #[actix_rt::test] - async fn test_drop_field_awaken_multipart() { - let (sender, payload) = create_stream(); - let (bytes, headers) = create_double_request_with_header(); - sender.send(Ok(bytes)).unwrap(); - drop(sender); // eof - - let mut multipart = Multipart::new(&headers, payload); - let mut field = multipart.next().await.unwrap().unwrap(); - - let task = rt::spawn(async move { - rt::time::sleep(Duration::from_secs(1)).await; - assert_eq!(field.next().await.unwrap().unwrap(), "test"); - drop(field); - }); - - // dropping field should awaken current task - let _ = multipart.next().await.unwrap().unwrap(); - task.await.unwrap(); - } -} diff --git a/actix-multipart/src/test.rs b/actix-multipart/src/test.rs index 77d918283..b0e907266 100644 --- a/actix-multipart/src/test.rs +++ b/actix-multipart/src/test.rs @@ -1,10 +1,11 @@ -use actix_web::http::header::{self, HeaderMap}; -use bytes::{BufMut as _, Bytes, BytesMut}; -use mime::Mime; -use rand::{ - distributions::{Alphanumeric, DistString as _}, - thread_rng, +//! Multipart testing utilities. + +use actix_web::{ + http::header::{self, HeaderMap}, + web::{BufMut as _, Bytes, BytesMut}, }; +use mime::Mime; +use rand::distr::{Alphanumeric, SampleString as _}; const CRLF: &[u8] = b"\r\n"; const CRLF_CRLF: &[u8] = b"\r\n\r\n"; @@ -21,8 +22,7 @@ const BOUNDARY_PREFIX: &str = "------------------------"; /// /// ``` /// use actix_multipart::test::create_form_data_payload_and_headers; -/// use actix_web::test::TestRequest; -/// use bytes::Bytes; +/// use actix_web::{test::TestRequest, web::Bytes}; /// use memchr::memmem::find; /// /// let (body, headers) = create_form_data_payload_and_headers( @@ -61,7 +61,7 @@ pub fn create_form_data_payload_and_headers( content_type: Option, file: Bytes, ) -> (Bytes, HeaderMap) { - let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32); + let boundary = Alphanumeric.sample_string(&mut rand::rng(), 32); create_form_data_payload_and_headers_with_boundary( &boundary, diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 7e7e3beb8..7def1bdb4 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -38,6 +38,9 @@ http = "0.2.7" serde = { version = "1", features = ["derive"] } percent-encoding = "2.1" +[lints] +workspace = true + [[bench]] name = "router" harness = false diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index ce2dcf8f3..2f50619f8 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -511,11 +511,6 @@ mod tests { value: String, } - #[derive(Deserialize)] - struct Id { - _id: String, - } - #[derive(Debug, Deserialize)] struct Test1(String, u32); diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs index c4d0d2c87..3f5e969e7 100644 --- a/actix-router/src/lib.rs +++ b/actix-router/src/lib.rs @@ -1,7 +1,5 @@ //! Resource path matching and router. -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index 9031ab763..ab4a943fe 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -143,9 +143,9 @@ impl Path { for (seg_name, val) in self.segments.iter() { if name == seg_name { return match val { - PathItem::Static(ref s) => Some(s), - PathItem::Segment(s, e) => { - Some(&self.path.path()[(*s as usize)..(*e as usize)]) + PathItem::Static(ref seg) => Some(seg), + PathItem::Segment(start, end) => { + Some(&self.path.path()[(*start as usize)..(*end as usize)]) } }; } @@ -193,8 +193,10 @@ impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> { if self.idx < self.params.segment_count() { let idx = self.idx; let res = match self.params.segments[idx].1 { - PathItem::Static(ref s) => s, - PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], + PathItem::Static(ref seg) => seg, + PathItem::Segment(start, end) => { + &self.params.path.path()[(start as usize)..(end as usize)] + } }; self.idx += 1; return Some((&self.params.segments[idx].0, res)); @@ -217,8 +219,8 @@ impl Index for Path { fn index(&self, idx: usize) -> &str { match self.segments[idx].1 { - PathItem::Static(ref s) => s, - PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], + PathItem::Static(ref seg) => seg, + PathItem::Segment(start, end) => &self.path.path()[(start as usize)..(end as usize)], } } } diff --git a/actix-router/src/quoter.rs b/actix-router/src/quoter.rs index 6c929d3ac..78694ed72 100644 --- a/actix-router/src/quoter.rs +++ b/actix-router/src/quoter.rs @@ -105,7 +105,7 @@ fn hex_pair_to_char(d1: u8, d2: u8) -> Option { let d_low = char::from(d2).to_digit(16)?; // left shift high nibble by 4 bits - Some((d_high as u8) << 4 | (d_low as u8)) + Some(((d_high as u8) << 4) | (d_low as u8)) } #[derive(Debug, Default, Clone)] diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 3a102945b..b5ee01958 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -1021,6 +1021,7 @@ impl ResourceDef { panic!("prefix resource definitions should not have tail segments"); } + #[allow(clippy::literal_string_with_formatting_args)] if unprocessed.ends_with('*') { // unnamed tail segment @@ -1369,6 +1370,7 @@ mod tests { assert_eq!(path.unprocessed(), ""); } + #[allow(clippy::literal_string_with_formatting_args)] #[test] fn newline_patterns_and_paths() { let re = ResourceDef::new("/user/a\nb"); diff --git a/actix-router/src/resource_path.rs b/actix-router/src/resource_path.rs index 45948aa2a..610dc344d 100644 --- a/actix-router/src/resource_path.rs +++ b/actix-router/src/resource_path.rs @@ -19,7 +19,7 @@ impl ResourcePath for String { } } -impl<'a> ResourcePath for &'a str { +impl ResourcePath for &str { fn path(&self) -> &str { self } diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index 1dd4449da..b20cb7ee3 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -145,6 +145,7 @@ mod tests { }; #[allow(clippy::cognitive_complexity)] + #[allow(clippy::literal_string_with_formatting_args)] #[test] fn test_recognizer_1() { let mut router = Router::::build(); diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index e810ae80b..34fdf2c82 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -73,3 +73,6 @@ tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true } tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true } tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true } tokio = { version = "1.24.2", features = ["sync"] } + +[lints] +workspace = true diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 9be99978d..f0da2c20d 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -27,8 +27,6 @@ //! } //! ``` -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 3e854c0b8..3f214274d 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -2,7 +2,10 @@ ## Unreleased -- Take the encoded buffer when yielding bytes in the response stream rather than splitting the buffer, reducing memory use +## 4.3.1 + +- Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream. +- Mark crate as deprecated. - Minimum supported Rust version (MSRV) is now 1.72. ## 4.3.0 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 3c74a4f47..e7034ab84 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "actix-web-actors" -version = "4.3.0" +version = "4.3.1+deprecated" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" -license = "MIT OR Apache-2.0" -edition = "2021" +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true [package.metadata.cargo_check_external_types] allowed_external_types = [ @@ -41,3 +42,6 @@ actix-web = { version = "4", features = ["macros"] } env_logger = "0.11" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } mime = "0.3" + +[lints] +workspace = true diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index feb3d1b33..0ec91a224 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -1,15 +1,17 @@ # `actix-web-actors` > Actix actors support for Actix Web. +> +> This crate is deprecated. Migrate to [`actix-ws`](https://crates.io/crates/actix-ws). [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.0)](https://docs.rs/actix-web-actors/4.3.0) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.1)](https://docs.rs/actix-web-actors/4.3.1) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.3.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.3.0) +![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index d89b0ee35..4831d2637 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,5 +1,7 @@ //! Actix actors support for Actix Web. //! +//! This crate is deprecated. Migrate to [`actix-ws`](https://crates.io/crates/actix-ws). +//! //! # Examples //! //! ```no_run @@ -55,8 +57,6 @@ //! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses. //! -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 7f7607fa9..0002f87e2 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -796,11 +796,8 @@ where Some(frm) => { let msg = match frm { Frame::Text(data) => { - Message::Text(ByteString::try_from(data).map_err(|e| { - ProtocolError::Io(io::Error::new( - io::ErrorKind::Other, - format!("{}", e), - )) + Message::Text(ByteString::try_from(data).map_err(|err| { + ProtocolError::Io(io::Error::new(io::ErrorKind::Other, err)) })?) } Frame::Binary(data) => Message::Binary(data), diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 7500807d2..b61565951 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -34,4 +34,7 @@ actix-web = "4" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } trybuild = "1" -rustversion = "1" +rustversion-msrv = "0.100" + +[lints] +workspace = true diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index c518007a0..e22bff8cd 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -73,8 +73,6 @@ //! [DELETE]: macro@delete #![recursion_limit = "512"] -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/actix-web-codegen/tests/routes.rs b/actix-web-codegen/tests/routes.rs index fb50d4ae0..1443f9a75 100644 --- a/actix-web-codegen/tests/routes.rs +++ b/actix-web-codegen/tests/routes.rs @@ -136,7 +136,7 @@ async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responde } #[get("/custom_resource_name", name = "custom")] -async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder { +async fn custom_resource_name_test(req: HttpRequest) -> impl Responder { assert!(req.url_for_static("custom").is_ok()); assert!(req.url_for_static("custom_resource_name_test").is_err()); HttpResponse::Ok() @@ -145,7 +145,7 @@ async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder { mod guard_module { use actix_web::{guard::GuardContext, http::header}; - pub fn guard(ctx: &GuardContext) -> bool { + pub fn guard(ctx: &GuardContext<'_>) -> bool { ctx.header::() .map(|h| h.preference() == "image/*") .unwrap_or(false) diff --git a/actix-web-codegen/tests/scopes.rs b/actix-web-codegen/tests/scopes.rs index 4ee6db16f..b8c832682 100644 --- a/actix-web-codegen/tests/scopes.rs +++ b/actix-web-codegen/tests/scopes.rs @@ -1,7 +1,7 @@ use actix_web::{guard::GuardContext, http, http::header, web, App, HttpResponse, Responder}; use actix_web_codegen::{delete, get, post, route, routes, scope}; -pub fn image_guard(ctx: &GuardContext) -> bool { +pub fn image_guard(ctx: &GuardContext<'_>) -> bool { ctx.header::() .map(|h| h.preference() == "image/*") .unwrap_or(false) diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 91073cf3b..0150d56f2 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.72)] // MSRV +#[rustversion_msrv::msrv] #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 1c8731a3e..ddab0e028 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -2,6 +2,35 @@ ## Unreleased +## 4.10.2 + +- No significant changes since `4.10.1`. + +## 4.10.1 + +- No significant changes since `4.10.0`. + +## 4.10.0 + +### Added + +- Implement `Responder` for `Result<(), E: Into>`. Returning `Ok(())` responds with HTTP 204 No Content. + +### Changed + +- On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use. +- Update `brotli` dependency to `7`. +- Minimum supported Rust version (MSRV) is now 1.75. + +## 4.9.0 + +### Added + +- Add `middleware::from_fn()` helper. +- Add `web::ThinData` extractor. + +## 4.8.0 + ### Added - Add `web::Html` responder. @@ -10,8 +39,9 @@ ### Fixed -- `ConnectionInfo::realip_remote_addr()` now handles IPv6 addresses from `Forwarded` header correctly. Previously, it sometimes returned the forwarded port as well. +- Always remove port from return value of `ConnectionInfo::realip_remote_addr()` when handling IPv6 addresses. from the `Forwarded` header. - The `UrlencodedError::ContentType` variant (relevant to the `Form` extractor) now uses the 415 (Media Type Unsupported) status code in it's `ResponseError` implementation. +- Apply `HttpServer::max_connection_rate()` setting when using rustls v0.22 or v0.23. ## 4.7.0 diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 8c1c9fbc6..86263811c 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.7.0" +version = "4.10.2" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" authors = [ "Nikolay Kim ", @@ -93,18 +93,18 @@ secure-cookies = ["cookies", "cookie/secure"] http2 = ["actix-http/http2"] # TLS via OpenSSL -openssl = ["http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] +openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # TLS via Rustls v0.20 rustls = ["rustls-0_20"] # TLS via Rustls v0.20 -rustls-0_20 = ["http2", "actix-http/rustls-0_20", "actix-tls/accept", "actix-tls/rustls-0_20"] +rustls-0_20 = ["__tls", "http2", "actix-http/rustls-0_20", "actix-tls/accept", "actix-tls/rustls-0_20"] # TLS via Rustls v0.21 -rustls-0_21 = ["http2", "actix-http/rustls-0_21", "actix-tls/accept", "actix-tls/rustls-0_21"] +rustls-0_21 = ["__tls", "http2", "actix-http/rustls-0_21", "actix-tls/accept", "actix-tls/rustls-0_21"] # TLS via Rustls v0.22 -rustls-0_22 = ["http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"] +rustls-0_22 = ["__tls", "http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"] # TLS via Rustls v0.23 -rustls-0_23 = ["http2", "actix-http/rustls-0_23", "actix-tls/accept", "actix-tls/rustls-0_23"] +rustls-0_23 = ["__tls", "http2", "actix-http/rustls-0_23", "actix-tls/accept", "actix-tls/rustls-0_23"] # Full unicode support unicode = ["dep:regex", "actix-router/unicode"] @@ -113,6 +113,10 @@ unicode = ["dep:regex", "actix-router/unicode"] # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] +# Internal (PRIVATE!) features used to aid checking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__tls = [] + # io-uring feature only available for Linux OSes. experimental-io-uring = ["actix-server/io-uring"] @@ -133,24 +137,25 @@ actix-service = "2" actix-utils = "3" actix-tls = { version = "3.4", default-features = false, optional = true } -actix-http = { version = "3.7", features = ["ws"] } +actix-http = { version = "3.10", features = ["ws"] } actix-router = { version = "0.5.3", default-features = false, features = ["http"] } actix-web-codegen = { version = "4.3", optional = true, default-features = false } -ahash = "0.8" bytes = "1" bytestring = "1" cfg-if = "1" cookie = { version = "0.16", features = ["percent-encode"], optional = true } -derive_more = "0.99.8" +derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] } encoding_rs = "0.8" +foldhash = "0.1" futures-core = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false } itoa = "1" +impl-more = "0.1.4" language-tags = "0.3" log = "0.4" mime = "0.3" -once_cell = "1.5" +once_cell = "1.21" pin-project-lite = "0.2.7" regex = { version = "1.5.5", optional = true } regex-lite = "0.1" @@ -158,6 +163,7 @@ serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" smallvec = "1.6.1" +tracing = "0.1.30" socket2 = "0.5" time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" @@ -167,17 +173,17 @@ actix-files = "0.6" actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] } awc = { version = "3", features = ["openssl"] } -brotli = "6" +brotli = "7" const-str = "0.5" core_affinity = "0.8" criterion = { version = "0.5", features = ["html_reports"] } env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } -rand = "0.8" +rand = "0.9" rcgen = "0.13" rustls-pemfile = "2" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1", features = ["derive"] } static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.55" } tls-rustls = { package = "rustls", version = "0.23" } @@ -185,6 +191,9 @@ tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } zstd = "0.13" capture-logger = "0.1" +[lints] +workspace = true + [[test]] name = "test_server" required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] diff --git a/actix-web/README.md b/actix-web/README.md index 3f9d3e0d5..63349a35a 100644 --- a/actix-web/README.md +++ b/actix-web/README.md @@ -8,13 +8,13 @@ [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.7.0)](https://docs.rs/actix-web/4.7.0) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.10.2)](https://docs.rs/actix-web/4.10.2) ![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.7.0/status.svg)](https://deps.rs/crate/actix-web/4.7.0) +[![Dependency Status](https://deps.rs/crate/actix-web/4.10.2/status.svg)](https://deps.rs/crate/actix-web/4.10.2)
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) +[![codecov](https://codecov.io/gh/actix/actix-web/graph/badge.svg?token=dSwOnp9QCv)](https://codecov.io/gh/actix/actix-web) ![downloads](https://img.shields.io/crates/d/actix-web.svg) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web/benches/responder.rs b/actix-web/benches/responder.rs index c675eadff..489515e40 100644 --- a/actix-web/benches/responder.rs +++ b/actix-web/benches/responder.rs @@ -2,11 +2,9 @@ use std::{future::Future, time::Instant}; use actix_http::body::BoxBody; use actix_utils::future::{ready, Ready}; -use actix_web::{ - error, http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder, -}; +use actix_web::{http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder}; use criterion::{criterion_group, criterion_main, Criterion}; -use futures_util::future::{join_all, Either}; +use futures_util::future::join_all; // responder simulate the old responder trait. trait FutureResponder { @@ -16,9 +14,6 @@ trait FutureResponder { fn future_respond_to(self, req: &HttpRequest) -> Self::Future; } -// a simple option responder type. -struct OptionResponder(Option); - // a simple wrapper type around string struct StringResponder(String); @@ -34,22 +29,6 @@ impl FutureResponder for StringResponder { } } -impl FutureResponder for OptionResponder -where - T: FutureResponder, - T::Future: Future>, -{ - type Error = Error; - type Future = Either>>; - - fn future_respond_to(self, req: &HttpRequest) -> Self::Future { - match self.0 { - Some(t) => Either::Left(t.future_respond_to(req)), - None => Either::Right(ready(Err(error::ErrorInternalServerError("err")))), - } - } -} - impl Responder for StringResponder { type Body = BoxBody; @@ -60,17 +39,6 @@ impl Responder for StringResponder { } } -impl Responder for OptionResponder { - type Body = BoxBody; - - fn respond_to(self, req: &HttpRequest) -> HttpResponse { - match self.0 { - Some(t) => t.respond_to(req).map_into_boxed_body(), - None => HttpResponse::from_error(error::ErrorInternalServerError("err")), - } - } -} - fn future_responder(c: &mut Criterion) { let rt = actix_rt::System::new(); let req = TestRequest::default().to_http_request(); diff --git a/actix-web/examples/from_fn.rs b/actix-web/examples/from_fn.rs new file mode 100644 index 000000000..a6006d23c --- /dev/null +++ b/actix-web/examples/from_fn.rs @@ -0,0 +1,128 @@ +//! Shows a few of ways to use the `from_fn` middleware. + +use std::{collections::HashMap, io, rc::Rc, time::Duration}; + +use actix_web::{ + body::MessageBody, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + http::header::{self, HeaderValue, Range}, + middleware::{from_fn, Logger, Next}, + web::{self, Header, Query}, + App, Error, HttpResponse, HttpServer, +}; +use tracing::info; + +async fn noop(req: ServiceRequest, next: Next) -> Result, Error> { + next.call(req).await +} + +async fn print_range_header( + range_header: Option>, + req: ServiceRequest, + next: Next, +) -> Result, Error> { + if let Some(Header(range)) = range_header { + println!("Range: {range}"); + } else { + println!("No Range header"); + } + + next.call(req).await +} + +async fn mutate_body_type( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + let res = next.call(req).await?; + Ok(res.map_into_left_body::<()>()) +} + +async fn mutate_body_type_with_extractors( + string_body: String, + query: Query>, + req: ServiceRequest, + next: Next, +) -> Result, Error> { + println!("body is: {string_body}"); + println!("query string: {query:?}"); + + let res = next.call(req).await?; + + Ok(res.map_body(move |_, _| string_body)) +} + +async fn timeout_10secs( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + match tokio::time::timeout(Duration::from_secs(10), next.call(req)).await { + Ok(res) => res, + Err(_err) => Err(actix_web::error::ErrorRequestTimeout("")), + } +} + +struct MyMw(bool); + +impl MyMw { + async fn mw_cb( + &self, + req: ServiceRequest, + next: Next, + ) -> Result, Error> { + let mut res = match self.0 { + true => req.into_response("short-circuited").map_into_right_body(), + false => next.call(req).await?.map_into_left_body(), + }; + + res.headers_mut() + .insert(header::WARNING, HeaderValue::from_static("42")); + + Ok(res) + } + + pub fn into_middleware( + self, + ) -> impl Transform< + S, + ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + > + where + S: Service, Error = Error> + 'static, + B: MessageBody + 'static, + { + let this = Rc::new(self); + from_fn(move |req, next| { + let this = Rc::clone(&this); + async move { Self::mw_cb(&this, req, next).await } + }) + } +} + +#[actix_web::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let bind = ("127.0.0.1", 8080); + info!("staring server at http://{}:{}", &bind.0, &bind.1); + + HttpServer::new(|| { + App::new() + .wrap(from_fn(noop)) + .wrap(from_fn(print_range_header)) + .wrap(from_fn(mutate_body_type)) + .wrap(from_fn(mutate_body_type_with_extractors)) + .wrap(from_fn(timeout_10secs)) + // switch bool to true to observe early response + .wrap(MyMw(false).into_middleware()) + .wrap(Logger::default()) + .default_service(web::to(HttpResponse::Ok)) + }) + .workers(1) + .bind(bind)? + .run() + .await +} diff --git a/actix-web/examples/middleware_from_fn.rs b/actix-web/examples/middleware_from_fn.rs new file mode 100644 index 000000000..da92ef05b --- /dev/null +++ b/actix-web/examples/middleware_from_fn.rs @@ -0,0 +1,127 @@ +//! Shows a couple of ways to use the `from_fn` middleware. + +use std::{collections::HashMap, io, rc::Rc, time::Duration}; + +use actix_web::{ + body::MessageBody, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + http::header::{self, HeaderValue, Range}, + middleware::{from_fn, Logger, Next}, + web::{self, Header, Query}, + App, Error, HttpResponse, HttpServer, +}; + +async fn noop(req: ServiceRequest, next: Next) -> Result, Error> { + next.call(req).await +} + +async fn print_range_header( + range_header: Option>, + req: ServiceRequest, + next: Next, +) -> Result, Error> { + if let Some(Header(range)) = range_header { + println!("Range: {range}"); + } else { + println!("No Range header"); + } + + next.call(req).await +} + +async fn mutate_body_type( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + let res = next.call(req).await?; + Ok(res.map_into_left_body::<()>()) +} + +async fn mutate_body_type_with_extractors( + string_body: String, + query: Query>, + req: ServiceRequest, + next: Next, +) -> Result, Error> { + println!("body is: {string_body}"); + println!("query string: {query:?}"); + + let res = next.call(req).await?; + + Ok(res.map_body(move |_, _| string_body)) +} + +async fn timeout_10secs( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + match tokio::time::timeout(Duration::from_secs(10), next.call(req)).await { + Ok(res) => res, + Err(_err) => Err(actix_web::error::ErrorRequestTimeout("")), + } +} + +struct MyMw(bool); + +impl MyMw { + async fn mw_cb( + &self, + req: ServiceRequest, + next: Next, + ) -> Result, Error> { + let mut res = match self.0 { + true => req.into_response("short-circuited").map_into_right_body(), + false => next.call(req).await?.map_into_left_body(), + }; + + res.headers_mut() + .insert(header::WARNING, HeaderValue::from_static("42")); + + Ok(res) + } + + pub fn into_middleware( + self, + ) -> impl Transform< + S, + ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + > + where + S: Service, Error = Error> + 'static, + B: MessageBody + 'static, + { + let this = Rc::new(self); + from_fn(move |req, next| { + let this = Rc::clone(&this); + async move { Self::mw_cb(&this, req, next).await } + }) + } +} + +#[actix_web::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let bind = ("127.0.0.1", 8080); + log::info!("staring server at http://{}:{}", &bind.0, &bind.1); + + HttpServer::new(|| { + App::new() + .wrap(from_fn(noop)) + .wrap(from_fn(print_range_header)) + .wrap(from_fn(mutate_body_type)) + .wrap(from_fn(mutate_body_type_with_extractors)) + .wrap(from_fn(timeout_10secs)) + // switch bool to true to observe early response + .wrap(MyMw(false).into_middleware()) + .wrap(Logger::default()) + .default_service(web::to(HttpResponse::Ok)) + }) + .workers(1) + .bind(bind)? + .run() + .await +} diff --git a/actix-web/src/app.rs b/actix-web/src/app.rs index 3d86d1f9b..f12d39979 100644 --- a/actix-web/src/app.rs +++ b/actix-web/src/app.rs @@ -39,7 +39,7 @@ impl App { let factory_ref = Rc::new(RefCell::new(None)); App { - endpoint: AppEntry::new(factory_ref.clone()), + endpoint: AppEntry::new(Rc::clone(&factory_ref)), data_factories: Vec::new(), services: Vec::new(), default: None, @@ -234,7 +234,6 @@ where /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. - /// * "StaticFiles" is a service for static files support pub fn service(mut self, factory: F) -> Self where F: HttpServiceFactory + 'static, @@ -270,9 +269,9 @@ where + 'static, U::InitError: fmt::Debug, { - let svc = svc - .into_factory() - .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)); + let svc = svc.into_factory().map_init_err(|err| { + log::error!("Can not construct default service: {err:?}"); + }); self.default = Some(Rc::new(boxed::factory(svc))); diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index 65a6ed87b..7aa16b790 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -71,7 +71,7 @@ where }); // create App config to pass to child services - let mut config = AppService::new(config, default.clone()); + let mut config = AppService::new(config, Rc::clone(&default)); // register services mem::take(&mut *self.services.borrow_mut()) diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index 5e8b056f1..0e856f574 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -68,7 +68,7 @@ impl AppService { pub(crate) fn clone_config(&self) -> Self { AppService { config: self.config.clone(), - default: self.default.clone(), + default: Rc::clone(&self.default), services: Vec::new(), root: false, } @@ -81,7 +81,7 @@ impl AppService { /// Returns default handler factory. pub fn default_service(&self) -> Rc { - self.default.clone() + Rc::clone(&self.default) } /// Register HTTP service. diff --git a/actix-web/src/data.rs b/actix-web/src/data.rs index acbb8e23a..088df55d2 100644 --- a/actix-web/src/data.rs +++ b/actix-web/src/data.rs @@ -184,7 +184,7 @@ impl FromRequest for Data { impl DataFactory for Data { fn create(&self, extensions: &mut Extensions) -> bool { - extensions.insert(Data(self.0.clone())); + extensions.insert(Data(Arc::clone(&self.0))); true } } diff --git a/actix-web/src/error/mod.rs b/actix-web/src/error/mod.rs index 25535332c..b2f672720 100644 --- a/actix-web/src/error/mod.rs +++ b/actix-web/src/error/mod.rs @@ -29,7 +29,7 @@ pub type Result = std::result::Result; /// An error representing a problem running a blocking task on a thread pool. #[derive(Debug, Display, Error)] -#[display(fmt = "Blocking thread pool is shut down unexpectedly")] +#[display("Blocking thread pool is shut down unexpectedly")] #[non_exhaustive] pub struct BlockingError; @@ -40,15 +40,15 @@ impl ResponseError for crate::error::BlockingError {} #[non_exhaustive] pub enum UrlGenerationError { /// Resource not found. - #[display(fmt = "Resource not found")] + #[display("Resource not found")] ResourceNotFound, /// Not all URL parameters covered. - #[display(fmt = "Not all URL parameters covered")] + #[display("Not all URL parameters covered")] NotEnoughElements, /// URL parse error. - #[display(fmt = "{}", _0)] + #[display("{}", _0)] ParseError(UrlParseError), } @@ -59,39 +59,39 @@ impl ResponseError for UrlGenerationError {} #[non_exhaustive] pub enum UrlencodedError { /// Can not decode chunked transfer encoding. - #[display(fmt = "Can not decode chunked transfer encoding.")] + #[display("Can not decode chunked transfer encoding.")] Chunked, /// Payload size is larger than allowed. (default limit: 256kB). #[display( - fmt = "URL encoded payload is larger ({} bytes) than allowed (limit: {} bytes).", + "URL encoded payload is larger ({} bytes) than allowed (limit: {} bytes).", size, limit )] Overflow { size: usize, limit: usize }, /// Payload size is now known. - #[display(fmt = "Payload size is now known.")] + #[display("Payload size is now known.")] UnknownLength, /// Content type error. - #[display(fmt = "Content type error.")] + #[display("Content type error.")] ContentType, /// Parse error. - #[display(fmt = "Parse error: {}.", _0)] + #[display("Parse error: {}.", _0)] Parse(FormDeError), /// Encoding error. - #[display(fmt = "Encoding error.")] + #[display("Encoding error.")] Encoding, /// Serialize error. - #[display(fmt = "Serialize error: {}.", _0)] + #[display("Serialize error: {}.", _0)] Serialize(FormError), /// Payload error. - #[display(fmt = "Error that occur during reading payload: {}.", _0)] + #[display("Error that occur during reading payload: {}.", _0)] Payload(PayloadError), } @@ -113,30 +113,30 @@ impl ResponseError for UrlencodedError { pub enum JsonPayloadError { /// Payload size is bigger than allowed & content length header set. (default: 2MB) #[display( - fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).", + "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).", length, limit )] OverflowKnownLength { length: usize, limit: usize }, /// Payload size is bigger than allowed but no content length header set. (default: 2MB) - #[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)] + #[display("JSON payload has exceeded limit ({} bytes).", limit)] Overflow { limit: usize }, /// Content type error - #[display(fmt = "Content type error")] + #[display("Content type error")] ContentType, /// Deserialize error - #[display(fmt = "Json deserialize error: {}", _0)] + #[display("Json deserialize error: {}", _0)] Deserialize(JsonError), /// Serialize error - #[display(fmt = "Json serialize error: {}", _0)] + #[display("Json serialize error: {}", _0)] Serialize(JsonError), /// Payload error - #[display(fmt = "Error that occur during reading payload: {}", _0)] + #[display("Error that occur during reading payload: {}", _0)] Payload(PayloadError), } @@ -166,7 +166,7 @@ impl ResponseError for JsonPayloadError { #[non_exhaustive] pub enum PathError { /// Deserialize error - #[display(fmt = "Path deserialize error: {}", _0)] + #[display("Path deserialize error: {}", _0)] Deserialize(serde::de::value::Error), } @@ -182,7 +182,7 @@ impl ResponseError for PathError { #[non_exhaustive] pub enum QueryPayloadError { /// Query deserialize error. - #[display(fmt = "Query deserialize error: {}", _0)] + #[display("Query deserialize error: {}", _0)] Deserialize(serde::de::value::Error), } @@ -196,20 +196,20 @@ impl ResponseError for QueryPayloadError { #[derive(Debug, Display, Error, From)] #[non_exhaustive] pub enum ReadlinesError { - #[display(fmt = "Encoding error")] + #[display("Encoding error")] /// Payload size is bigger than allowed. (default: 256kB) EncodingError, /// Payload error. - #[display(fmt = "Error that occur during reading payload: {}", _0)] + #[display("Error that occur during reading payload: {}", _0)] Payload(PayloadError), /// Line limit exceeded. - #[display(fmt = "Line limit exceeded")] + #[display("Line limit exceeded")] LimitOverflow, /// ContentType error. - #[display(fmt = "Content-type error")] + #[display("Content-type error")] ContentTypeError(ContentTypeError), } diff --git a/actix-web/src/handler.rs b/actix-web/src/handler.rs index 6e4e2250a..10015cb69 100644 --- a/actix-web/src/handler.rs +++ b/actix-web/src/handler.rs @@ -19,7 +19,7 @@ use crate::{ /// 1. It is an async function (or a function/closure that returns an appropriate future); /// 1. The function parameters (up to 12) implement [`FromRequest`]; /// 1. The async function (or future) resolves to a type that can be converted into an -/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). +/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// /// /// # Compiler Errors diff --git a/actix-web/src/helpers.rs b/actix-web/src/helpers.rs index 1d2679fce..c7b33a083 100644 --- a/actix-web/src/helpers.rs +++ b/actix-web/src/helpers.rs @@ -10,7 +10,7 @@ use bytes::BufMut; /// perform a remaining length check before writing. pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); -impl<'a, B> io::Write for MutWriter<'a, B> +impl io::Write for MutWriter<'_, B> where B: BufMut, { diff --git a/actix-web/src/http/header/content_disposition.rs b/actix-web/src/http/header/content_disposition.rs index 9725cd19b..c836b1073 100644 --- a/actix-web/src/http/header/content_disposition.rs +++ b/actix-web/src/http/header/content_disposition.rs @@ -154,7 +154,7 @@ impl DispositionParam { #[inline] pub fn as_name(&self) -> Option<&str> { match self { - DispositionParam::Name(ref name) => Some(name.as_str()), + DispositionParam::Name(name) => Some(name.as_str()), _ => None, } } @@ -163,7 +163,7 @@ impl DispositionParam { #[inline] pub fn as_filename(&self) -> Option<&str> { match self { - DispositionParam::Filename(ref filename) => Some(filename.as_str()), + DispositionParam::Filename(filename) => Some(filename.as_str()), _ => None, } } @@ -172,7 +172,7 @@ impl DispositionParam { #[inline] pub fn as_filename_ext(&self) -> Option<&ExtendedValue> { match self { - DispositionParam::FilenameExt(ref value) => Some(value), + DispositionParam::FilenameExt(value) => Some(value), _ => None, } } @@ -206,11 +206,11 @@ impl DispositionParam { } } -/// A *Content-Disposition* header. It is compatible to be used either as -/// [a response header for the main body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body) -/// as (re)defined in [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266), or as -/// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) -/// as (re)defined in [RFC 7587](https://datatracker.ietf.org/doc/html/rfc7578). +/// `Content-Disposition` header. +/// +/// It is compatible to be used either as [a response header for the main body][use_main_body] +/// as (re)defined in [RFC 6266], or as [a header for a multipart body][use_multipart] as +/// (re)defined in [RFC 7587]. /// /// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if /// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as @@ -267,7 +267,7 @@ impl DispositionParam { /// parameters: vec![DispositionParam::FilenameExt(ExtendedValue { /// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename /// language_tag: None, // The optional language tag (see `language-tag` crate) -/// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename +/// value: b"\xA9 Ferris 2011.txt".to_vec(), // the actual bytes of the filename /// })], /// }; /// assert!(cd1.is_attachment()); @@ -305,6 +305,11 @@ impl DispositionParam { /// change to match local file system conventions if applicable, and do not use directory path /// information that may be present. /// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). +/// +/// [use_main_body]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body +/// [RFC 6266]: https://datatracker.ietf.org/doc/html/rfc6266 +/// [use_multipart]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_header_for_a_multipart_body +/// [RFC 7587]: https://datatracker.ietf.org/doc/html/rfc7578 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ContentDisposition { /// The disposition type @@ -493,7 +498,7 @@ impl Header for ContentDisposition { } fn parse(msg: &T) -> Result { - if let Some(h) = msg.headers().get(&Self::name()) { + if let Some(h) = msg.headers().get(Self::name()) { Self::from_raw(h) } else { Err(crate::error::ParseError::Header) diff --git a/actix-web/src/http/header/range.rs b/actix-web/src/http/header/range.rs index 2326bb19c..4a5d95d93 100644 --- a/actix-web/src/http/header/range.rs +++ b/actix-web/src/http/header/range.rs @@ -107,16 +107,16 @@ impl ByteRangeSpec { /// satisfiable if they meet the following conditions: /// /// > If a valid byte-range-set includes at least one byte-range-spec with a first-byte-pos that - /// is less than the current length of the representation, or at least one - /// suffix-byte-range-spec with a non-zero suffix-length, then the byte-range-set - /// is satisfiable. Otherwise, the byte-range-set is unsatisfiable. + /// > is less than the current length of the representation, or at least one + /// > suffix-byte-range-spec with a non-zero suffix-length, then the byte-range-set is + /// > satisfiable. Otherwise, the byte-range-set is unsatisfiable. /// /// The function also computes remainder ranges based on the RFC: /// /// > If the last-byte-pos value is absent, or if the value is greater than or equal to the - /// current length of the representation data, the byte range is interpreted as the remainder - /// of the representation (i.e., the server replaces the value of last-byte-pos with a value - /// that is one less than the current length of the selected representation). + /// > current length of the representation data, the byte range is interpreted as the remainder + /// > of the representation (i.e., the server replaces the value of last-byte-pos with a value + /// > that is one less than the current length of the selected representation). /// /// [RFC 7233 §2.1]: https://datatracker.ietf.org/doc/html/rfc7233 pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { @@ -270,7 +270,7 @@ impl Header for Range { #[inline] fn parse(msg: &T) -> Result { - header::from_one_raw_str(msg.headers().get(&Self::name())) + header::from_one_raw_str(msg.headers().get(Self::name())) } } diff --git a/actix-web/src/info.rs b/actix-web/src/info.rs index 1b2e554f9..76ca28ea7 100644 --- a/actix-web/src/info.rs +++ b/actix-web/src/info.rs @@ -235,7 +235,7 @@ impl FromRequest for ConnectionInfo { /// # let _svc = actix_web::web::to(handler); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] -#[display(fmt = "{}", _0)] +#[display("{}", _0)] pub struct PeerAddr(pub SocketAddr); impl PeerAddr { @@ -247,7 +247,7 @@ impl PeerAddr { #[derive(Debug, Display, Error)] #[non_exhaustive] -#[display(fmt = "Missing peer address")] +#[display("Missing peer address")] pub struct MissingPeerAddr; impl ResponseError for MissingPeerAddr {} diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index 205391388..e2a8e2275 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -70,8 +70,6 @@ //! - `rustls-0_23` - HTTPS support via `rustls` 0.23 crate, supports `HTTP/2` //! - `secure-cookies` - secure cookies support -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -106,6 +104,7 @@ mod scope; mod server; mod service; pub mod test; +mod thin_data; pub(crate) mod types; pub mod web; diff --git a/actix-web/src/middleware/default_headers.rs b/actix-web/src/middleware/default_headers.rs index f21afe6eb..2669a047e 100644 --- a/actix-web/src/middleware/default_headers.rs +++ b/actix-web/src/middleware/default_headers.rs @@ -141,7 +141,7 @@ where actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { - let inner = self.inner.clone(); + let inner = Rc::clone(&self.inner); let fut = self.service.call(req); DefaultHeaderFuture { diff --git a/actix-web/src/middleware/err_handlers.rs b/actix-web/src/middleware/err_handlers.rs index aa6d1c8a4..649c1a97a 100644 --- a/actix-web/src/middleware/err_handlers.rs +++ b/actix-web/src/middleware/err_handlers.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_service::{Service, Transform}; -use ahash::AHashMap; +use foldhash::HashMap as FoldHashMap; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; @@ -185,7 +185,7 @@ pub struct ErrorHandlers { handlers: Handlers, } -type Handlers = Rc>>>; +type Handlers = Rc>>>; impl Default for ErrorHandlers { fn default() -> Self { @@ -220,16 +220,20 @@ impl ErrorHandlers { /// [`.handler()`][ErrorHandlers::handler]) will fall back on this. /// /// Note that this will overwrite any default handlers previously set by calling - /// [`.default_handler_client()`][ErrorHandlers::default_handler_client] or - /// [`.default_handler_server()`][ErrorHandlers::default_handler_server], but not any set by - /// calling [`.handler()`][ErrorHandlers::handler]. + /// [`default_handler_client()`] or [`.default_handler_server()`], but not any set by calling + /// [`.handler()`]. + /// + /// [`default_handler_client()`]: ErrorHandlers::default_handler_client + /// [`.default_handler_server()`]: ErrorHandlers::default_handler_server + /// [`.handler()`]: ErrorHandlers::handler pub fn default_handler(self, handler: F) -> Self where F: Fn(ServiceResponse) -> Result> + 'static, { let handler = Rc::new(handler); + let handler2 = Rc::clone(&handler); Self { - default_server: Some(handler.clone()), + default_server: Some(handler2), default_client: Some(handler), ..self } @@ -288,7 +292,7 @@ where type Future = LocalBoxFuture<'static, Result>; fn new_transform(&self, service: S) -> Self::Future { - let handlers = self.handlers.clone(); + let handlers = Rc::clone(&self.handlers); let default_client = self.default_client.clone(); let default_server = self.default_server.clone(); Box::pin(async move { @@ -323,7 +327,7 @@ where actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { - let handlers = self.handlers.clone(); + let handlers = Rc::clone(&self.handlers); let default_client = self.default_client.clone(); let default_server = self.default_server.clone(); let fut = self.service.call(req); diff --git a/actix-web/src/middleware/from_fn.rs b/actix-web/src/middleware/from_fn.rs new file mode 100644 index 000000000..608833319 --- /dev/null +++ b/actix-web/src/middleware/from_fn.rs @@ -0,0 +1,349 @@ +use std::{future::Future, marker::PhantomData, rc::Rc}; + +use actix_service::boxed::{self, BoxFuture, RcService}; +use actix_utils::future::{ready, Ready}; +use futures_core::future::LocalBoxFuture; + +use crate::{ + body::MessageBody, + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + Error, FromRequest, +}; + +/// Wraps an async function to be used as a middleware. +/// +/// # Examples +/// +/// The wrapped function should have the following form: +/// +/// ``` +/// # use actix_web::{ +/// # App, Error, +/// # body::MessageBody, +/// # dev::{ServiceRequest, ServiceResponse, Service as _}, +/// # }; +/// use actix_web::middleware::{self, Next}; +/// +/// async fn my_mw( +/// req: ServiceRequest, +/// next: Next, +/// ) -> Result, Error> { +/// // pre-processing +/// next.call(req).await +/// // post-processing +/// } +/// # App::new().wrap(middleware::from_fn(my_mw)); +/// ``` +/// +/// Then use in an app builder like this: +/// +/// ``` +/// use actix_web::{ +/// App, Error, +/// dev::{ServiceRequest, ServiceResponse, Service as _}, +/// }; +/// use actix_web::middleware::from_fn; +/// # use actix_web::middleware::Next; +/// # async fn my_mw(req: ServiceRequest, next: Next) -> Result, Error> { +/// # next.call(req).await +/// # } +/// +/// App::new() +/// .wrap(from_fn(my_mw)) +/// # ; +/// ``` +/// +/// It is also possible to write a middleware that automatically uses extractors, similar to request +/// handlers, by declaring them as the first parameters. As usual, **take care with extractors that +/// consume the body stream**, since handlers will no longer be able to read it again without +/// putting the body "back" into the request object within your middleware. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use actix_web::{ +/// # App, Error, +/// # body::MessageBody, +/// # dev::{ServiceRequest, ServiceResponse}, +/// # http::header::{Accept, Date}, +/// # web::{Header, Query}, +/// # }; +/// use actix_web::middleware::Next; +/// +/// async fn my_extracting_mw( +/// accept: Header, +/// query: Query>, +/// req: ServiceRequest, +/// next: Next, +/// ) -> Result, Error> { +/// // pre-processing +/// next.call(req).await +/// // post-processing +/// } +/// # App::new().wrap(actix_web::middleware::from_fn(my_extracting_mw)); +pub fn from_fn(mw_fn: F) -> MiddlewareFn { + MiddlewareFn { + mw_fn: Rc::new(mw_fn), + _phantom: PhantomData, + } +} + +/// Middleware transform for [`from_fn`]. +#[allow(missing_debug_implementations)] +pub struct MiddlewareFn { + mw_fn: Rc, + _phantom: PhantomData, +} + +impl Transform for MiddlewareFn +where + S: Service, Error = Error> + 'static, + F: Fn(ServiceRequest, Next) -> Fut + 'static, + Fut: Future, Error>>, + B2: MessageBody, +{ + type Response = ServiceResponse; + type Error = Error; + type Transform = MiddlewareFnService; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(MiddlewareFnService { + service: boxed::rc_service(service), + mw_fn: Rc::clone(&self.mw_fn), + _phantom: PhantomData, + })) + } +} + +/// Middleware service for [`from_fn`]. +#[allow(missing_debug_implementations)] +pub struct MiddlewareFnService { + service: RcService, Error>, + mw_fn: Rc, + _phantom: PhantomData<(B, Es)>, +} + +impl Service for MiddlewareFnService +where + F: Fn(ServiceRequest, Next) -> Fut, + Fut: Future, Error>>, + B2: MessageBody, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = Fut; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + (self.mw_fn)( + req, + Next:: { + service: Rc::clone(&self.service), + }, + ) + } +} + +macro_rules! impl_middleware_fn_service { + ($($ext_type:ident),*) => { + impl Transform for MiddlewareFn + where + S: Service, Error = Error> + 'static, + F: Fn($($ext_type),*, ServiceRequest, Next) -> Fut + 'static, + $($ext_type: FromRequest + 'static,)* + Fut: Future, Error>> + 'static, + B: MessageBody + 'static, + B2: MessageBody + 'static, + { + type Response = ServiceResponse; + type Error = Error; + type Transform = MiddlewareFnService; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(MiddlewareFnService { + service: boxed::rc_service(service), + mw_fn: Rc::clone(&self.mw_fn), + _phantom: PhantomData, + })) + } + } + + impl Service + for MiddlewareFnService + where + F: Fn( + $($ext_type),*, + ServiceRequest, + Next + ) -> Fut + 'static, + $($ext_type: FromRequest + 'static,)* + Fut: Future, Error>> + 'static, + B2: MessageBody + 'static, + { + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + #[allow(nonstandard_style)] + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let mw_fn = Rc::clone(&self.mw_fn); + let service = Rc::clone(&self.service); + + Box::pin(async move { + let ($($ext_type,)*) = req.extract::<($($ext_type,)*)>().await?; + + (mw_fn)($($ext_type),*, req, Next:: { service }).await + }) + } + } + }; +} + +impl_middleware_fn_service!(E1); +impl_middleware_fn_service!(E1, E2); +impl_middleware_fn_service!(E1, E2, E3); +impl_middleware_fn_service!(E1, E2, E3, E4); +impl_middleware_fn_service!(E1, E2, E3, E4, E5); +impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6); +impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7); +impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7, E8); +impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7, E8, E9); + +/// Wraps the "next" service in the middleware chain. +#[allow(missing_debug_implementations)] +pub struct Next { + service: RcService, Error>, +} + +impl Next { + /// Equivalent to `Service::call(self, req)`. + pub fn call(&self, req: ServiceRequest) -> >::Future { + Service::call(self, req) + } +} + +impl Service for Next { + type Response = ServiceResponse; + type Error = Error; + type Future = BoxFuture>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + self.service.call(req) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + http::header::{self, HeaderValue}, + middleware::{Compat, Logger}, + test, web, App, HttpResponse, + }; + + async fn noop(req: ServiceRequest, next: Next) -> Result, Error> { + next.call(req).await + } + + async fn add_res_header( + req: ServiceRequest, + next: Next, + ) -> Result, Error> { + let mut res = next.call(req).await?; + res.headers_mut() + .insert(header::WARNING, HeaderValue::from_static("42")); + Ok(res) + } + + async fn mutate_body_type( + req: ServiceRequest, + next: Next, + ) -> Result, Error> { + let res = next.call(req).await?; + Ok(res.map_into_left_body::<()>()) + } + + struct MyMw(bool); + + impl MyMw { + async fn mw_cb( + &self, + req: ServiceRequest, + next: Next, + ) -> Result, Error> { + let mut res = match self.0 { + true => req.into_response("short-circuited").map_into_right_body(), + false => next.call(req).await?.map_into_left_body(), + }; + res.headers_mut() + .insert(header::WARNING, HeaderValue::from_static("42")); + Ok(res) + } + + pub fn into_middleware( + self, + ) -> impl Transform< + S, + ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + > + where + S: Service, Error = Error> + 'static, + B: MessageBody + 'static, + { + let this = Rc::new(self); + from_fn(move |req, next| { + let this = Rc::clone(&this); + async move { Self::mw_cb(&this, req, next).await } + }) + } + } + + #[actix_rt::test] + async fn compat_compat() { + let _ = App::new().wrap(Compat::new(from_fn(noop))); + let _ = App::new().wrap(Compat::new(from_fn(mutate_body_type))); + } + + #[actix_rt::test] + async fn permits_different_in_and_out_body_types() { + let app = test::init_service( + App::new() + .wrap(from_fn(mutate_body_type)) + .wrap(from_fn(add_res_header)) + .wrap(Logger::default()) + .wrap(from_fn(noop)) + .default_service(web::to(HttpResponse::NotFound)), + ) + .await; + + let req = test::TestRequest::default().to_request(); + let res = test::call_service(&app, req).await; + assert!(res.headers().contains_key(header::WARNING)); + } + + #[actix_rt::test] + async fn closure_capture_and_return_from_fn() { + let app = test::init_service( + App::new() + .wrap(Logger::default()) + .wrap(MyMw(true).into_middleware()) + .wrap(Logger::default()), + ) + .await; + + let req = test::TestRequest::default().to_request(); + let res = test::call_service(&app, req).await; + assert!(res.headers().contains_key(header::WARNING)); + } +} diff --git a/actix-web/src/middleware/logger.rs b/actix-web/src/middleware/logger.rs index 1e0a23058..5a3e05967 100644 --- a/actix-web/src/middleware/logger.rs +++ b/actix-web/src/middleware/logger.rs @@ -303,7 +303,7 @@ where ready(Ok(LoggerMiddleware { service, - inner: self.0.clone(), + inner: Rc::clone(&self.0), })) } } @@ -657,11 +657,7 @@ impl FormatText { FormatText::ResponseHeader(ref name) => { let s = if let Some(val) = res.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } + val.to_str().unwrap_or("-") } else { "-" }; @@ -705,11 +701,7 @@ impl FormatText { FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()), FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } + val.to_str().unwrap_or("-") } else { "-" }; @@ -747,7 +739,7 @@ impl FormatText { /// Converter to get a String from something that writes to a Formatter. pub(crate) struct FormatDisplay<'a>(&'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>); -impl<'a> fmt::Display for FormatDisplay<'a> { +impl fmt::Display for FormatDisplay<'_> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { (self.0)(fmt) } diff --git a/actix-web/src/middleware/mod.rs b/actix-web/src/middleware/mod.rs index 1c27b1110..4b5b3e896 100644 --- a/actix-web/src/middleware/mod.rs +++ b/actix-web/src/middleware/mod.rs @@ -15,10 +15,47 @@ //! - Access external services (e.g., [sessions](https://docs.rs/actix-session), etc.) //! //! Middleware is registered for each [`App`], [`Scope`](crate::Scope), or -//! [`Resource`](crate::Resource) and executed in opposite order as registration. In general, a -//! middleware is a pair of types that implements the [`Service`] trait and [`Transform`] trait, -//! respectively. The [`new_transform`] and [`call`] methods must return a [`Future`], though it -//! can often be [an immediately-ready one](actix_utils::future::Ready). +//! [`Resource`](crate::Resource) and executed in opposite order as registration. +//! +//! # Simple Middleware +//! +//! In many cases, you can model your middleware as an async function via the [`from_fn()`] helper +//! that provides a natural interface for implementing your desired behaviors. +//! +//! ``` +//! # use actix_web::{ +//! # App, Error, +//! # body::MessageBody, +//! # dev::{ServiceRequest, ServiceResponse, Service as _}, +//! # }; +//! use actix_web::middleware::{self, Next}; +//! +//! async fn my_mw( +//! req: ServiceRequest, +//! next: Next, +//! ) -> Result, Error> { +//! // pre-processing +//! +//! // invoke the wrapped middleware or service +//! let res = next.call(req).await?; +//! +//! // post-processing +//! +//! Ok(res) +//! } +//! +//! App::new() +//! .wrap(middleware::from_fn(my_mw)); +//! ``` +//! +//! ## Complex Middleware +//! +//! In the more general ase, a middleware is a pair of types that implements the [`Service`] trait +//! and [`Transform`] trait, respectively. The [`new_transform`] and [`call`] methods must return a +//! [`Future`], though it can often be [an immediately-ready one](actix_utils::future::Ready). +//! +//! All the built-in middleware use this pattern with pairs of builder (`Transform`) + +//! implementation (`Service`) types. //! //! # Ordering //! @@ -67,7 +104,7 @@ //! Response //! ``` //! The request _first_ gets processed by the middleware specified _last_ - `MiddlewareC`. It passes -//! the request (modified a modified one) to the next middleware - `MiddlewareB` - _or_ directly +//! the request (possibly a modified one) to the next middleware - `MiddlewareB` - _or_ directly //! responds to the request (e.g. when the request was invalid or an error occurred). `MiddlewareB` //! processes the request as well and passes it to `MiddlewareA`, which then passes it to the //! [`Service`]. In the [`Service`], the extractors will run first. They don't pass the request on, @@ -196,18 +233,6 @@ //! # } //! ``` //! -//! # Simpler Middleware -//! -//! In many cases, you _can_ actually use an async function via a helper that will provide a more -//! natural flow for your behavior. -//! -//! The experimental `actix_web_lab` crate provides a [`from_fn`][lab_from_fn] utility which allows -//! an async fn to be wrapped and used in the same way as other middleware. See the -//! [`from_fn`][lab_from_fn] docs for more info and examples of it's use. -//! -//! While [`from_fn`][lab_from_fn] is experimental currently, it's likely this helper will graduate -//! to Actix Web in some form, so feedback is appreciated. -//! //! [`Future`]: std::future::Future //! [`App`]: crate::App //! [`FromRequest`]: crate::FromRequest @@ -215,7 +240,7 @@ //! [`Transform`]: crate::dev::Transform //! [`call`]: crate::dev::Service::call() //! [`new_transform`]: crate::dev::Transform::new_transform() -//! [lab_from_fn]: https://docs.rs/actix-web-lab/latest/actix_web_lab/middleware/fn.from_fn.html +//! [`from_fn`]: crate mod compat; #[cfg(feature = "__compress")] @@ -223,6 +248,7 @@ mod compress; mod condition; mod default_headers; mod err_handlers; +mod from_fn; mod identity; mod logger; mod normalize; @@ -234,6 +260,7 @@ pub use self::{ condition::Condition, default_headers::DefaultHeaders, err_handlers::{ErrorHandlerResponse, ErrorHandlers}, + from_fn::{from_fn, Next}, identity::Identity, logger::Logger, normalize::{NormalizePath, TrailingSlash}, diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index 00555b7b2..aee0dff93 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -28,9 +28,9 @@ use crate::{ /// /// Resource in turn has at least one route. Route consists of an handlers objects and list of /// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern -/// for configuration. During request handling, resource object iterate through all routes and check -/// guards for specific route, if request matches all guards, route considered matched and route -/// handler get called. +/// for configuration. During request handling, the resource object iterates through all routes +/// and checks guards for the specific route, if the request matches all the guards, then the route +/// is considered matched and the route handler gets called. /// /// # Examples /// ``` @@ -62,14 +62,14 @@ pub struct Resource { impl Resource { /// Constructs new resource that matches a `path` pattern. pub fn new(path: T) -> Resource { - let fref = Rc::new(RefCell::new(None)); + let factory_ref = Rc::new(RefCell::new(None)); Resource { routes: Vec::new(), rdef: path.patterns(), name: None, - endpoint: ResourceEndpoint::new(fref.clone()), - factory_ref: fref, + endpoint: ResourceEndpoint::new(Rc::clone(&factory_ref)), + factory_ref, guards: Vec::new(), app_data: None, default: boxed::factory(fn_service(|req: ServiceRequest| async { @@ -358,10 +358,9 @@ where U::InitError: fmt::Debug, { // create and configure default resource - self.default = boxed::factory( - f.into_factory() - .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)), - ); + self.default = boxed::factory(f.into_factory().map_init_err(|err| { + log::error!("Can not construct default service: {err:?}"); + })); self } diff --git a/actix-web/src/response/builder.rs b/actix-web/src/response/builder.rs index 023842ee5..c23de8e36 100644 --- a/actix-web/src/response/builder.rs +++ b/actix-web/src/response/builder.rs @@ -463,7 +463,7 @@ mod tests { // content type override let res = HttpResponse::Ok() .insert_header((CONTENT_TYPE, "text/json")) - .json(&vec!["v1", "v2", "v3"]); + .json(["v1", "v2", "v3"]); let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("text/json")); assert_body_eq!(res, br#"["v1","v2","v3"]"#); diff --git a/actix-web/src/response/responder.rs b/actix-web/src/response/responder.rs index 90d8f6e52..82bc38d6b 100644 --- a/actix-web/src/response/responder.rs +++ b/actix-web/src/response/responder.rs @@ -131,6 +131,23 @@ where } } +// Note: see https://github.com/actix/actix-web/issues/1108 for reasoning why Responder is not +// implemented for `()`, and https://github.com/actix/actix-web/pull/3560 for discussion about this +// impl and the decision not to include a similar one for `Option<()>`. +impl Responder for Result<(), E> +where + E: Into, +{ + type Body = BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + match self { + Ok(()) => HttpResponse::new(StatusCode::NO_CONTENT), + Err(err) => HttpResponse::from_error(err.into()), + } + } +} + impl Responder for (R, StatusCode) { type Body = R::Body; diff --git a/actix-web/src/rmap.rs b/actix-web/src/rmap.rs index 462f3b313..b445687ac 100644 --- a/actix-web/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -6,7 +6,7 @@ use std::{ }; use actix_router::ResourceDef; -use ahash::AHashMap; +use foldhash::HashMap as FoldHashMap; use url::Url; use crate::{error::UrlGenerationError, request::HttpRequest}; @@ -19,7 +19,7 @@ pub struct ResourceMap { /// Named resources within the tree or, for external resources, it points to isolated nodes /// outside the tree. - named: AHashMap>, + named: FoldHashMap>, parent: RefCell>, @@ -32,7 +32,7 @@ impl ResourceMap { pub fn new(root: ResourceDef) -> Self { ResourceMap { pattern: root, - named: AHashMap::default(), + named: FoldHashMap::default(), parent: RefCell::new(Weak::new()), nodes: Some(Vec::new()), } @@ -86,7 +86,7 @@ impl ResourceMap { } else { let new_node = Rc::new(ResourceMap { pattern: pattern.clone(), - named: AHashMap::default(), + named: FoldHashMap::default(), parent: RefCell::new(Weak::new()), nodes: None, }); diff --git a/actix-web/src/route.rs b/actix-web/src/route.rs index 261e6b9ae..e05e6be52 100644 --- a/actix-web/src/route.rs +++ b/actix-web/src/route.rs @@ -77,7 +77,7 @@ impl ServiceFactory for Route { fn new_service(&self, _: ()) -> Self::Future { let fut = self.service.new_service(()); - let guards = self.guards.clone(); + let guards = Rc::clone(&self.guards); Box::pin(async move { let service = fut.await?; diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs index adc9f75d3..e317349da 100644 --- a/actix-web/src/scope.rs +++ b/actix-web/src/scope.rs @@ -213,7 +213,6 @@ where /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. - /// * "StaticFiles" is a service for static files support /// /// ``` /// use actix_web::{web, App, HttpRequest}; @@ -279,7 +278,9 @@ where { // create and configure default resource self.default = Some(Rc::new(boxed::factory(f.into_factory().map_init_err( - |e| log::error!("Can not construct default service: {:?}", e), + |err| { + log::error!("Can not construct default service: {err:?}"); + }, )))); self diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index 33b1e1894..1ea4de4ca 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -7,13 +7,7 @@ use std::{ time::Duration, }; -#[cfg(any( - feature = "openssl", - feature = "rustls-0_20", - feature = "rustls-0_21", - feature = "rustls-0_22", - feature = "rustls-0_23", -))] +#[cfg(feature = "__tls")] use actix_http::TlsAcceptorConfig; use actix_http::{body::MessageBody, Extensions, HttpService, KeepAlive, Request, Response}; use actix_server::{Server, ServerBuilder}; @@ -190,7 +184,7 @@ where /// By default max connections is set to a 256. #[allow(unused_variables)] pub fn max_connection_rate(self, num: usize) -> Self { - #[cfg(any(feature = "rustls-0_20", feature = "rustls-0_21", feature = "openssl"))] + #[cfg(feature = "__tls")] actix_tls::accept::max_concurrent_tls_connect(num); self } @@ -199,7 +193,7 @@ where /// /// One thread pool is set up **per worker**; not shared across workers. /// - /// By default set to 512 divided by the number of workers. + /// By default, set to 512 divided by [available parallelism](std::thread::available_parallelism()). pub fn worker_max_blocking_threads(mut self, num: usize) -> Self { self.builder = self.builder.worker_max_blocking_threads(num); self @@ -243,13 +237,7 @@ where /// time, the connection is closed. /// /// By default, the handshake timeout is 3 seconds. - #[cfg(any( - feature = "openssl", - feature = "rustls-0_20", - feature = "rustls-0_21", - feature = "rustls-0_22", - feature = "rustls-0_23", - ))] + #[cfg(feature = "__tls")] pub fn tls_handshake_timeout(self, dur: Duration) -> Self { self.config .lock() @@ -522,7 +510,7 @@ where /// No changes are made to `lst`'s configuration. Ensure it is configured properly before /// passing ownership to `listen()`. pub fn listen(mut self, lst: net::TcpListener) -> io::Result { - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let addr = lst.local_addr().unwrap(); @@ -566,7 +554,7 @@ where /// Binds to existing listener for accepting incoming plaintext HTTP/1.x or HTTP/2 connections. #[cfg(feature = "http2")] pub fn listen_auto_h2c(mut self, lst: net::TcpListener) -> io::Result { - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let addr = lst.local_addr().unwrap(); @@ -644,7 +632,7 @@ where config: actix_tls::accept::rustls_0_20::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, @@ -695,7 +683,7 @@ where config: actix_tls::accept::rustls_0_21::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, @@ -761,7 +749,7 @@ where config: actix_tls::accept::rustls_0_22::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, @@ -827,7 +815,7 @@ where config: actix_tls::accept::rustls_0_23::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, @@ -892,7 +880,7 @@ where acceptor: SslAcceptor, ) -> io::Result { let factory = self.factory.clone(); - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, @@ -949,7 +937,7 @@ where use actix_rt::net::UnixStream; use actix_service::{fn_service, ServiceFactoryExt as _}; - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let socket_addr = net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); @@ -994,7 +982,7 @@ where use actix_rt::net::UnixStream; use actix_service::{fn_service, ServiceFactoryExt as _}; - let cfg = self.config.clone(); + let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let socket_addr = net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); @@ -1097,7 +1085,10 @@ fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result {()}; ($($x:expr),+ $(,)?) => { ($($x,)+) } @@ -870,4 +871,40 @@ mod tests { let req = test::TestRequest::default().to_request(); let _res = test::call_service(&app, req).await; } + + #[test] + fn define_services_macro_with_multiple_arguments() { + let result = services!(1, 2, 3); + assert_eq!(result, (1, 2, 3)); + } + + #[test] + fn define_services_macro_with_single_argument() { + let result = services!(1); + assert_eq!(result, (1,)); + } + + #[test] + fn define_services_macro_with_no_arguments() { + let result = services!(); + let () = result; + } + + #[test] + fn define_services_macro_with_trailing_comma() { + let result = services!(1, 2, 3,); + assert_eq!(result, (1, 2, 3)); + } + + #[test] + fn define_services_macro_with_comments_in_arguments() { + let result = services!( + 1, // First comment + 2, // Second comment + 3 // Third comment + ); + + // Assert that comments are ignored and it correctly returns a tuple. + assert_eq!(result, (1, 2, 3)); + } } diff --git a/actix-web/src/test/mod.rs b/actix-web/src/test/mod.rs index 5e647956b..86cb60956 100644 --- a/actix-web/src/test/mod.rs +++ b/actix-web/src/test/mod.rs @@ -1,6 +1,6 @@ //! Various helpers for Actix applications to use during testing. //! -//! # Creating A Test Service +//! # Initializing A Test Service //! - [`init_service`] //! //! # Off-The-Shelf Test Services @@ -49,6 +49,7 @@ pub use self::{ /// Must be used inside an async test. Works for both `ServiceRequest` and `HttpRequest`. /// /// # Examples +/// /// ``` /// use actix_web::{http::StatusCode, HttpResponse}; /// diff --git a/actix-web/src/thin_data.rs b/actix-web/src/thin_data.rs new file mode 100644 index 000000000..a9cd4e3a4 --- /dev/null +++ b/actix-web/src/thin_data.rs @@ -0,0 +1,121 @@ +use std::any::type_name; + +use actix_utils::future::{ready, Ready}; + +use crate::{dev::Payload, error, FromRequest, HttpRequest}; + +/// Application data wrapper and extractor for cheaply-cloned types. +/// +/// Similar to the [`Data`] wrapper but for `Clone`/`Copy` types that are already an `Arc` internally, +/// share state using some other means when cloned, or is otherwise static data that is very cheap +/// to clone. +/// +/// Unlike `Data`, this wrapper clones `T` during extraction. Therefore, it is the user's +/// responsibility to ensure that clones of `T` do actually share the same state, otherwise state +/// may be unexpectedly different across multiple requests. +/// +/// Note that if your type is literally an `Arc` then it's recommended to use the +/// [`Data::from(arc)`][data_from_arc] conversion instead. +/// +/// # Examples +/// +/// ``` +/// use actix_web::{ +/// web::{self, ThinData}, +/// App, HttpResponse, Responder, +/// }; +/// +/// // Use the `ThinData` extractor to access a database connection pool. +/// async fn index(ThinData(db_pool): ThinData) -> impl Responder { +/// // database action ... +/// +/// HttpResponse::Ok() +/// } +/// +/// # type DbPool = (); +/// let db_pool = DbPool::default(); +/// +/// App::new() +/// .app_data(ThinData(db_pool.clone())) +/// .service(web::resource("/").get(index)) +/// # ; +/// ``` +/// +/// [`Data`]: crate::web::Data +/// [data_from_arc]: crate::web::Data#impl-From>-for-Data +#[derive(Debug, Clone)] +pub struct ThinData(pub T); + +impl_more::impl_as_ref!(ThinData => T); +impl_more::impl_as_mut!(ThinData => T); +impl_more::impl_deref_and_mut!( in ThinData => T); + +impl FromRequest for ThinData { + type Error = crate::Error; + type Future = Ready>; + + #[inline] + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ready(req.app_data::().cloned().ok_or_else(|| { + log::debug!( + "Failed to extract `ThinData<{}>` for `{}` handler. For the ThinData extractor to work \ + correctly, wrap the data with `ThinData()` and pass it to `App::app_data()`. \ + Ensure that types align in both the set and retrieve calls.", + type_name::(), + req.match_name().unwrap_or(req.path()) + ); + + error::ErrorInternalServerError( + "Requested application data is not configured correctly. \ + View/enable debug logs for more details.", + ) + })) + } +} + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Mutex}; + + use super::*; + use crate::{ + http::StatusCode, + test::{call_service, init_service, TestRequest}, + web, App, HttpResponse, + }; + + type TestT = Arc>; + + #[actix_rt::test] + async fn thin_data() { + let test_data = TestT::default(); + + let app = init_service(App::new().app_data(ThinData(test_data.clone())).service( + web::resource("/").to(|td: ThinData| { + *td.lock().unwrap() += 1; + HttpResponse::Ok() + }), + )) + .await; + + for _ in 0..3 { + let req = TestRequest::default().to_request(); + let resp = call_service(&app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + } + + assert_eq!(*test_data.lock().unwrap(), 3); + } + + #[actix_rt::test] + async fn thin_data_missing() { + let app = init_service( + App::new().service(web::resource("/").to(|_: ThinData| HttpResponse::Ok())), + ) + .await; + + let req = TestRequest::default().to_request(); + let resp = call_service(&app, req).await; + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } +} diff --git a/actix-web/src/types/either.rs b/actix-web/src/types/either.rs index 7883e89f6..d8b3f1180 100644 --- a/actix-web/src/types/either.rs +++ b/actix-web/src/types/either.rs @@ -238,7 +238,7 @@ where match res { Ok(bytes) => { let fallback = bytes.clone(); - let left = L::from_request(this.req, &mut payload_from_bytes(bytes)); + let left = L::from_request(this.req, &mut dev::Payload::from(bytes)); EitherExtractState::Left { left, fallback } } Err(err) => break Err(EitherExtractError::Bytes(err)), @@ -251,7 +251,7 @@ where Err(left_err) => { let right = R::from_request( this.req, - &mut payload_from_bytes(mem::take(fallback)), + &mut dev::Payload::from(mem::take(fallback)), ); EitherExtractState::Right { left_err: Some(left_err), @@ -276,12 +276,6 @@ where } } -fn payload_from_bytes(bytes: Bytes) -> dev::Payload { - let (_, mut h1_payload) = actix_http::h1::Payload::create(true); - h1_payload.unread_data(bytes); - dev::Payload::from(h1_payload) -} - #[cfg(test)] mod tests { use serde::{Deserialize, Serialize}; diff --git a/actix-web/src/types/json.rs b/actix-web/src/types/json.rs index 6b75c0cfe..22ed624c3 100644 --- a/actix-web/src/types/json.rs +++ b/actix-web/src/types/json.rs @@ -332,7 +332,7 @@ impl JsonBody { (true, Ok(Some(mime))) => { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - || ctype_fn.map_or(false, |predicate| predicate(mime)) + || ctype_fn.is_some_and(|predicate| predicate(mime)) } // if content-type is expected but not parsable as mime type, bail @@ -398,7 +398,7 @@ impl JsonBody { _res: PhantomData, } } - JsonBody::Error(e) => JsonBody::Error(e), + JsonBody::Error(err) => JsonBody::Error(err), } } } @@ -434,7 +434,7 @@ impl Future for JsonBody { } } }, - JsonBody::Error(e) => Poll::Ready(Err(e.take().unwrap())), + JsonBody::Error(err) => Poll::Ready(Err(err.take().unwrap())), } } } diff --git a/actix-web/src/types/path.rs b/actix-web/src/types/path.rs index cc87bb80f..5f22568cc 100644 --- a/actix-web/src/types/path.rs +++ b/actix-web/src/types/path.rs @@ -89,8 +89,8 @@ where ); if let Some(error_handler) = error_handler { - let e = PathError::Deserialize(err); - (error_handler)(e, req) + let err = PathError::Deserialize(err); + (error_handler)(err, req) } else { ErrorNotFound(err) } @@ -159,7 +159,7 @@ mod tests { use crate::{error, http, test::TestRequest, HttpResponse}; #[derive(Deserialize, Debug, Display)] - #[display(fmt = "MyStruct({}, {})", key, value)] + #[display("MyStruct({}, {})", key, value)] struct MyStruct { key: String, value: String, diff --git a/actix-web/src/types/query.rs b/actix-web/src/types/query.rs index e71b886f2..e5505131e 100644 --- a/actix-web/src/types/query.rs +++ b/actix-web/src/types/query.rs @@ -2,7 +2,7 @@ use std::{fmt, ops, sync::Arc}; -use actix_utils::future::{err, ok, Ready}; +use actix_utils::future::{ok, ready, Ready}; use serde::de::DeserializeOwned; use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest}; @@ -118,8 +118,8 @@ impl FromRequest for Query { serde_urlencoded::from_str::(req.query_string()) .map(|val| ok(Query(val))) - .unwrap_or_else(move |e| { - let e = QueryPayloadError::Deserialize(e); + .unwrap_or_else(move |err| { + let err = QueryPayloadError::Deserialize(err); log::debug!( "Failed during Query extractor deserialization. \ @@ -127,13 +127,13 @@ impl FromRequest for Query { req.path() ); - let e = if let Some(error_handler) = error_handler { - (error_handler)(e, req) + let err = if let Some(error_handler) = error_handler { + (error_handler)(err, req) } else { - e.into() + err.into() }; - err(e) + ready(Err(err)) }) } } diff --git a/actix-web/src/web.rs b/actix-web/src/web.rs index 204313752..3a4c46730 100644 --- a/actix-web/src/web.rs +++ b/actix-web/src/web.rs @@ -2,6 +2,7 @@ //! //! # Request Extractors //! - [`Data`]: Application data item +//! - [`ThinData`]: Cheap-to-clone application data item //! - [`ReqData`]: Request-local data item //! - [`Path`]: URL path parameters / dynamic segments //! - [`Query`]: URL query parameters @@ -22,7 +23,8 @@ use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; pub use crate::{ - config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData, types::*, + config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData, + thin_data::ThinData, types::*, }; use crate::{ error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource, diff --git a/actix-web/tests/test_httpserver.rs b/actix-web/tests/test_httpserver.rs index 039c0ffbc..5fd7d7190 100644 --- a/actix-web/tests/test_httpserver.rs +++ b/actix-web/tests/test_httpserver.rs @@ -1,13 +1,10 @@ #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -#[cfg(any(unix, feature = "openssl"))] -use { - actix_web::{web, App, HttpResponse, HttpServer}, - std::{sync::mpsc, thread, time::Duration}, -}; +use std::{sync::mpsc, thread, time::Duration}; + +use actix_web::{web, App, HttpResponse, HttpServer}; -#[cfg(unix)] #[actix_rt::test] async fn test_start() { let addr = actix_test::unused_addr(); @@ -53,6 +50,27 @@ async fn test_start() { let response = client.get(host.clone()).send().await.unwrap(); assert!(response.status().is_success()); + // Attempt to start a second server using the same address. + let result = HttpServer::new(|| { + App::new().service( + web::resource("/").route(web::to(|| async { HttpResponse::Ok().body("test") })), + ) + }) + .workers(1) + .backlog(1) + .max_connections(10) + .max_connection_rate(10) + .keep_alive(Duration::from_secs(10)) + .client_request_timeout(Duration::from_secs(5)) + .client_disconnect_timeout(Duration::ZERO) + .server_hostname("localhost") + .system_exit() + .disable_signals() + .bind(format!("{}", addr)); + + // This should fail: the address is in use. + assert!(result.is_err()); + srv.stop(false).await; } diff --git a/actix-web/tests/test_server.rs b/actix-web/tests/test_server.rs index 960cf1e2b..f13aa3cfd 100644 --- a/actix-web/tests/test_server.rs +++ b/actix-web/tests/test_server.rs @@ -25,7 +25,7 @@ use openssl::{ ssl::{SslAcceptor, SslMethod}, x509::X509, }; -use rand::{distributions::Alphanumeric, Rng as _}; +use rand::distr::{Alphanumeric, SampleString as _}; mod utils; @@ -188,11 +188,7 @@ async fn body_gzip_large() { #[actix_rt::test] async fn test_body_gzip_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(70_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 70_000); let srv_data = data.clone(); let srv = actix_test::start_with(actix_test::config().h1(), move || { @@ -432,11 +428,7 @@ async fn test_zstd_encoding() { #[actix_rt::test] async fn test_zstd_encoding_large() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(320_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 320_000); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( @@ -529,11 +521,7 @@ async fn test_gzip_encoding_large() { #[actix_rt::test] async fn test_reading_gzip_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(60_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 60_000); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { @@ -599,11 +587,7 @@ async fn test_reading_deflate_encoding_large() { #[actix_rt::test] async fn test_reading_deflate_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 160_000); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { @@ -648,11 +632,7 @@ async fn test_brotli_encoding() { #[actix_rt::test] async fn test_brotli_encoding_large() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(320_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 320_000); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( @@ -737,11 +717,7 @@ mod plus_rustls { #[actix_rt::test] async fn test_reading_deflate_encoding_large_random_rustls() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 160_000); let srv = actix_test::start_with(actix_test::config().rustls_0_23(tls_config()), || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| async { diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 54c5e9869..d3fd7147b 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -2,11 +2,22 @@ ## Unreleased +## 3.6.0 + +- Prevent panics on connection pool drop when Tokio runtime is shutdown early. +- Do not send `Host` header on HTTP/2 requests, as it is not required, and some web servers may reject it. +- Update `brotli` dependency to `7`. +- Minimum supported Rust version (MSRV) is now 1.75. + +## 3.5.1 + +- Fix WebSocket `Host` request header value when using a non-default port. + ## 3.5.0 - Add `rustls-0_23`, `rustls-0_23-webpki-roots`, and `rustls-0_23-native-roots` crate features. - Add `awc::Connector::rustls_0_23()` constructor. -- Fix `rustls-0_22-native-roots` root store lookup +- Fix `rustls-0_22-native-roots` root store lookup. - Update `brotli` dependency to `6`. - Minimum supported Rust version (MSRV) is now 1.72. diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 4fc2057f6..106def621 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.5.0" +version = "3.6.0" authors = ["Nikolay Kim "] description = "Async HTTP and WebSocket client library" keywords = ["actix", "http", "framework", "async", "web"] @@ -98,7 +98,7 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.5" actix-service = "2" -actix-http = { version = "3.7", features = ["http2", "ws"] } +actix-http = { version = "3.10", features = ["http2", "ws"] } actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3.4", features = ["connect", "uri"] } actix-utils = "3" @@ -106,17 +106,17 @@ actix-utils = "3" base64 = "0.22" bytes = "1" cfg-if = "1" -derive_more = "0.99.5" +derive_more = { version = "2", features = ["display", "error", "from"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] } h2 = "0.3.26" http = "0.2.7" itoa = "1" -log =" 0.4" +log ="0.4" mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" -rand = "0.8" +rand = "0.9" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" @@ -141,7 +141,7 @@ actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23"] } actix-utils = "3" actix-web = { version = "4", features = ["openssl"] } -brotli = "6" +brotli = "7" const-str = "0.5" env_logger = "0.11" flate2 = "1.0.13" @@ -156,3 +156,6 @@ tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 wit [[example]] name = "client" required-features = ["rustls-0_23-webpki-roots"] + +[lints] +workspace = true diff --git a/awc/README.md b/awc/README.md index 8e7b42812..954cc2803 100644 --- a/awc/README.md +++ b/awc/README.md @@ -5,9 +5,9 @@ [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.5.0)](https://docs.rs/awc/3.5.0) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.6.0)](https://docs.rs/awc/3.6.0) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.5.0/status.svg)](https://deps.rs/crate/awc/3.5.0) +[![Dependency Status](https://deps.rs/crate/awc/3.6.0/status.svg)](https://deps.rs/crate/awc/3.6.0) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/awc/examples/client.rs b/awc/examples/client.rs index 16ad330b8..b6eb919c7 100644 --- a/awc/examples/client.rs +++ b/awc/examples/client.rs @@ -1,23 +1,39 @@ -use std::error::Error as StdError; +//! Demonstrates construction and usage of a TLS-capable HTTP client. -#[tokio::main] +extern crate tls_rustls_0_23 as rustls; + +use std::{error::Error as StdError, sync::Arc}; + +use actix_tls::connect::rustls_0_23::webpki_roots_cert_store; +use rustls::ClientConfig; + +#[actix_rt::main] async fn main() -> Result<(), Box> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - // construct request builder - let client = awc::Client::new(); + let mut config = ClientConfig::builder() + .with_root_certificates(webpki_roots_cert_store()) + .with_no_client_auth(); + + let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + config.alpn_protocols = protos; + + // construct request builder with TLS support + let client = awc::Client::builder() + .connector(awc::Connector::new().rustls_0_23(Arc::new(config))) + .finish(); // configure request let request = client .get("https://www.rust-lang.org/") - .append_header(("User-Agent", "Actix-web")); + .append_header(("User-Agent", "awc/3.0")); - println!("Request: {:?}", request); + println!("Request: {request:?}"); let mut response = request.send().await?; // server response head - println!("Response: {:?}", response); + println!("Response: {response:?}"); // read response body let body = response.body().await?; diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs index 08f5cc25e..ef0edfb9e 100644 --- a/awc/src/any_body.rs +++ b/awc/src/any_body.rs @@ -163,6 +163,7 @@ mod tests { use super::*; + #[allow(dead_code)] struct PinType(PhantomPinned); impl MessageBody for PinType { diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index f3d443070..a7c606826 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -89,9 +89,9 @@ impl Connector<()> { /// # Panics /// /// - When the `rustls-0_23-webpki-roots` or `rustls-0_23-native-roots` features are enabled - /// and no default crypto provider has been loaded, this method will panic. + /// and no default crypto provider has been loaded, this method will panic. /// - When the `rustls-0_23-native-roots` or `rustls-0_22-native-roots` features are enabled - /// and the runtime system has no native root certificates, this method will panic. + /// and the runtime system has no native root certificates, this method will panic. #[allow(clippy::new_ret_no_self, clippy::let_unit_value)] pub fn new() -> Connector< impl Service< @@ -511,7 +511,8 @@ where let h2 = sock .ssl() .selected_alpn_protocol() - .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + .is_some_and(|protos| protos.windows(2).any(|w| w == H2)); + if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -550,7 +551,8 @@ where .get_ref() .1 .alpn_protocol() - .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + .is_some_and(|protos| protos.windows(2).any(|w| w == H2)); + if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -584,7 +586,8 @@ where .get_ref() .1 .alpn_protocol() - .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + .is_some_and(|protos| protos.windows(2).any(|w| w == H2)); + if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -621,7 +624,8 @@ where .get_ref() .1 .alpn_protocol() - .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + .is_some_and(|protos| protos.windows(2).any(|w| w == H2)); + if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -655,7 +659,8 @@ where .get_ref() .1 .alpn_protocol() - .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + .is_some_and(|protos| protos.windows(2).any(|w| w == H2)); + if h2 { (Box::new(sock), Protocol::Http2) } else { diff --git a/awc/src/client/error.rs b/awc/src/client/error.rs index d351e1067..6cb595770 100644 --- a/awc/src/client/error.rs +++ b/awc/src/client/error.rs @@ -12,40 +12,40 @@ use crate::BoxError; #[non_exhaustive] pub enum ConnectError { /// SSL feature is not enabled - #[display(fmt = "SSL is not supported")] + #[display("SSL is not supported")] SslIsNotSupported, /// SSL error #[cfg(feature = "openssl")] - #[display(fmt = "{}", _0)] + #[display("{}", _0)] SslError(OpensslError), /// Failed to resolve the hostname - #[display(fmt = "Failed resolving hostname: {}", _0)] + #[display("Failed resolving hostname: {}", _0)] Resolver(Box), /// No dns records - #[display(fmt = "No DNS records found for the input")] + #[display("No DNS records found for the input")] NoRecords, /// Http2 error - #[display(fmt = "{}", _0)] + #[display("{}", _0)] H2(h2::Error), /// Connecting took too long - #[display(fmt = "Timeout while establishing connection")] + #[display("Timeout while establishing connection")] Timeout, /// Connector has been disconnected - #[display(fmt = "Internal error: connector has been disconnected")] + #[display("Internal error: connector has been disconnected")] Disconnected, /// Unresolved host name - #[display(fmt = "Connector received `Connect` method with unresolved host")] + #[display("Connector received `Connect` method with unresolved host")] Unresolved, /// Connection io error - #[display(fmt = "{}", _0)] + #[display("{}", _0)] Io(io::Error), } @@ -54,11 +54,11 @@ impl std::error::Error for ConnectError {} impl From for ConnectError { fn from(err: actix_tls::connect::ConnectError) -> ConnectError { match err { - actix_tls::connect::ConnectError::Resolver(e) => ConnectError::Resolver(e), + actix_tls::connect::ConnectError::Resolver(err) => ConnectError::Resolver(err), actix_tls::connect::ConnectError::NoRecords => ConnectError::NoRecords, actix_tls::connect::ConnectError::InvalidInput => panic!(), actix_tls::connect::ConnectError::Unresolved => ConnectError::Unresolved, - actix_tls::connect::ConnectError::Io(e) => ConnectError::Io(e), + actix_tls::connect::ConnectError::Io(err) => ConnectError::Io(err), } } } @@ -66,16 +66,16 @@ impl From for ConnectError { #[derive(Debug, Display, From)] #[non_exhaustive] pub enum InvalidUrl { - #[display(fmt = "Missing URL scheme")] + #[display("Missing URL scheme")] MissingScheme, - #[display(fmt = "Unknown URL scheme")] + #[display("Unknown URL scheme")] UnknownScheme, - #[display(fmt = "Missing host name")] + #[display("Missing host name")] MissingHost, - #[display(fmt = "URL parse error: {}", _0)] + #[display("URL parse error: {}", _0)] HttpError(http::Error), } @@ -86,11 +86,11 @@ impl std::error::Error for InvalidUrl {} #[non_exhaustive] pub enum SendRequestError { /// Invalid URL - #[display(fmt = "Invalid URL: {}", _0)] + #[display("Invalid URL: {}", _0)] Url(InvalidUrl), /// Failed to connect to host - #[display(fmt = "Failed to connect to host: {}", _0)] + #[display("Failed to connect to host: {}", _0)] Connect(ConnectError), /// Error sending request @@ -100,26 +100,26 @@ pub enum SendRequestError { Response(ParseError), /// Http error - #[display(fmt = "{}", _0)] + #[display("{}", _0)] Http(HttpError), /// Http2 error - #[display(fmt = "{}", _0)] + #[display("{}", _0)] H2(h2::Error), /// Response took too long - #[display(fmt = "Timeout while waiting for response")] + #[display("Timeout while waiting for response")] Timeout, /// Tunnels are not supported for HTTP/2 connection - #[display(fmt = "Tunnels are not supported for http2 connection")] + #[display("Tunnels are not supported for http2 connection")] TunnelNotSupported, /// Error sending request body Body(BoxError), /// Other errors that can occur after submitting a request. - #[display(fmt = "{:?}: {}", _1, _0)] + #[display("{:?}: {}", _1, _0)] Custom(BoxError, Box), } @@ -130,15 +130,15 @@ impl std::error::Error for SendRequestError {} #[non_exhaustive] pub enum FreezeRequestError { /// Invalid URL - #[display(fmt = "Invalid URL: {}", _0)] + #[display("Invalid URL: {}", _0)] Url(InvalidUrl), /// HTTP error - #[display(fmt = "{}", _0)] + #[display("{}", _0)] Http(HttpError), /// Other errors that can occur after submitting a request. - #[display(fmt = "{:?}: {}", _1, _0)] + #[display("{:?}: {}", _1, _0)] Custom(BoxError, Box), } diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index c3f801f20..f12ac3b43 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -12,7 +12,7 @@ use h2::{ SendStream, }; use http::{ - header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}, + header::{HeaderValue, CONNECTION, CONTENT_LENGTH, HOST, TRANSFER_ENCODING}, request::Request, Method, Version, }; @@ -97,7 +97,7 @@ where // TODO: consider skipping other headers according to: // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 // omit HTTP/1.x only headers - CONNECTION | TRANSFER_ENCODING => continue, + CONNECTION | TRANSFER_ENCODING | HOST => continue, CONTENT_LENGTH if skip_len => continue, // DATE => has_date = true, _ => {} diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs index 2938353fd..5d764f729 100644 --- a/awc/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -31,7 +31,7 @@ use super::{ Connect, }; -#[derive(Hash, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Key { authority: Authority, } @@ -42,8 +42,8 @@ impl From for Key { } } +/// Connections pool to reuse I/O per [`Authority`]. #[doc(hidden)] -/// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key. pub struct ConnectionPool where Io: AsyncWrite + Unpin + 'static, @@ -52,7 +52,7 @@ where inner: ConnectionPoolInner, } -/// wrapper type for check the ref count of Rc. +/// Wrapper type for check the ref count of Rc. pub struct ConnectionPoolInner(Rc>) where Io: AsyncWrite + Unpin + 'static; @@ -63,7 +63,7 @@ where { fn new(config: ConnectorConfig) -> Self { let permits = Arc::new(Semaphore::new(config.limit)); - let available = RefCell::new(HashMap::default()); + let available = RefCell::new(HashMap::new()); Self(Rc::new(ConnectionPoolInnerPriv { config, @@ -72,11 +72,13 @@ where })) } - /// spawn a async for graceful shutdown h1 Io type with a timeout. + /// Spawns a graceful shutdown task for the underlying I/O with a timeout. fn close(&self, conn: ConnectionInnerType) { if let Some(timeout) = self.config.disconnect_timeout { if let ConnectionInnerType::H1(io) = conn { - actix_rt::spawn(CloseConnection::new(io, timeout)); + if tokio::runtime::Handle::try_current().is_ok() { + actix_rt::spawn(CloseConnection::new(io, timeout)); + } } } } @@ -173,12 +175,15 @@ where }; // acquire an owned permit and carry it with connection - let permit = inner.permits.clone().acquire_owned().await.map_err(|_| { - ConnectError::Io(io::Error::new( - io::ErrorKind::Other, - "failed to acquire semaphore on client connection pool", - )) - })?; + let permit = Arc::clone(&inner.permits) + .acquire_owned() + .await + .map_err(|_| { + ConnectError::Io(io::Error::new( + io::ErrorKind::Other, + "failed to acquire semaphore on client connection pool", + )) + })?; let conn = { let mut conn = None; diff --git a/awc/src/error.rs b/awc/src/error.rs index 0104e5fe8..d2d6d71c4 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -18,35 +18,35 @@ pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendReques #[derive(Debug, Display, From)] pub enum WsClientError { /// Invalid response status - #[display(fmt = "Invalid response status")] + #[display("Invalid response status")] InvalidResponseStatus(StatusCode), /// Invalid upgrade header - #[display(fmt = "Invalid upgrade header")] + #[display("Invalid upgrade header")] InvalidUpgradeHeader, /// Invalid connection header - #[display(fmt = "Invalid connection header")] + #[display("Invalid connection header")] InvalidConnectionHeader(HeaderValue), /// Missing Connection header - #[display(fmt = "Missing Connection header")] + #[display("Missing Connection header")] MissingConnectionHeader, /// Missing Sec-Websocket-Accept header - #[display(fmt = "Missing Sec-Websocket-Accept header")] + #[display("Missing Sec-Websocket-Accept header")] MissingWebSocketAcceptHeader, /// Invalid challenge response - #[display(fmt = "Invalid challenge response")] + #[display("Invalid challenge response")] InvalidChallengeResponse([u8; 28], HeaderValue), /// Protocol error - #[display(fmt = "{}", _0)] + #[display("{}", _0)] Protocol(WsProtocolError), /// Send request error - #[display(fmt = "{}", _0)] + #[display("{}", _0)] SendRequest(SendRequestError), } @@ -68,13 +68,13 @@ impl From for WsClientError { #[derive(Debug, Display, From)] pub enum JsonPayloadError { /// Content type error - #[display(fmt = "Content type error")] + #[display("Content type error")] ContentType, /// Deserialize error - #[display(fmt = "Json deserialize error: {}", _0)] + #[display("Json deserialize error: {}", _0)] Deserialize(JsonError), /// Payload error - #[display(fmt = "Error that occur during reading payload: {}", _0)] + #[display("Error that occur during reading payload: {}", _0)] Payload(PayloadError), } diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 8f3244997..862405234 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -49,7 +49,7 @@ impl FrozenClientRequest { where B: MessageBody + 'static, { - RequestSender::Rc(self.head.clone(), None).send_body( + RequestSender::Rc(Rc::clone(&self.head), None).send_body( self.addr, self.response_decompress, self.timeout, @@ -60,7 +60,7 @@ impl FrozenClientRequest { /// Send a json body. pub fn send_json(&self, value: &T) -> SendClientRequest { - RequestSender::Rc(self.head.clone(), None).send_json( + RequestSender::Rc(Rc::clone(&self.head), None).send_json( self.addr, self.response_decompress, self.timeout, @@ -71,7 +71,7 @@ impl FrozenClientRequest { /// Send an urlencoded body. pub fn send_form(&self, value: &T) -> SendClientRequest { - RequestSender::Rc(self.head.clone(), None).send_form( + RequestSender::Rc(Rc::clone(&self.head), None).send_form( self.addr, self.response_decompress, self.timeout, @@ -86,7 +86,7 @@ impl FrozenClientRequest { S: Stream> + 'static, E: Into + 'static, { - RequestSender::Rc(self.head.clone(), None).send_stream( + RequestSender::Rc(Rc::clone(&self.head), None).send_stream( self.addr, self.response_decompress, self.timeout, @@ -97,7 +97,7 @@ impl FrozenClientRequest { /// Send an empty body. pub fn send(&self) -> SendClientRequest { - RequestSender::Rc(self.head.clone(), None).send( + RequestSender::Rc(Rc::clone(&self.head), None).send( self.addr, self.response_decompress, self.timeout, @@ -147,8 +147,8 @@ impl FrozenSendBuilder { /// Complete request construction and send a body. pub fn send_body(self, body: impl MessageBody + 'static) -> SendClientRequest { - if let Some(e) = self.err { - return e.into(); + if let Some(err) = self.err { + return err.into(); } RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_body( @@ -177,8 +177,8 @@ impl FrozenSendBuilder { /// Complete request construction and send an urlencoded body. pub fn send_form(self, value: impl Serialize) -> SendClientRequest { - if let Some(e) = self.err { - return e.into(); + if let Some(err) = self.err { + return err.into(); } RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_form( @@ -196,8 +196,8 @@ impl FrozenSendBuilder { S: Stream> + 'static, E: Into + 'static, { - if let Some(e) = self.err { - return e.into(); + if let Some(err) = self.err { + return err.into(); } RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_stream( @@ -211,8 +211,8 @@ impl FrozenSendBuilder { /// Complete request construction and send an empty body. pub fn send(self) -> SendClientRequest { - if let Some(e) = self.err { - return e.into(); + if let Some(err) = self.err { + return err.into(); } RequestSender::Rc(self.req.head, Some(self.extra_headers)).send( diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 460480994..b582d51e4 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -100,8 +100,6 @@ //! # } //! ``` -#![deny(rust_2018_idioms, nonstandard_style)] -#![warn(future_incompatible)] #![allow(unknown_lints)] // temp: #[allow(non_local_definitions)] #![allow( clippy::type_complexity, diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 0ea5f174e..b2cf9c45b 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -78,7 +78,7 @@ where RedirectServiceFuture::Tunnel { fut } } ConnectRequest::Client(head, body, addr) => { - let connector = self.connector.clone(); + let connector = Rc::clone(&self.connector); let max_redirect_times = self.max_redirect_times; // backup the uri and method for reuse schema and authority. diff --git a/awc/src/request.rs b/awc/src/request.rs index 28ed8b5f5..5f42f67ec 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -415,8 +415,8 @@ impl ClientRequest { // allow unused mut when cookies feature is disabled fn prep_for_sending(#[allow(unused_mut)] mut self) -> Result { - if let Some(e) = self.err { - return Err(e.into()); + if let Some(err) = self.err { + return Err(err.into()); } // validate uri diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 8de1033a3..0015743bd 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -54,8 +54,8 @@ impl From for FreezeRequestError { impl From for SendRequestError { fn from(err: PrepForSendingError) -> SendRequestError { match err { - PrepForSendingError::Url(e) => SendRequestError::Url(e), - PrepForSendingError::Http(e) => SendRequestError::Http(e), + PrepForSendingError::Url(err) => SendRequestError::Url(err), + PrepForSendingError::Http(err) => SendRequestError::Http(err), PrepForSendingError::Json(err) => { SendRequestError::Custom(Box::new(err), Box::new("json serialization error")) } @@ -156,20 +156,20 @@ impl Future for SendClientRequest { } impl From for SendClientRequest { - fn from(e: SendRequestError) -> Self { - SendClientRequest::Err(Some(e)) + fn from(err: SendRequestError) -> Self { + SendClientRequest::Err(Some(err)) } } impl From for SendClientRequest { - fn from(e: HttpError) -> Self { - SendClientRequest::Err(Some(e.into())) + fn from(err: HttpError) -> Self { + SendClientRequest::Err(Some(err.into())) } } impl From for SendClientRequest { - fn from(e: PrepForSendingError) -> Self { - SendClientRequest::Err(Some(e.into())) + fn from(err: PrepForSendingError) -> Self { + SendClientRequest::Err(Some(err.into())) } } diff --git a/awc/src/test.rs b/awc/src/test.rs index 126583179..a7ed3faf0 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -65,9 +65,7 @@ impl TestResponse { /// Set response's payload pub fn set_payload>(mut self, data: B) -> Self { - let (_, mut payload) = h1::Payload::create(true); - payload.unread_data(data.into()); - self.payload = Some(payload.into()); + self.payload = Some(Payload::from(data.into())); self } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index c3340206d..3ce1d286a 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -253,12 +253,13 @@ impl WebsocketsRequest { pub async fn connect( mut self, ) -> Result<(ClientResponse, Framed), WsClientError> { - if let Some(e) = self.err.take() { - return Err(e.into()); + if let Some(err) = self.err.take() { + return Err(err.into()); } - // validate uri + // validate URI let uri = &self.head.uri; + if uri.host().is_none() { return Err(InvalidUrl::MissingHost.into()); } else if uri.scheme().is_none() { @@ -273,9 +274,12 @@ impl WebsocketsRequest { } if !self.head.headers.contains_key(header::HOST) { + let hostname = uri.host().unwrap(); + let port = uri.port(); + self.head.headers.insert( header::HOST, - HeaderValue::from_str(uri.host().unwrap()).unwrap(), + HeaderValue::from_str(&Host { hostname, port }.to_string()).unwrap(), ); } @@ -322,7 +326,7 @@ impl WebsocketsRequest { // Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded // (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3). - let sec_key: [u8; 16] = rand::random(); + let sec_key = rand::random::<[u8; 16]>(); let key = BASE64_STANDARD.encode(sec_key); self.head.headers.insert( @@ -434,6 +438,25 @@ impl fmt::Debug for WebsocketsRequest { } } +/// Formatter for host (hostname+port) header values. +struct Host<'a> { + hostname: &'a str, + port: Option>, +} + +impl fmt::Display for Host<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.hostname)?; + + if let Some(port) = &self.port { + f.write_str(":")?; + f.write_str(port.as_str())?; + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 76915630f..43c83891a 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -20,7 +20,7 @@ use base64::prelude::*; use bytes::Bytes; use cookie::Cookie; use futures_util::stream; -use rand::Rng; +use rand::distr::{Alphanumeric, SampleString as _}; mod utils; @@ -516,11 +516,7 @@ async fn client_gzip_encoding_large() { #[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn client_gzip_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(100_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 100_000); let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| async { @@ -562,11 +558,7 @@ async fn client_brotli_encoding() { #[cfg(feature = "compress-brotli")] #[actix_rt::test] async fn client_brotli_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(70_000) - .map(char::from) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 70_000); let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| async { @@ -607,11 +599,7 @@ async fn client_deflate_encoding() { #[actix_rt::test] async fn client_deflate_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(rand::distributions::Alphanumeric) - .map(char::from) - .take(70_000) - .collect::(); + let data = Alphanumeric.sample_string(&mut rand::rng(), 70_000); let srv = actix_test::start(|| { App::new().default_service(web::to(|body: Bytes| async { diff --git a/justfile b/justfile index 646c6b44d..8449c3056 100644 --- a/justfile +++ b/justfile @@ -3,13 +3,18 @@ _list: # Format workspace. fmt: + just --unstable --fmt cargo +nightly fmt fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --write -# Downgrade dev-dependencies necessary to run MSRV checks/tests. +# Downgrade dependencies necessary to run MSRV checks/tests. [private] downgrade-for-msrv: - cargo update -p=clap --precise=4.4.18 + cargo update -p=clap --precise=4.4.18 # next ver: 1.74.0 + cargo update -p=divan --precise=0.1.15 # next ver: 1.80.0 + cargo update -p=litemap --precise=0.7.4 # next ver: 1.81.0 + cargo update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0 + cargo update -p=half --precise=2.4.1 # next ver: 1.81.0 msrv := ``` cargo metadata --format-version=1 \ @@ -17,20 +22,25 @@ msrv := ``` | sed -E 's/^1\.([0-9]{2})$/1\.\1\.0/' ``` msrv_rustup := "+" + msrv - non_linux_all_features_list := ``` cargo metadata --format-version=1 \ | jq '.packages[] | select(.source == null) | .features | keys' \ | jq -r --slurp \ - --arg exclusions "tokio-uring,io-uring,experimental-io-uring" \ + --arg exclusions "__tls,__compress,tokio-uring,io-uring,experimental-io-uring" \ 'add | unique | . - ($exclusions | split(",")) | join(",")' ``` +all_crate_features := if os() == "linux" { "--all-features" } else { "--features='" + non_linux_all_features_list + "'" } -all_crate_features := if os() == "linux" { - "--all-features" -} else { - "--features='" + non_linux_all_features_list + "'" -} +[private] +check-min: + cargo hack --workspace check --no-default-features + +[private] +check-default: + cargo hack --workspace check + +# Run Clippy over workspace. +check toolchain="": && (clippy toolchain) # Run Clippy over workspace. clippy toolchain="": @@ -43,8 +53,8 @@ test-msrv: downgrade-for-msrv (test msrv_rustup) test toolchain="": cargo {{ toolchain }} test --lib --tests -p=actix-web-codegen --all-features cargo {{ toolchain }} test --lib --tests -p=actix-multipart-derive --all-features - cargo {{ toolchain }} nextest run -p=actix-router --no-default-features - cargo {{ toolchain }} nextest run --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)" + cargo {{ toolchain }} nextest run --no-tests=warn -p=actix-router --no-default-features + cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)" # Test workspace docs. test-docs toolchain="": && doc @@ -56,7 +66,7 @@ test-all toolchain="": (test toolchain) (test-docs toolchain) # Test workspace and collect coverage info. [private] test-coverage toolchain="": - cargo {{ toolchain }} llvm-cov nextest --no-report {{ all_crate_features }} + cargo {{ toolchain }} llvm-cov nextest --no-tests=warn --no-report {{ all_crate_features }} cargo {{ toolchain }} llvm-cov --doc --no-report {{ all_crate_features }} # Test workspace and generate Codecov report. @@ -69,6 +79,7 @@ test-coverage-lcov toolchain="": (test-coverage toolchain) # Document crates in workspace. doc *args: && doc-set-workspace-crates + rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --workspace {{ all_crate_features }} {{ args }} [private] @@ -76,7 +87,8 @@ doc-set-workspace-crates: #!/usr/bin/env bash ( echo "window.ALL_CRATES =" - cargo metadata --format-version=1 | jq '[.packages[] | select(.source == null) | .name]' + cargo metadata --format-version=1 \ + | jq '[.packages[] | select(.source == null) | .targets | map(select(.doc) | .name)] | flatten' echo ";" ) > "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" @@ -93,13 +105,22 @@ update-readmes: && fmt cd ./actix-multipart && cargo rdme --force cd ./actix-test && cargo rdme --force +feature_combo_skip_list := if os() == "linux" { "__tls,__compress" } else { "__tls,__compress,experimental-io-uring" } + +# Checks compatibility of feature combinations. +check-feature-combinations: + cargo hack --workspace \ + --feature-powerset --depth=4 \ + --skip={{ feature_combo_skip_list }} \ + check + # Check for unintentional external type exposure on all crates in workspace. check-external-types-all toolchain="+nightly": #!/usr/bin/env bash set -euo pipefail exit=0 for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do - if ! just check-external-types-manifest "$f" {{toolchain}}; then exit=1; fi + if ! just check-external-types-manifest "$f" {{ toolchain }}; then exit=1; fi echo echo done @@ -112,9 +133,9 @@ check-external-types-all-table toolchain="+nightly": for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do echo echo "Checking for $f" - just check-external-types-manifest "$f" {{toolchain}} --output-format=markdown-table + just check-external-types-manifest "$f" {{ toolchain }} --output-format=markdown-table done # Check for unintentional external type exposure on a crate. check-external-types-manifest manifest_path toolchain="+nightly" *extra_args="": - cargo {{toolchain}} check-external-types --manifest-path "{{manifest_path}}" {{extra_args}} + cargo {{ toolchain }} check-external-types --manifest-path "{{ manifest_path }}" {{ extra_args }} diff --git a/scripts/bump b/scripts/bump index 6fd879eae..7a57e6ed0 100755 --- a/scripts/bump +++ b/scripts/bump @@ -169,3 +169,5 @@ if [ "$GH_RELEASE" = 'y' ] || [ "$GH_RELEASE" = 'Y' ]; then fi echo + +cargo update >/dev/null 2>&1 || true