1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-08-27 15:35:53 +02:00

Compare commits

...

39 Commits

Author SHA1 Message Date
Rob Ede
455d5c460d prepare actix-files release 0.6.0-beta.14 2022-01-14 20:01:11 +00:00
Rob Ede
8faca783fa prepare actix-web release 4.0.0-beta.20 2022-01-14 20:00:26 +00:00
Rob Ede
edbb9b047e prepare actix-router release 0.5.0-rc.1 2022-01-14 19:59:36 +00:00
Ali MJ Al-Nasrawy
32742d0715 support opaque app in test helpers (#2584) 2022-01-14 19:45:32 +00:00
Ali MJ Al-Nasrawy
d90c1a2331 convert error in Result extractor (#2581)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:59:22 +00:00
Ali MJ Al-Nasrawy
2a12b41456 fix support for 12 extractors (#2582)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:31:48 +00:00
Rob Ede
6c97d448b7 update tokio-uring to 0.2 (#2583) 2022-01-12 17:53:36 +00:00
Ali MJ Al-Nasrawy
c3ce33df05 unify generics across App, Scope and Resource (#2572)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 15:02:28 +00:00
Rob Ede
4431c8da65 fix bench 2022-01-05 14:10:38 +00:00
Michael
2d11ab5977 Add ServiceConfig::configure (#1988)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 12:31:39 +00:00
Rob Ede
4ebf16890d add GuardContext::header (#2569) 2022-01-05 11:47:14 +00:00
Ali MJ Al-Nasrawy
fe0bbfb3da optimize PathDeserializer (#2570) 2022-01-05 10:48:20 +00:00
Ali MJ Al-Nasrawy
2462b6dd5d generalize impl Responder for HttpResponse (#2567)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:42:52 +00:00
Ali MJ Al-Nasrawy
49cfabeaf5 simplify Resource trait (#2568)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:34:13 +00:00
Rob Ede
0f7292c69a remove readme msrv link 2022-01-05 04:24:40 +00:00
Rob Ede
8bbf2b5052 prepare actix-test release 0.1.0-beta.11 2022-01-04 15:37:48 +00:00
Rob Ede
8c975bcc1f prepare actix-http-test release 3.0.0-beta.11 2022-01-04 15:37:33 +00:00
Rob Ede
742ad56d30 prepare actix-web-actors release 4.0.0-beta.10 2022-01-04 15:37:14 +00:00
Rob Ede
bcc8d5c441 prepare actix-multipart release 0.4.0-beta.12 2022-01-04 15:36:56 +00:00
Rob Ede
f659098d21 prepare awc release 3.0.0-beta.18 2022-01-04 15:35:21 +00:00
Rob Ede
8621ae12f8 prepare actix-web release 4.0.0-beta.19 2022-01-04 15:35:08 +00:00
Rob Ede
b338eb8473 prepare actix-http release 3.0.0-beta.18 2022-01-04 15:34:52 +00:00
Rob Ede
5abd1c2c2c prepare actix-web-codegen release 0.5.0-rc.1 2022-01-04 15:34:16 +00:00
Rob Ede
05336269f9 prepare actix-router release 0.5.0-beta.4 2022-01-04 15:33:44 +00:00
Rob Ede
86df295ee2 fully percent decode path segments when capturing (#2566) 2022-01-04 15:19:29 +00:00
Rob Ede
85c9b1a263 move quoter 2022-01-04 12:58:40 +00:00
Rob Ede
577597a80a rename on-connect example 2022-01-04 12:54:20 +00:00
Ali MJ Al-Nasrawy
374dc9bfc9 files: percent-decode url path (#2398)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-04 12:54:11 +00:00
Rob Ede
93754f307f try path config from Data as well 2022-01-04 04:08:46 +00:00
Rob Ede
c7639bc3be document quoter 2022-01-04 03:48:12 +00:00
Rob Ede
0bc4ae9158 remove BodyEncoding trait (#2565) 2022-01-03 18:46:04 +00:00
Rob Ede
19a46e3925 fix doc test 2022-01-03 15:35:47 +00:00
Rob Ede
68cd853aa2 improve docs for Compress 2022-01-03 14:59:01 +00:00
Rob Ede
25fe1bbaa5 add double compress layer test 2022-01-03 14:05:08 +00:00
Rob Ede
e890307091 Fix AcceptEncoding header (#2501) 2022-01-03 13:17:57 +00:00
Rob Ede
b708924590 only run nightly checks on master ci 2021-12-31 08:38:58 +00:00
Rob Ede
5dcb250237 fix doc test 2021-12-31 07:53:53 +00:00
Rob Ede
b4ff6addfe use match name if possible in data debug log 2021-12-30 07:15:57 +00:00
Rob Ede
231a24ef8d improve application data docs 2021-12-30 07:11:35 +00:00
107 changed files with 2941 additions and 1999 deletions

View File

@@ -1,8 +1,6 @@
name: Benchmark name: Benchmark
on: on:
pull_request:
types: [opened, synchronize, reopened]
push: push:
branches: branches:
- master - master

View File

@@ -5,6 +5,93 @@ on:
branches: [master] branches: [master]
jobs: jobs:
build_and_test_nightly:
strategy:
fail-fast: false
matrix:
target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
version:
- nightly
name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }}
env:
CI: 1
CARGO_INCREMENTAL: 0
VCPKGRS_DYNAMIC: 1
steps:
- uses: actions/checkout@v2
# install OpenSSL on Windows
# TODO: GitHub actions docs state that OpenSSL is
# already installed on these Windows machines somewhere
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: vcpkg install openssl:x64-windows
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: check minimal
uses: actions-rs/cargo@v1
with: { command: ci-check-min }
- name: check default
uses: actions-rs/cargo@v1
with: { command: ci-check-default }
- name: tests
timeout-minutes: 60
run: |
cargo test --lib --tests -p=actix-router --all-features
cargo test --lib --tests -p=actix-http --all-features
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
cargo test --lib --tests -p=actix-web-codegen --all-features
cargo test --lib --tests -p=awc --all-features
cargo test --lib --tests -p=actix-http-test --all-features
cargo test --lib --tests -p=actix-test --all-features
cargo test --lib --tests -p=actix-files
cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --all-features
- name: tests (io-uring)
if: matrix.target.os == 'ubuntu-latest'
timeout-minutes: 60
run: >
sudo bash -c "ulimit -Sl 512
&& ulimit -Hl 512
&& PATH=$PATH:/usr/share/rust/.cargo/bin
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
- name: Clear the cargo caches
run: |
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
cargo-cache
ci_feature_powerset_check: ci_feature_powerset_check:
name: Verify Feature Combinations name: Verify Feature Combinations
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -18,7 +18,6 @@ jobs:
version: version:
- 1.54.0 # MSRV - 1.54.0 # MSRV
- stable - stable
- nightly
name: ${{ matrix.target.name }} / ${{ matrix.version }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}

View File

@@ -14,6 +14,7 @@ jobs:
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
profile: minimal
components: rustfmt components: rustfmt
- name: Check with rustfmt - name: Check with rustfmt
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -30,10 +31,18 @@ jobs:
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
profile: minimal
components: clippy components: clippy
override: true override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Check with Clippy - name: Check with Clippy
uses: actions-rs/clippy-check@v1 uses: actions-rs/clippy-check@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --all-features --tests args: --workspace --tests --examples --all-features

View File

@@ -3,6 +3,47 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.20 - 2022-01-14
### Added
- `GuardContext::header` [#2569]
- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988]
### Changed
- `HttpResponse` can now be used as a `Responder` with any body type. [#2567]
- `Result` extractor wrapper can now convert error types. [#2581]
- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581]
- Maximum number of handler extractors has increased to 12. [#2582]
- Removed bound `<B as MessageBody>::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584]
[#1988]: https://github.com/actix/actix-web/pull/1988
[#2567]: https://github.com/actix/actix-web/pull/2567
[#2569]: https://github.com/actix/actix-web/pull/2569
[#2581]: https://github.com/actix/actix-web/pull/2581
[#2582]: https://github.com/actix/actix-web/pull/2582
[#2584]: https://github.com/actix/actix-web/pull/2584
## 4.0.0-beta.19 - 2022-01-04
### Added
- `impl Hash` for `http::header::Encoding`. [#2501]
- `AcceptEncoding::negotiate()`. [#2501]
### Changed
- `AcceptEncoding::preference` now returns `Option<Preference<Encoding>>`. [#2501]
- Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501]
- `http::header::Encoding` now only represents `Content-Encoding` types. [#2501]
### Fixed
- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501]
### Removed
- `Compress::new`; restricting compression algorithm is done through feature flags. [#2501]
- `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565]
[#2501]: https://github.com/actix/actix-web/pull/2501
[#2565]: https://github.com/actix/actix-web/pull/2565
## 4.0.0-beta.18 - 2021-12-29 ## 4.0.0-beta.18 - 2021-12-29
### Changed ### Changed
- Update `cookie` dependency (re-exported) to `0.16`. [#2555] - Update `cookie` dependency (re-exported) to `0.16`. [#2555]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.18" version = "4.0.0-beta.20"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@@ -28,15 +28,15 @@ path = "src/lib.rs"
resolver = "2" resolver = "2"
members = [ members = [
".", ".",
"awc",
"actix-http",
"actix-files", "actix-files",
"actix-http-test",
"actix-http",
"actix-multipart", "actix-multipart",
"actix-router",
"actix-test",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"actix-http-test", "awc",
"actix-test",
"actix-router",
] ]
[features] [features]
@@ -71,15 +71,15 @@ experimental-io-uring = ["actix-server/io-uring"]
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.3" actix-rt = "2.6"
actix-server = "2.0.0-rc.2" actix-server = "2.0.0-rc.4"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-tls = { version = "3.0.0", default-features = false, optional = true } actix-tls = { version = "3.0.0", default-features = false, optional = true }
actix-http = "3.0.0-beta.17" actix-http = "3.0.0-beta.18"
actix-router = "0.5.0-beta.3" actix-router = "0.5.0-rc.1"
actix-web-codegen = "0.5.0-beta.6" actix-web-codegen = "0.5.0-rc.1"
ahash = "0.7" ahash = "0.7"
bytes = "1" bytes = "1"
@@ -105,10 +105,12 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } actix-files = "0.6.0-beta.14"
awc = { version = "3.0.0-beta.17", features = ["openssl"] } actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.18", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
const-str = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
@@ -116,6 +118,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["std"]
rand = "0.8" rand = "0.8"
rcgen = "0.8" rcgen = "0.8"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
zstd = "0.9" zstd = "0.9"
@@ -163,7 +166,7 @@ name = "uds"
required-features = ["compress-gzip"] required-features = ["compress-gzip"]
[[example]] [[example]]
name = "on_connect" name = "on-connect"
required-features = [] required-features = []
[[bench]] [[bench]]

View File

@@ -6,13 +6,13 @@
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![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.0.0-beta.18)](https://docs.rs/actix-web/4.0.0-beta.18) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.20)](https://docs.rs/actix-web/4.0.0-beta.20)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.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.0.0-beta.18/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.18) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.20/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.20)
<br /> <br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![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/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
![downloads](https://img.shields.io/crates/d/actix-web.svg) ![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -1,8 +1,21 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.14 - 2022-01-14
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
[#2583]: https://github.com/actix/actix-web/pull/2583
## 0.6.0-beta.13 - 2022-01-04
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
[#2398]: https://github.com/actix/actix-web/pull/2398
## 0.6.0-beta.12 - 2021-12-29 ## 0.6.0-beta.12 - 2021-12-29
- No significant changes since `0.6.0-beta.11`. - No significant changes since `0.6.0-beta.11`.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.12" version = "0.6.0-beta.14"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@@ -22,10 +22,10 @@ path = "src/lib.rs"
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies] [dependencies]
actix-http = "3.0.0-beta.17" actix-http = "3.0.0-beta.18"
actix-service = "2" actix-service = "2"
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4.0.0-beta.18", default-features = false } actix-web = { version = "4.0.0-beta.20", default-features = false }
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"
@@ -39,9 +39,10 @@ mime_guess = "2.0.1"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
tokio-uring = { version = "0.1", optional = true } tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.10" actix-test = "0.1.0-beta.11"
actix-web = "4.0.0-beta.18" actix-web = "4.0.0-beta.20"
tempfile = "3.2"

View File

@@ -3,11 +3,11 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.12)](https://docs.rs/actix-files/0.6.0-beta.12) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.14)](https://docs.rs/actix-files/0.6.0-beta.14)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.12/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.12) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.14/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.14)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -10,6 +10,9 @@ use actix_web::{error::Error, web::Bytes};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
#[cfg(feature = "experimental-io-uring")]
use bytes::BytesMut;
use super::named::File; use super::named::File;
pin_project! { pin_project! {
@@ -214,64 +217,3 @@ where
} }
} }
} }
#[cfg(feature = "experimental-io-uring")]
use bytes_mut::BytesMut;
// TODO: remove new type and use bytes::BytesMut directly
#[doc(hidden)]
#[cfg(feature = "experimental-io-uring")]
mod bytes_mut {
use std::ops::{Deref, DerefMut};
use tokio_uring::buf::{IoBuf, IoBufMut};
#[derive(Debug)]
pub struct BytesMut(bytes::BytesMut);
impl BytesMut {
pub(super) fn new() -> Self {
Self(bytes::BytesMut::new())
}
}
impl Deref for BytesMut {
type Target = bytes::BytesMut;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BytesMut {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
unsafe impl IoBuf for BytesMut {
fn stable_ptr(&self) -> *const u8 {
self.0.as_ptr()
}
fn bytes_init(&self) -> usize {
self.0.len()
}
fn bytes_total(&self) -> usize {
self.0.capacity()
}
}
unsafe impl IoBufMut for BytesMut {
fn stable_mut_ptr(&mut self) -> *mut u8 {
self.0.as_mut_ptr()
}
unsafe fn set_init(&mut self, init_len: usize) {
if self.len() < init_len {
self.0.set_len(init_len);
}
}
}
}

View File

@@ -40,14 +40,23 @@ impl Directory {
pub(crate) type DirectoryRenderer = pub(crate) type DirectoryRenderer =
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>; dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
// show file url as relative to static path /// Returns percent encoded file URL path.
macro_rules! encode_file_url { macro_rules! encode_file_url {
($path:ident) => { ($path:ident) => {
utf8_percent_encode(&$path, CONTROLS) utf8_percent_encode(&$path, CONTROLS)
}; };
} }
// " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f; /// Returns HTML entity encoded formatter.
///
/// ```plain
/// " => &quot;
/// & => &amp;
/// ' => &#x27;
/// < => &lt;
/// > => &gt;
/// / => &#x2f;
/// ```
macro_rules! encode_file_name { macro_rules! encode_file_name {
($entry:ident) => { ($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy(), Html) escape_html_entity(&$entry.file_name().to_string_lossy(), Html)

View File

@@ -23,16 +23,23 @@ impl ResponseError for FilesError {
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
#[derive(Display, Debug, PartialEq)] #[derive(Display, Debug, PartialEq)]
#[non_exhaustive]
pub enum UriSegmentError { pub enum UriSegmentError {
/// The segment started with the wrapped invalid character. /// The segment started with the wrapped invalid character.
#[display(fmt = "The segment started with the wrapped invalid character")] #[display(fmt = "The segment started with the wrapped invalid character")]
BadStart(char), BadStart(char),
/// The segment contained the wrapped invalid character. /// The segment contained the wrapped invalid character.
#[display(fmt = "The segment contained the wrapped invalid character")] #[display(fmt = "The segment contained the wrapped invalid character")]
BadChar(char), BadChar(char),
/// The segment ended with the wrapped invalid character. /// The segment ended with the wrapped invalid character.
#[display(fmt = "The segment ended with the wrapped invalid character")] #[display(fmt = "The segment ended with the wrapped invalid character")]
BadEnd(char), BadEnd(char),
/// The path is not a valid UTF-8 string after doing percent decoding.
#[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
NotValidUtf8,
} }
/// Return `BadRequest` for `UriSegmentError` /// Return `BadRequest` for `UriSegmentError`

View File

@@ -28,6 +28,7 @@ use crate::{
/// ///
/// `Files` service must be registered with `App::service()` method. /// `Files` service must be registered with `App::service()` method.
/// ///
/// # Examples
/// ``` /// ```
/// use actix_web::App; /// use actix_web::App;
/// use actix_files::Files; /// use actix_files::Files;

View File

@@ -67,8 +67,8 @@ mod tests {
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use actix_service::ServiceFactory;
use actix_web::{ use actix_web::{
dev::ServiceFactory,
guard, guard,
http::{ http::{
header::{self, ContentDisposition, DispositionParam, DispositionType}, header::{self, ContentDisposition, DispositionParam, DispositionType},
@@ -303,7 +303,7 @@ mod tests {
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript" "application/javascript; charset=utf-8"
); );
assert_eq!( assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
@@ -597,7 +597,8 @@ mod tests {
.to_request(); .to_request();
let res = test::call_service(&srv, request).await; let res = test::call_service(&srv, request).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); assert!(res.headers().contains_key(header::CONTENT_ENCODING));
assert!(!test::read_body(res).await.is_empty());
} }
#[actix_rt::test] #[actix_rt::test]
@@ -802,6 +803,38 @@ mod tests {
let req = TestRequest::get().uri("/test/%43argo.toml").to_request(); let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
// `%2F` == `/`
let req = TestRequest::get().uri("/test%2Ftest.binary").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = TestRequest::get().uri("/test/Cargo.toml%00").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_percent_encoding_2() {
let tmpdir = tempfile::tempdir().unwrap();
let filename = match cfg!(unix) {
true => "ض:?#[]{}<>()@!$&'`|*+,;= %20.test",
false => "ض#[]{}()@!$&'`+,;= %20.test",
};
let filename_encoded = filename
.as_bytes()
.iter()
.map(|c| format!("%{:02X}", c))
.collect::<String>();
std::fs::File::create(tmpdir.path().join(filename)).unwrap();
let srv = test::init_service(App::new().service(Files::new("", tmpdir.path()))).await;
let req = TestRequest::get()
.uri(&format!("/{}", filename_encoded))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
} }
#[actix_rt::test] #[actix_rt::test]

View File

@@ -1,22 +1,20 @@
use std::{ use std::{
fmt,
fs::Metadata, fs::Metadata,
io, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use actix_service::{Service, ServiceFactory};
use actix_web::{ use actix_web::{
body::{self, BoxBody, SizedStream}, body::{self, BoxBody, SizedStream},
dev::{ dev::{
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
ServiceResponse, ServiceRequest, ServiceResponse,
}, },
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, ContentEncoding, DispositionParam, self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
DispositionType, ExtendedValue, DispositionType, ExtendedValue, HeaderValue,
}, },
StatusCode, StatusCode,
}, },
@@ -40,7 +38,7 @@ bitflags! {
impl Default for Flags { impl Default for Flags {
fn default() -> Self { fn default() -> Self {
Flags::from_bits_truncate(0b0000_0111) Flags::from_bits_truncate(0b0000_1111)
} }
} }
@@ -68,12 +66,12 @@ impl Default for Flags {
/// NamedFile::open_async("./static/index.html").await /// NamedFile::open_async("./static/index.html").await
/// } /// }
/// ``` /// ```
#[derive(Deref, DerefMut)] #[derive(Debug, Deref, DerefMut)]
pub struct NamedFile { pub struct NamedFile {
path: PathBuf,
#[deref] #[deref]
#[deref_mut] #[deref_mut]
file: File, file: File,
path: PathBuf,
modified: Option<SystemTime>, modified: Option<SystemTime>,
pub(crate) md: Metadata, pub(crate) md: Metadata,
pub(crate) flags: Flags, pub(crate) flags: Flags,
@@ -83,32 +81,6 @@ pub struct NamedFile {
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
} }
impl fmt::Debug for NamedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedFile")
.field("path", &self.path)
.field(
"file",
#[cfg(feature = "experimental-io-uring")]
{
&"tokio_uring::File"
},
#[cfg(not(feature = "experimental-io-uring"))]
{
&self.file
},
)
.field("modified", &self.modified)
.field("md", &self.md)
.field("flags", &self.flags)
.field("status_code", &self.status_code)
.field("content_type", &self.content_type)
.field("content_disposition", &self.content_disposition)
.field("encoding", &self.encoding)
.finish()
}
}
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File; pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")] #[cfg(feature = "experimental-io-uring")]
@@ -224,7 +196,6 @@ impl NamedFile {
}) })
} }
#[cfg(not(feature = "experimental-io-uring"))]
/// Attempts to open a file in read-only mode. /// Attempts to open a file in read-only mode.
/// ///
/// # Examples /// # Examples
@@ -232,6 +203,7 @@ impl NamedFile {
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
/// ``` /// ```
#[cfg(not(feature = "experimental-io-uring"))]
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> { pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
let file = File::open(&path)?; let file = File::open(&path)?;
Self::from_file(file, path) Self::from_file(file, path)
@@ -295,23 +267,21 @@ impl NamedFile {
self self
} }
/// Set the MIME Content-Type for serving this file. By default /// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred
/// the Content-Type is inferred from the filename extension. /// from the filename extension.
#[inline] #[inline]
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
self.content_type = mime_type; self.content_type = mime_type;
self self
} }
/// Set the Content-Disposition for serving this file. This allows /// Set the Content-Disposition for serving this file. This allows changing the
/// changing the inline/attachment disposition as well as the filename /// `inline/attachment` disposition as well as the filename sent to the peer.
/// sent to the peer.
/// ///
/// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the
/// and the filename is taken from the path provided in the `open` method /// filename is taken from the path provided in the `open` method after converting it to UTF-8
/// after converting it to UTF-8 using. /// (using `to_string_lossy`).
/// [`std::ffi::OsStr::to_string_lossy`]
#[inline] #[inline]
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
self.content_disposition = cd; self.content_disposition = cd;
@@ -337,7 +307,7 @@ impl NamedFile {
self self
} }
/// Specifies whether to use ETag or not. /// Specifies whether to return `ETag` header in response.
/// ///
/// Default is true. /// Default is true.
#[inline] #[inline]
@@ -346,7 +316,7 @@ impl NamedFile {
self self
} }
/// Specifies whether to use Last-Modified or not. /// Specifies whether to return `Last-Modified` header in response.
/// ///
/// Default is true. /// Default is true.
#[inline] #[inline]
@@ -364,7 +334,7 @@ impl NamedFile {
self self
} }
/// Creates a etag in a format is similar to Apache's. /// Creates an `ETag` in a format is similar to Apache's.
pub(crate) fn etag(&self) -> Option<header::EntityTag> { pub(crate) fn etag(&self) -> Option<header::EntityTag> {
self.modified.as_ref().map(|mtime| { self.modified.as_ref().map(|mtime| {
let ino = { let ino = {
@@ -386,7 +356,7 @@ impl NamedFile {
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("modification time must be after epoch"); .expect("modification time must be after epoch");
header::EntityTag::strong(format!( header::EntityTag::new_strong(format!(
"{:x}:{:x}:{:x}:{:x}", "{:x}:{:x}:{:x}:{:x}",
ino, ino,
self.md.len(), self.md.len(),
@@ -405,12 +375,13 @@ impl NamedFile {
if self.status_code != StatusCode::OK { if self.status_code != StatusCode::OK {
let mut res = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) { let ct = if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone()); equiv_utf8_text(self.content_type.clone())
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
} else { } else {
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string())); self.content_type
} };
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
if self.flags.contains(Flags::CONTENT_DISPOSITION) { if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.insert_header(( res.insert_header((
@@ -420,7 +391,7 @@ impl NamedFile {
} }
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding); res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
} }
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
@@ -478,12 +449,13 @@ impl NamedFile {
let mut res = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) { let ct = if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone()); equiv_utf8_text(self.content_type.clone())
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
} else { } else {
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string())); self.content_type
} };
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
if self.flags.contains(Flags::CONTENT_DISPOSITION) { if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.insert_header(( res.insert_header((
@@ -492,9 +464,8 @@ impl NamedFile {
)); ));
} }
// default compressing
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding); res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
} }
if let Some(lm) = last_modified { if let Some(lm) = last_modified {
@@ -517,7 +488,12 @@ impl NamedFile {
length = ranges[0].length; length = ranges[0].length;
offset = ranges[0].start; offset = ranges[0].start;
res.encoding(ContentEncoding::Identity); // don't allow compression middleware to modify partial content
res.insert_header((
header::CONTENT_ENCODING,
HeaderValue::from_static("identity"),
));
res.insert_header(( res.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
@@ -626,7 +602,7 @@ impl Service<ServiceRequest> for NamedFileService {
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();

View File

@@ -1,5 +1,5 @@
use std::{ use std::{
path::{Path, PathBuf}, path::{Component, Path, PathBuf},
str::FromStr, str::FromStr,
}; };
@@ -26,8 +26,23 @@ impl PathBufWrap {
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> { pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
let mut buf = PathBuf::new(); let mut buf = PathBuf::new();
// equivalent to `path.split('/').count()`
let mut segment_count = path.matches('/').count() + 1;
// we can decode the whole path here (instead of per-segment decoding)
// because we will reject `%2F` in paths using `segement_count`.
let path = percent_encoding::percent_decode_str(path)
.decode_utf8()
.map_err(|_| UriSegmentError::NotValidUtf8)?;
// disallow decoding `%2F` into `/`
if segment_count != path.matches('/').count() + 1 {
return Err(UriSegmentError::BadChar('/'));
}
for segment in path.split('/') { for segment in path.split('/') {
if segment == ".." { if segment == ".." {
segment_count -= 1;
buf.pop(); buf.pop();
} else if !hidden_files && segment.starts_with('.') { } else if !hidden_files && segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.')); return Err(UriSegmentError::BadStart('.'));
@@ -40,6 +55,7 @@ impl PathBufWrap {
} else if segment.ends_with('<') { } else if segment.ends_with('<') {
return Err(UriSegmentError::BadEnd('<')); return Err(UriSegmentError::BadEnd('<'));
} else if segment.is_empty() { } else if segment.is_empty() {
segment_count -= 1;
continue; continue;
} else if cfg!(windows) && segment.contains('\\') { } else if cfg!(windows) && segment.contains('\\') {
return Err(UriSegmentError::BadChar('\\')); return Err(UriSegmentError::BadChar('\\'));
@@ -48,6 +64,12 @@ impl PathBufWrap {
} }
} }
// make sure we agree with stdlib parser
for (i, component) in buf.components().enumerate() {
assert!(matches!(component, Component::Normal(_)));
assert!(i < segment_count);
}
Ok(PathBufWrap(buf)) Ok(PathBufWrap(buf))
} }
} }

View File

@@ -2,7 +2,7 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
use actix_web::{ use actix_web::{
body::BoxBody, body::BoxBody,
dev::{Service, ServiceRequest, ServiceResponse}, dev::{self, Service, ServiceRequest, ServiceResponse},
error::Error, error::Error,
guard::Guard, guard::Guard,
http::{header, Method}, http::{header, Method},
@@ -98,7 +98,7 @@ impl Service<ServiceRequest> for FilesService {
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards { let is_method_valid = if let Some(guard) = &self.guards {
@@ -114,7 +114,7 @@ impl Service<ServiceRequest> for FilesService {
Box::pin(async move { Box::pin(async move {
if !is_method_valid { if !is_method_valid {
return Ok(req.into_response( return Ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed() HttpResponse::MethodNotAllowed()
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.body("Request did not meet this resource's requirements."), .body("Request did not meet this resource's requirements."),
)); ));
@@ -123,7 +123,7 @@ impl Service<ServiceRequest> for FilesService {
let real_path = let real_path =
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) { match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Ok(req.error_response(e)), Err(err) => return Ok(req.error_response(err)),
}; };
if let Some(filter) = &this.path_filter { if let Some(filter) = &this.path_filter {
@@ -131,9 +131,7 @@ impl Service<ServiceRequest> for FilesService {
if let Some(ref default) = this.default { if let Some(ref default) = this.default {
return default.call(req).await; return default.call(req).await;
} else { } else {
return Ok( return Ok(req.into_response(HttpResponse::NotFound().finish()));
req.into_response(actix_web::HttpResponse::NotFound().finish())
);
} }
} }
} }

View File

@@ -19,12 +19,12 @@ async fn test_utf8_file_contents() {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(header::CONTENT_TYPE), res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain")), Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
); );
// prefer UTF-8 encoding // disable UTF-8 attribute
let srv = let srv =
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true))) test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false)))
.await; .await;
let req = TestRequest::with_uri("/utf8.txt").to_request(); let req = TestRequest::with_uri("/utf8.txt").to_request();
@@ -33,6 +33,6 @@ async fn test_utf8_file_contents() {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(header::CONTENT_TYPE), res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain; charset=utf-8")), Some(&HeaderValue::from_static("text/plain")),
); );
} }

View File

@@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.11 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.10" version = "3.0.0-beta.11"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@@ -35,7 +35,7 @@ actix-tls = "3.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-rc.2" actix-server = "2.0.0-rc.2"
awc = { version = "3.0.0-beta.17", default-features = false } awc = { version = "3.0.0-beta.18", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
@@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.18", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.17" actix-http = "3.0.0-beta.18"

View File

@@ -3,11 +3,11 @@
> Various helpers for Actix applications to use during testing. > Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http-test/3.0.0-beta.10) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http-test/3.0.0-beta.11)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.10) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.11)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -1,7 +1,33 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.18 - 2022-01-04
### Added
- `impl Eq` for `header::ContentEncoding`. [#2501]
- `impl Copy` for `QualityItem` where `T: Copy`. [#2501]
- `Quality::ZERO` equivalent to `q=0`. [#2501]
- `QualityItem::zero` that uses `Quality::ZERO`. [#2501]
- `ContentEncoding::to_header_value()`. [#2501]
### Changed
- `Quality::MIN` is now the smallest non-zero value. [#2501]
- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501]
- Rename `ContentEncoding::{Br => Brotli}`. [#2501]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565]
### Fixed
- `ContentEncoding::Identity` can now be parsed from a string. [#2501]
- A `Vary` header is now correctly sent along with compressed content. [#2501]
### Removed
- `ContentEncoding::Auto` variant. [#2501]
- `ContentEncoding::is_compression()`. [#2501]
[#2501]: https://github.com/actix/actix-web/pull/2501
[#2565]: https://github.com/actix/actix-web/pull/2565
## 3.0.0-beta.17 - 2021-12-27 ## 3.0.0-beta.17 - 2021-12-27

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.17" version = "3.0.0-beta.18"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
@@ -79,10 +79,10 @@ flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.9", optional = true } zstd = { version = "0.9", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
actix-server = "2.0.0-rc.2" actix-server = "2.0.0-rc.2"
actix-tls = { version = "3.0.0", features = ["openssl"] } actix-tls = { version = "3.0.0", features = ["openssl"] }
actix-web = "4.0.0-beta.18" actix-web = "4.0.0-beta.20"
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }

View File

@@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![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.0.0-beta.17)](https://docs.rs/actix-http/3.0.0-beta.17) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.18)](https://docs.rs/actix-http/3.0.0-beta.18)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.17) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.18/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.18)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -42,32 +42,37 @@ mod _new {
if x < 10 { if x < 10 {
f.write_str("00")?; f.write_str("00")?;
// 0 is handled so it's not possible to have a trailing 0, we can just return // 0 is handled so it's not possible to have a trailing 0, we can just return
itoa::fmt(f, x) itoa_fmt(f, x)
} else if x < 100 { } else if x < 100 {
f.write_str("0")?; f.write_str("0")?;
if x % 10 == 0 { if x % 10 == 0 {
// trailing 0, divide by 10 and write // trailing 0, divide by 10 and write
itoa::fmt(f, x / 10) itoa_fmt(f, x / 10)
} else { } else {
itoa::fmt(f, x) itoa_fmt(f, x)
} }
} else { } else {
// x is in range 101999 // x is in range 101999
if x % 100 == 0 { if x % 100 == 0 {
// two trailing 0s, divide by 100 and write // two trailing 0s, divide by 100 and write
itoa::fmt(f, x / 100) itoa_fmt(f, x / 100)
} else if x % 10 == 0 { } else if x % 10 == 0 {
// one trailing 0, divide by 10 and write // one trailing 0, divide by 10 and write
itoa::fmt(f, x / 10) itoa_fmt(f, x / 10)
} else { } else {
itoa::fmt(f, x) itoa_fmt(f, x)
} }
} }
} }
} }
} }
} }
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
let mut buf = itoa::Buffer::new();
wr.write_str(buf.format(value))
}
} }
mod _naive { mod _naive {

View File

@@ -47,9 +47,9 @@ where
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> { pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
let decoder = match encoding { let decoder = match encoding {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(BrotliDecoder::new( ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
Writer::new(), BrotliDecoder::new(Writer::new()),
)))), ))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()), ZlibDecoder::new(Writer::new()),
@@ -165,7 +165,7 @@ enum ContentDecoder {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
Gzip(Box<GzDecoder<Writer>>), Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Br(Box<BrotliDecoder<Writer>>), Brotli(Box<BrotliDecoder<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime // We need explicit 'static lifetime here because ZstdDecoder need lifetime
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static` // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
@@ -176,7 +176,7 @@ impl ContentDecoder {
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> { fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.flush() { ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
Ok(()) => { Ok(()) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@@ -234,7 +234,7 @@ impl ContentDecoder {
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> { fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Brotli(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();

View File

@@ -56,25 +56,24 @@ impl<B: MessageBody> Encoder<B> {
} }
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self { pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto);
// no need to compress an empty body // no need to compress an empty body
if matches!(body.size(), BodySize::None) { if matches!(body.size(), BodySize::None) {
return Self::none(); return Self::none();
} }
let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity);
let body = match body.try_into_bytes() { let body = match body.try_into_bytes() {
Ok(body) => EncoderBody::Full { body }, Ok(body) => EncoderBody::Full { body },
Err(body) => EncoderBody::Stream { body }, Err(body) => EncoderBody::Stream { body },
}; };
if can_encode { if should_encode {
// Modify response body only if encoder is set // wrap body only if encoder is feature-enabled
if let Some(enc) = ContentEncoder::encoder(encoding) { if let Some(enc) = ContentEncoder::select(encoding) {
update_head(encoding, head); update_head(encoding, head);
return Encoder { return Encoder {
@@ -169,6 +168,7 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
let mut this = self.project(); let mut this = self.project();
loop { loop {
if *this.eof { if *this.eof {
return Poll::Ready(None); return Poll::Ready(None);
@@ -252,10 +252,10 @@ where
} }
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut().insert( head.headers_mut()
header::CONTENT_ENCODING, .insert(header::CONTENT_ENCODING, encoding.to_header_value());
HeaderValue::from_static(encoding.as_str()), head.headers_mut()
); .insert(header::VARY, HeaderValue::from_static("accept-encoding"));
head.no_chunking(false); head.no_chunking(false);
} }
@@ -268,7 +268,7 @@ enum ContentEncoder {
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Br(BrotliEncoder<Writer>), Brotli(BrotliEncoder<Writer>),
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`. // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
@@ -277,7 +277,7 @@ enum ContentEncoder {
} }
impl ContentEncoder { impl ContentEncoder {
fn encoder(encoding: ContentEncoding) -> Option<Self> { fn select(encoding: ContentEncoding) -> Option<Self> {
match encoding { match encoding {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
@@ -292,8 +292,8 @@ impl ContentEncoder {
))), ))),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoding::Br => { ContentEncoding::Brotli => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Some(ContentEncoder::Brotli(BrotliEncoder::new(Writer::new(), 3)))
} }
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
@@ -310,7 +310,7 @@ impl ContentEncoder {
pub(crate) fn take(&mut self) -> Bytes { pub(crate) fn take(&mut self) -> Bytes {
match *self { match *self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Brotli(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
@@ -326,7 +326,7 @@ impl ContentEncoder {
fn finish(self) -> Result<Bytes, io::Error> { fn finish(self) -> Result<Bytes, io::Error> {
match self { match self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoder::Br(encoder) => match encoder.finish() { ContentEncoder::Brotli(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
@@ -354,7 +354,7 @@ impl ContentEncoder {
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
match *self { match *self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding br encoding: {}", err); trace!("Error decoding br encoding: {}", err);

View File

@@ -250,6 +250,7 @@ impl From<ParseError> for Response<BoxBody> {
/// A set of errors that can occur running blocking tasks in thread pool. /// A set of errors that can occur running blocking tasks in thread pool.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "Blocking thread pool is gone")] #[display(fmt = "Blocking thread pool is gone")]
// TODO: non-exhaustive
pub struct BlockingError; pub struct BlockingError;
/// A set of errors that can occur during payload parsing. /// A set of errors that can occur during payload parsing.

View File

@@ -6,7 +6,7 @@ use ahash::AHashMap;
use http::header::{HeaderName, HeaderValue}; use http::header::{HeaderName, HeaderValue};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use crate::header::AsHeaderName; use super::AsHeaderName;
/// A multi-map of HTTP headers. /// A multi-map of HTTP headers.
/// ///
@@ -605,6 +605,13 @@ impl<'a> IntoIterator for &'a HeaderMap {
} }
} }
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
}
}
/// Iterator over removed, owned values with the same associated name. /// Iterator over removed, owned values with the same associated name.
/// ///
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`] /// Returned from methods that remove or replace items. See [`HeaderMap::insert`]

View File

@@ -50,20 +50,13 @@ pub use self::utils::{
/// An interface for types that already represent a valid header. /// An interface for types that already represent a valid header.
pub trait Header: TryIntoHeaderValue { pub trait Header: TryIntoHeaderValue {
/// Returns the name of the header field /// Returns the name of the header field.
fn name() -> HeaderName; fn name() -> HeaderName;
/// Parse a header /// Parse the header from a HTTP message.
fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>; fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>;
} }
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
}
}
/// This encode set is used for HTTP header values and is defined at /// This encode set is used for HTTP header values and is defined at
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>. /// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS

View File

@@ -20,14 +20,16 @@ pub struct ContentEncodingParseError;
/// See [IANA HTTP Content Coding Registry]. /// See [IANA HTTP Content Coding Registry].
/// ///
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml /// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive] #[non_exhaustive]
pub enum ContentEncoding { pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation. /// Indicates the no-op identity encoding.
Auto, ///
/// I.e., no compression or modification.
Identity,
/// A format using the Brotli algorithm. /// A format using the Brotli algorithm.
Br, Brotli,
/// A format using the zlib structure with deflate algorithm. /// A format using the zlib structure with deflate algorithm.
Deflate, Deflate,
@@ -37,32 +39,36 @@ pub enum ContentEncoding {
/// Zstd algorithm. /// Zstd algorithm.
Zstd, Zstd,
/// Indicates the identity function (i.e. no compression, nor modification).
Identity,
} }
impl ContentEncoding { impl ContentEncoding {
/// Is the content compressed?
#[inline]
pub const fn is_compression(self) -> bool {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
}
/// Convert content encoding to string. /// Convert content encoding to string.
#[inline] #[inline]
pub const fn as_str(self) -> &'static str { pub const fn as_str(self) -> &'static str {
match self { match self {
ContentEncoding::Br => "br", ContentEncoding::Brotli => "br",
ContentEncoding::Gzip => "gzip", ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate", ContentEncoding::Deflate => "deflate",
ContentEncoding::Zstd => "zstd", ContentEncoding::Zstd => "zstd",
ContentEncoding::Identity | ContentEncoding::Auto => "identity", ContentEncoding::Identity => "identity",
}
}
/// Convert content encoding to header value.
#[inline]
pub const fn to_header_value(self) -> HeaderValue {
match self {
ContentEncoding::Brotli => HeaderValue::from_static("br"),
ContentEncoding::Gzip => HeaderValue::from_static("gzip"),
ContentEncoding::Deflate => HeaderValue::from_static("deflate"),
ContentEncoding::Zstd => HeaderValue::from_static("zstd"),
ContentEncoding::Identity => HeaderValue::from_static("identity"),
} }
} }
} }
impl Default for ContentEncoding { impl Default for ContentEncoding {
#[inline]
fn default() -> Self { fn default() -> Self {
Self::Identity Self::Identity
} }
@@ -71,16 +77,18 @@ impl Default for ContentEncoding {
impl FromStr for ContentEncoding { impl FromStr for ContentEncoding {
type Err = ContentEncodingParseError; type Err = ContentEncodingParseError;
fn from_str(val: &str) -> Result<Self, Self::Err> { fn from_str(enc: &str) -> Result<Self, Self::Err> {
let val = val.trim(); let enc = enc.trim();
if val.eq_ignore_ascii_case("br") { if enc.eq_ignore_ascii_case("br") {
Ok(ContentEncoding::Br) Ok(ContentEncoding::Brotli)
} else if val.eq_ignore_ascii_case("gzip") { } else if enc.eq_ignore_ascii_case("gzip") {
Ok(ContentEncoding::Gzip) Ok(ContentEncoding::Gzip)
} else if val.eq_ignore_ascii_case("deflate") { } else if enc.eq_ignore_ascii_case("deflate") {
Ok(ContentEncoding::Deflate) Ok(ContentEncoding::Deflate)
} else if val.eq_ignore_ascii_case("zstd") { } else if enc.eq_ignore_ascii_case("identity") {
Ok(ContentEncoding::Identity)
} else if enc.eq_ignore_ascii_case("zstd") {
Ok(ContentEncoding::Zstd) Ok(ContentEncoding::Zstd)
} else { } else {
Err(ContentEncodingParseError) Err(ContentEncodingParseError)

View File

@@ -27,7 +27,8 @@ const MAX_QUALITY_FLOAT: f32 = 1.0;
/// ///
/// assert_eq!(q(0.42).to_string(), "0.42"); /// assert_eq!(q(0.42).to_string(), "0.42");
/// assert_eq!(q(1.0).to_string(), "1"); /// assert_eq!(q(1.0).to_string(), "1");
/// assert_eq!(Quality::MIN.to_string(), "0"); /// assert_eq!(Quality::MIN.to_string(), "0.001");
/// assert_eq!(Quality::ZERO.to_string(), "0");
/// ``` /// ```
/// ///
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1 /// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
@@ -38,8 +39,11 @@ impl Quality {
/// The maximum quality value, equivalent to `q=1.0`. /// The maximum quality value, equivalent to `q=1.0`.
pub const MAX: Quality = Quality(MAX_QUALITY_INT); pub const MAX: Quality = Quality(MAX_QUALITY_INT);
/// The minimum quality value, equivalent to `q=0.0`. /// The minimum, non-zero quality value, equivalent to `q=0.001`.
pub const MIN: Quality = Quality(0); pub const MIN: Quality = Quality(1);
/// The zero quality value, equivalent to `q=0.0`.
pub const ZERO: Quality = Quality(0);
/// Converts a float in the range 0.01.0 to a `Quality`. /// Converts a float in the range 0.01.0 to a `Quality`.
/// ///
@@ -51,7 +55,7 @@ impl Quality {
// Check that `value` is within range should be done before calling this method. // Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful. // Just in case, this debug_assert should catch if we were forgetful.
debug_assert!( debug_assert!(
(0.0f32..=1.0f32).contains(&value), (0.0..=MAX_QUALITY_FLOAT).contains(&value),
"q value must be between 0.0 and 1.0" "q value must be between 0.0 and 1.0"
); );
@@ -154,10 +158,13 @@ impl TryFrom<f32> for Quality {
/// let q1 = q(1.0); /// let q1 = q(1.0);
/// assert_eq!(q1, Quality::MAX); /// assert_eq!(q1, Quality::MAX);
/// ///
/// let q2 = q(0.0); /// let q2 = q(0.001);
/// assert_eq!(q2, Quality::MIN); /// assert_eq!(q2, Quality::MIN);
/// ///
/// let q3 = q(0.42); /// let q3 = q(0.0);
/// assert_eq!(q3, Quality::ZERO);
///
/// let q4 = q(0.42);
/// ``` /// ```
/// ///
/// An out-of-range `f32` quality will panic. /// An out-of-range `f32` quality will panic.
@@ -185,6 +192,10 @@ mod tests {
#[test] #[test]
fn display_output() { fn display_output() {
assert_eq!(Quality::ZERO.to_string(), "0");
assert_eq!(Quality::MIN.to_string(), "0.001");
assert_eq!(Quality::MAX.to_string(), "1");
assert_eq!(q(0.0).to_string(), "0"); assert_eq!(q(0.0).to_string(), "0");
assert_eq!(q(1.0).to_string(), "1"); assert_eq!(q(1.0).to_string(), "1");
assert_eq!(q(0.001).to_string(), "0.001"); assert_eq!(q(0.001).to_string(), "0.001");

View File

@@ -31,7 +31,7 @@ use super::Quality;
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap(); /// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
/// assert!(q_item > q_item_fallback); /// assert!(q_item > q_item_fallback);
/// ``` /// ```
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QualityItem<T> { pub struct QualityItem<T> {
/// The wrapped contents of the field. /// The wrapped contents of the field.
pub item: T, pub item: T,
@@ -53,10 +53,15 @@ impl<T> QualityItem<T> {
Self::new(item, Quality::MAX) Self::new(item, Quality::MAX)
} }
/// Constructs a new `QualityItem` from an item, using the minimum q-value. /// Constructs a new `QualityItem` from an item, using the minimum, non-zero q-value.
pub fn min(item: T) -> Self { pub fn min(item: T) -> Self {
Self::new(item, Quality::MIN) Self::new(item, Quality::MIN)
} }
/// Constructs a new `QualityItem` from an item, using zero q-value of zero.
pub fn zero(item: T) -> Self {
Self::new(item, Quality::ZERO)
}
} }
impl<T: PartialEq> PartialOrd for QualityItem<T> { impl<T: PartialEq> PartialOrd for QualityItem<T> {
@@ -73,7 +78,10 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
// q-factor value is implied for max value // q-factor value is implied for max value
Quality::MAX => Ok(()), Quality::MAX => Ok(()),
Quality::MIN => f.write_str("; q=0"), // fast path for zero
Quality::ZERO => f.write_str("; q=0"),
// quality formatting is already using itoa
q => write!(f, "; q={}", q), q => write!(f, "; q={}", q),
} }
} }

View File

@@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.12 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.11" version = "0.4.0-beta.12"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@@ -15,7 +15,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.18", default-features = false } actix-web = { version = "4.0.0-beta.20", default-features = false }
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
@@ -28,7 +28,7 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.17" actix-http = "3.0.0-beta.18"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.8.4", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@@ -3,11 +3,11 @@
> Multipart form support for Actix Web. > Multipart form support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![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.4.0-beta.11)](https://docs.rs/actix-multipart/0.4.0-beta.11) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.12)](https://docs.rs/actix-multipart/0.4.0-beta.12)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.11/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.11) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.12/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.12)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -1,8 +1,21 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.5.0-rc.1 - 2022-01-14
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
[#2568]: https://github.com/actix/actix-web/pull/2568
## 0.5.0-beta.4 - 2022-01-04
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
[#2566]: https://github.com/actix/actix-net/pull/2566
## 0.5.0-beta.3 - 2021-12-17 ## 0.5.0-beta.3 - 2021-12-17
- Minimum supported Rust version (MSRV) is now 1.52. - Minimum supported Rust version (MSRV) is now 1.52.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-router" name = "actix-router"
version = "0.5.0-beta.3" version = "0.5.0-rc.1"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>", "Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",

View File

@@ -1,8 +1,14 @@
use std::borrow::Cow;
use serde::de::{self, Deserializer, Error as DeError, Visitor}; use serde::de::{self, Deserializer, Error as DeError, Visitor};
use serde::forward_to_deserialize_any; use serde::forward_to_deserialize_any;
use crate::path::{Path, PathIter}; use crate::path::{Path, PathIter};
use crate::ResourcePath; use crate::{Quoter, ResourcePath};
thread_local! {
static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b"");
}
macro_rules! unsupported_type { macro_rules! unsupported_type {
($trait_fn:ident, $name:expr) => { ($trait_fn:ident, $name:expr) => {
@@ -10,16 +16,13 @@ macro_rules! unsupported_type {
where where
V: Visitor<'de>, V: Visitor<'de>,
{ {
Err(de::value::Error::custom(concat!( Err(de::Error::custom(concat!("unsupported type: ", $name)))
"unsupported type: ",
$name
)))
} }
}; };
} }
macro_rules! parse_single_value { macro_rules! parse_single_value {
($trait_fn:ident, $visit_fn:ident, $tp:tt) => { ($trait_fn:ident) => {
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where where
V: Visitor<'de>, V: Visitor<'de>,
@@ -33,18 +36,35 @@ macro_rules! parse_single_value {
.as_str(), .as_str(),
)) ))
} else { } else {
let v = self.path[0].parse().map_err(|_| { Value {
de::value::Error::custom(format!( value: &self.path[0],
"can not parse {:?} to a {}", }
&self.path[0], $tp .$trait_fn(visitor)
))
})?;
visitor.$visit_fn(v)
} }
} }
}; };
} }
macro_rules! parse_value {
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
let decoded = FULL_QUOTER
.with(|q| q.requote(self.value.as_bytes()))
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(self.value));
let v = decoded.parse().map_err(|_| {
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
})?;
visitor.$visit_fn(v)
}
};
}
pub struct PathDeserializer<'de, T: ResourcePath> { pub struct PathDeserializer<'de, T: ResourcePath> {
path: &'de Path<T>, path: &'de Path<T>,
} }
@@ -172,23 +192,6 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
} }
} }
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
if self.path.segment_count() != 1 {
Err(de::value::Error::custom(
format!(
"wrong number of parameters: {} expected 1",
self.path.segment_count()
)
.as_str(),
))
} else {
visitor.visit_str(&self.path[0])
}
}
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where where
V: Visitor<'de>, V: Visitor<'de>,
@@ -199,25 +202,26 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
} }
unsupported_type!(deserialize_any, "'any'"); unsupported_type!(deserialize_any, "'any'");
unsupported_type!(deserialize_bytes, "bytes");
unsupported_type!(deserialize_option, "Option<T>"); unsupported_type!(deserialize_option, "Option<T>");
unsupported_type!(deserialize_identifier, "identifier"); unsupported_type!(deserialize_identifier, "identifier");
unsupported_type!(deserialize_ignored_any, "ignored_any"); unsupported_type!(deserialize_ignored_any, "ignored_any");
parse_single_value!(deserialize_bool, visit_bool, "bool"); parse_single_value!(deserialize_bool);
parse_single_value!(deserialize_i8, visit_i8, "i8"); parse_single_value!(deserialize_i8);
parse_single_value!(deserialize_i16, visit_i16, "i16"); parse_single_value!(deserialize_i16);
parse_single_value!(deserialize_i32, visit_i32, "i32"); parse_single_value!(deserialize_i32);
parse_single_value!(deserialize_i64, visit_i64, "i64"); parse_single_value!(deserialize_i64);
parse_single_value!(deserialize_u8, visit_u8, "u8"); parse_single_value!(deserialize_u8);
parse_single_value!(deserialize_u16, visit_u16, "u16"); parse_single_value!(deserialize_u16);
parse_single_value!(deserialize_u32, visit_u32, "u32"); parse_single_value!(deserialize_u32);
parse_single_value!(deserialize_u64, visit_u64, "u64"); parse_single_value!(deserialize_u64);
parse_single_value!(deserialize_f32, visit_f32, "f32"); parse_single_value!(deserialize_f32);
parse_single_value!(deserialize_f64, visit_f64, "f64"); parse_single_value!(deserialize_f64);
parse_single_value!(deserialize_string, visit_string, "String"); parse_single_value!(deserialize_str);
parse_single_value!(deserialize_byte_buf, visit_string, "String"); parse_single_value!(deserialize_string);
parse_single_value!(deserialize_char, visit_char, "char"); parse_single_value!(deserialize_bytes);
parse_single_value!(deserialize_byte_buf);
parse_single_value!(deserialize_char);
} }
struct ParamsDeserializer<'de, T: ResourcePath> { struct ParamsDeserializer<'de, T: ResourcePath> {
@@ -279,20 +283,6 @@ impl<'de> Deserializer<'de> for Key<'de> {
} }
} }
macro_rules! parse_value {
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
let v = self.value.parse().map_err(|_| {
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
})?;
visitor.$visit_fn(v)
}
};
}
struct Value<'de> { struct Value<'de> {
value: &'de str, value: &'de str,
} }
@@ -311,8 +301,6 @@ impl<'de> Deserializer<'de> for Value<'de> {
parse_value!(deserialize_u64, visit_u64, "u64"); parse_value!(deserialize_u64, visit_u64, "u64");
parse_value!(deserialize_f32, visit_f32, "f32"); parse_value!(deserialize_f32, visit_f32, "f32");
parse_value!(deserialize_f64, visit_f64, "f64"); parse_value!(deserialize_f64, visit_f64, "f64");
parse_value!(deserialize_string, visit_string, "String");
parse_value!(deserialize_byte_buf, visit_string, "String");
parse_value!(deserialize_char, visit_char, "char"); parse_value!(deserialize_char, visit_char, "char");
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@@ -340,18 +328,38 @@ impl<'de> Deserializer<'de> for Value<'de> {
visitor.visit_unit() visitor.visit_unit()
} }
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_borrowed_bytes(self.value.as_bytes())
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where where
V: Visitor<'de>, V: Visitor<'de>,
{ {
visitor.visit_borrowed_str(self.value) match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
Some(s) => visitor.visit_string(s),
None => visitor.visit_borrowed_str(self.value),
}
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
Some(s) => visitor.visit_byte_buf(s.into()),
None => visitor.visit_borrowed_bytes(self.value.as_bytes()),
}
}
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_bytes(visitor)
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_str(visitor)
} }
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error> fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@@ -497,6 +505,7 @@ mod tests {
use super::*; use super::*;
use crate::path::Path; use crate::path::Path;
use crate::router::Router; use crate::router::Router;
use crate::ResourceDef;
#[derive(Deserialize)] #[derive(Deserialize)]
struct MyStruct { struct MyStruct {
@@ -657,6 +666,79 @@ mod tests {
assert!(format!("{:?}", s).contains("can not parse")); assert!(format!("{:?}", s).contains("can not parse"));
} }
#[test]
fn deserialize_path_decode_string() {
let rdef = ResourceDef::new("/{key}");
let mut path = Path::new("/%25");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let segment: String = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment, "%");
let mut path = Path::new("/%2F");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let segment: String = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment, "/")
}
#[test]
fn deserialize_path_decode_seq() {
let rdef = ResourceDef::new("/{key}/{value}");
let mut path = Path::new("/%30%25/%30%2F");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment.0, "0%");
assert_eq!(segment.1, "0/");
}
#[test]
fn deserialize_path_decode_map() {
#[derive(Deserialize)]
struct Vals {
key: String,
value: String,
}
let rdef = ResourceDef::new("/{key}/{value}");
let mut path = Path::new("/%25/%2F");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let vals: Vals = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(vals.key, "%");
assert_eq!(vals.value, "/");
}
#[test]
fn deserialize_borrowed() {
#[derive(Debug, Deserialize)]
struct Params<'a> {
val: &'a str,
}
let rdef = ResourceDef::new("/{val}");
let mut path = Path::new("/X");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(params.val, "X");
let de = PathDeserializer::new(&path);
let params: &str = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(params, "X");
let mut path = Path::new("/%2F");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
assert!(<Params<'_> as serde::Deserialize>::deserialize(de).is_err());
let de = PathDeserializer::new(&path);
assert!(<&str as serde::Deserialize>::deserialize(de).is_err());
}
// #[test] // #[test]
// fn test_extract_path_decode() { // fn test_extract_path_decode() {
// let mut router = Router::<()>::default(); // let mut router = Router::<()>::default();

View File

@@ -8,6 +8,7 @@
mod de; mod de;
mod path; mod path;
mod pattern; mod pattern;
mod quoter;
mod resource; mod resource;
mod resource_path; mod resource_path;
mod router; mod router;
@@ -18,9 +19,10 @@ mod url;
pub use self::de::PathDeserializer; pub use self::de::PathDeserializer;
pub use self::path::Path; pub use self::path::Path;
pub use self::pattern::{IntoPatterns, Patterns}; pub use self::pattern::{IntoPatterns, Patterns};
pub use self::quoter::Quoter;
pub use self::resource::ResourceDef; pub use self::resource::ResourceDef;
pub use self::resource_path::{Resource, ResourcePath}; pub use self::resource_path::{Resource, ResourcePath};
pub use self::router::{ResourceInfo, Router, RouterBuilder}; pub use self::router::{ResourceInfo, Router, RouterBuilder};
#[cfg(feature = "http")] #[cfg(feature = "http")]
pub use self::url::{Quoter, Url}; pub use self::url::Url;

View File

@@ -1,5 +1,5 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::ops::Index; use std::ops::{DerefMut, Index};
use firestorm::profile_method; use firestorm::profile_method;
use serde::de; use serde::de;
@@ -213,8 +213,38 @@ impl<T: ResourcePath> Index<usize> for Path<T> {
} }
} }
impl<T: ResourcePath> Resource<T> for Path<T> { impl<T: ResourcePath> Resource for Path<T> {
fn resource_path(&mut self) -> &mut Self { type Path = T;
fn resource_path(&mut self) -> &mut Path<Self::Path> {
self self
} }
} }
impl<T, P> Resource for T
where
T: DerefMut<Target = Path<P>>,
P: ResourcePath,
{
type Path = P;
fn resource_path(&mut self) -> &mut Path<Self::Path> {
&mut *self
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
#[test]
fn deref_impls() {
let mut foo = Path::new("/foo");
let _ = (&mut foo).resource_path();
let foo = RefCell::new(foo);
let _ = foo.borrow_mut().resource_path();
}
}

219
actix-router/src/quoter.rs Normal file
View File

@@ -0,0 +1,219 @@
#[allow(dead_code)]
const GEN_DELIMS: &[u8] = b":/?#[]@";
#[allow(dead_code)]
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
#[allow(dead_code)]
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
#[allow(dead_code)]
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
#[allow(dead_code)]
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
1234567890
-._~";
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
1234567890
-._~
!$'()*,";
const QS: &[u8] = b"+&=;b";
/// A quoter
pub struct Quoter {
/// Simple bit-map of safe values in the 0-127 ASCII range.
safe_table: [u8; 16],
/// Simple bit-map of protected values in the 0-127 ASCII range.
protected_table: [u8; 16],
}
impl Quoter {
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
let mut quoter = Quoter {
safe_table: [0; 16],
protected_table: [0; 16],
};
// prepare safe table
for ch in 0..128 {
if ALLOWED.contains(&ch) {
set_bit(&mut quoter.safe_table, ch);
}
if QS.contains(&ch) {
set_bit(&mut quoter.safe_table, ch);
}
}
for &ch in safe {
set_bit(&mut quoter.safe_table, ch)
}
// prepare protected table
for &ch in protected {
set_bit(&mut quoter.safe_table, ch);
set_bit(&mut quoter.protected_table, ch);
}
quoter
}
/// Re-quotes... ?
///
/// Returns `None` when no modification to the original string was required.
pub fn requote(&self, val: &[u8]) -> Option<String> {
let mut has_pct = 0;
let mut pct = [b'%', 0, 0];
let mut idx = 0;
let mut cloned: Option<Vec<u8>> = None;
let len = val.len();
while idx < len {
let ch = val[idx];
if has_pct != 0 {
pct[has_pct] = val[idx];
has_pct += 1;
if has_pct == 3 {
has_pct = 0;
let buf = cloned.as_mut().unwrap();
if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) {
if ch < 128 {
if bit_at(&self.protected_table, ch) {
buf.extend_from_slice(&pct);
idx += 1;
continue;
}
if bit_at(&self.safe_table, ch) {
buf.push(ch);
idx += 1;
continue;
}
}
buf.push(ch);
} else {
buf.extend_from_slice(&pct[..]);
}
}
} else if ch == b'%' {
has_pct = 1;
if cloned.is_none() {
let mut c = Vec::with_capacity(len);
c.extend_from_slice(&val[..idx]);
cloned = Some(c);
}
} else if let Some(ref mut cloned) = cloned {
cloned.push(ch)
}
idx += 1;
}
cloned.map(|data| String::from_utf8_lossy(&data).into_owned())
}
}
/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer
/// representation from `0x0``0xF`.
///
/// - `0x30 ('0') => 0x0`
/// - `0x39 ('9') => 0x9`
/// - `0x41 ('a') => 0xA`
/// - `0x61 ('A') => 0xA`
/// - `0x46 ('f') => 0xF`
/// - `0x66 ('F') => 0xF`
fn from_ascii_hex(v: u8) -> Option<u8> {
match v {
b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30
b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41
b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61
_ => None,
}
}
/// Decode a ASCII hex-encoded pair to an integer.
///
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
///
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?);
// left shift high nibble by 4 bits
Some(d_high << 4 | d_low)
}
/// Sets bit in given bit-map to 1=true.
///
/// # Panics
/// Panics if `ch` index is out of bounds.
fn set_bit(array: &mut [u8], ch: u8) {
array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
}
/// Returns true if bit to true in given bit-map.
///
/// # Panics
/// Panics if `ch` index is out of bounds.
fn bit_at(array: &[u8], ch: u8) -> bool {
array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_encoding() {
let hex = b"0123456789abcdefABCDEF";
for i in 0..256 {
let c = i as u8;
if hex.contains(&c) {
assert!(from_ascii_hex(c).is_some())
} else {
assert!(from_ascii_hex(c).is_none())
}
}
let expected = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15,
];
for i in 0..hex.len() {
assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]);
}
}
#[test]
fn custom_quoter() {
let q = Quoter::new(b"", b"+");
assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c");
assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc");
let q = Quoter::new(b"%+", b"/");
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c");
assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb");
assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb");
assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb");
}
#[test]
fn quoter_no_modification() {
let q = Quoter::new(b"", b"");
assert_eq!(q.requote(b"/abc/../efg"), None);
}
}

View File

@@ -678,15 +678,14 @@ impl ResourceDef {
/// assert!(!try_match(&resource, &mut path)); /// assert!(!try_match(&resource, &mut path));
/// assert_eq!(path.unprocessed(), "/user/admin/stars"); /// assert_eq!(path.unprocessed(), "/user/admin/stars");
/// ``` /// ```
pub fn capture_match_info_fn<R, T, F, U>( pub fn capture_match_info_fn<R, F, U>(
&self, &self,
resource: &mut R, resource: &mut R,
check_fn: F, check_fn: F,
user_data: U, user_data: U,
) -> bool ) -> bool
where where
R: Resource<T>, R: Resource,
T: ResourcePath,
F: FnOnce(&R, U) -> bool, F: FnOnce(&R, U) -> bool,
{ {
profile_method!(capture_match_info_fn); profile_method!(capture_match_info_fn);

View File

@@ -2,8 +2,11 @@ use crate::Path;
// TODO: this trait is necessary, document it // TODO: this trait is necessary, document it
// see impl Resource for ServiceRequest // see impl Resource for ServiceRequest
pub trait Resource<T: ResourcePath> { pub trait Resource {
fn resource_path(&mut self) -> &mut Path<T>; /// Type of resource's path returned in `resource_path`.
type Path: ResourcePath;
fn resource_path(&mut self) -> &mut Path<Self::Path>;
} }
pub trait ResourcePath { pub trait ResourcePath {

View File

@@ -1,6 +1,6 @@
use firestorm::profile_method; use firestorm::profile_method;
use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; use crate::{IntoPatterns, Resource, ResourceDef};
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct ResourceId(pub u16); pub struct ResourceId(pub u16);
@@ -26,10 +26,9 @@ impl<T, U> Router<T, U> {
} }
} }
pub fn recognize<R, P>(&self, resource: &mut R) -> Option<(&T, ResourceId)> pub fn recognize<R>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
where where
R: Resource<P>, R: Resource,
P: ResourcePath,
{ {
profile_method!(recognize); profile_method!(recognize);
@@ -42,10 +41,9 @@ impl<T, U> Router<T, U> {
None None
} }
pub fn recognize_mut<R, P>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> pub fn recognize_mut<R>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
where where
R: Resource<P>, R: Resource,
P: ResourcePath,
{ {
profile_method!(recognize_mut); profile_method!(recognize_mut);
@@ -58,11 +56,10 @@ impl<T, U> Router<T, U> {
None None
} }
pub fn recognize_fn<R, P, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> pub fn recognize_fn<R, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
where where
F: Fn(&R, &Option<U>) -> bool, F: Fn(&R, &Option<U>) -> bool,
R: Resource<P>, R: Resource,
P: ResourcePath,
{ {
profile_method!(recognize_checked); profile_method!(recognize_checked);
@@ -75,15 +72,14 @@ impl<T, U> Router<T, U> {
None None
} }
pub fn recognize_mut_fn<R, P, F>( pub fn recognize_mut_fn<R, F>(
&mut self, &mut self,
resource: &mut R, resource: &mut R,
check: F, check: F,
) -> Option<(&mut T, ResourceId)> ) -> Option<(&mut T, ResourceId)>
where where
F: Fn(&R, &Option<U>) -> bool, F: Fn(&R, &Option<U>) -> bool,
R: Resource<P>, R: Resource,
P: ResourcePath,
{ {
profile_method!(recognize_mut_checked); profile_method!(recognize_mut_checked);

View File

@@ -1,40 +1,6 @@
use crate::ResourcePath; use crate::ResourcePath;
#[allow(dead_code)] use crate::Quoter;
const GEN_DELIMS: &[u8] = b":/?#[]@";
#[allow(dead_code)]
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
#[allow(dead_code)]
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
#[allow(dead_code)]
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
#[allow(dead_code)]
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
1234567890
-._~";
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
1234567890
-._~
!$'()*,";
const QS: &[u8] = b"+&=;b";
#[inline]
fn bit_at(array: &[u8], ch: u8) -> bool {
array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0
}
#[inline]
fn set_bit(array: &mut [u8], ch: u8) {
array[(ch >> 3) as usize] |= 1 << (ch & 7)
}
thread_local! { thread_local! {
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
@@ -54,18 +20,20 @@ impl Url {
} }
#[inline] #[inline]
pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
Url { Url {
path: quoter.requote(uri.path().as_bytes()), path: quoter.requote(uri.path().as_bytes()),
uri, uri,
} }
} }
/// Returns URI.
#[inline] #[inline]
pub fn uri(&self) -> &http::Uri { pub fn uri(&self) -> &http::Uri {
&self.uri &self.uri
} }
/// Returns path.
#[inline] #[inline]
pub fn path(&self) -> &str { pub fn path(&self) -> &str {
match self.path { match self.path {
@@ -94,113 +62,6 @@ impl ResourcePath for Url {
} }
} }
/// A quoter
pub struct Quoter {
safe_table: [u8; 16],
protected_table: [u8; 16],
}
impl Quoter {
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
let mut quoter = Quoter {
safe_table: [0; 16],
protected_table: [0; 16],
};
// prepare safe table
for i in 0..128 {
if ALLOWED.contains(&i) {
set_bit(&mut quoter.safe_table, i);
}
if QS.contains(&i) {
set_bit(&mut quoter.safe_table, i);
}
}
for ch in safe {
set_bit(&mut quoter.safe_table, *ch)
}
// prepare protected table
for ch in protected {
set_bit(&mut quoter.safe_table, *ch);
set_bit(&mut quoter.protected_table, *ch);
}
quoter
}
pub fn requote(&self, val: &[u8]) -> Option<String> {
let mut has_pct = 0;
let mut pct = [b'%', 0, 0];
let mut idx = 0;
let mut cloned: Option<Vec<u8>> = None;
let len = val.len();
while idx < len {
let ch = val[idx];
if has_pct != 0 {
pct[has_pct] = val[idx];
has_pct += 1;
if has_pct == 3 {
has_pct = 0;
let buf = cloned.as_mut().unwrap();
if let Some(ch) = restore_ch(pct[1], pct[2]) {
if ch < 128 {
if bit_at(&self.protected_table, ch) {
buf.extend_from_slice(&pct);
idx += 1;
continue;
}
if bit_at(&self.safe_table, ch) {
buf.push(ch);
idx += 1;
continue;
}
}
buf.push(ch);
} else {
buf.extend_from_slice(&pct[..]);
}
}
} else if ch == b'%' {
has_pct = 1;
if cloned.is_none() {
let mut c = Vec::with_capacity(len);
c.extend_from_slice(&val[..idx]);
cloned = Some(c);
}
} else if let Some(ref mut cloned) = cloned {
cloned.push(ch)
}
idx += 1;
}
cloned.map(|data| String::from_utf8_lossy(&data).into_owned())
}
}
#[inline]
fn from_hex(v: u8) -> Option<u8> {
if (b'0'..=b'9').contains(&v) {
Some(v - 0x30) // ord('0') == 0x30
} else if (b'A'..=b'F').contains(&v) {
Some(v - 0x41 + 10) // ord('A') == 0x41
} else if (b'a'..=b'f').contains(&v) {
Some(v - 0x61 + 10) // ord('a') == 0x61
} else {
None
}
}
#[inline]
fn restore_ch(d1: u8, d2: u8) -> Option<u8> {
from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use http::Uri; use http::Uri;
@@ -229,6 +90,16 @@ mod tests {
let path = match_url(re, "/user/2345/test"); let path = match_url(re, "/user/2345/test");
assert_eq!(path.get("id").unwrap(), "2345"); assert_eq!(path.get("id").unwrap(), "2345");
}
#[test]
fn protected_chars() {
let re = "/user/{id}/test";
let encoded = percent_encode(PROTECTED);
let path = match_url(re, format!("/user/{}/test", encoded));
// characters in captured segment remain unencoded
assert_eq!(path.get("id").unwrap(), &encoded);
// "%25" should never be decoded into '%' to guarantee the output is a valid // "%25" should never be decoded into '%' to guarantee the output is a valid
// percent-encoded format // percent-encoded format
@@ -239,13 +110,6 @@ mod tests {
assert_eq!(path.get("id").unwrap(), "qwe%25rty"); assert_eq!(path.get("id").unwrap(), "qwe%25rty");
} }
#[test]
fn protected_chars() {
let encoded = percent_encode(PROTECTED);
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
assert_eq!(path.get("id").unwrap(), &encoded);
}
#[test] #[test]
fn non_protected_ascii() { fn non_protected_ascii() {
let non_protected_ascii = ('\u{0}'..='\u{7F}') let non_protected_ascii = ('\u{0}'..='\u{7F}')
@@ -273,25 +137,4 @@ mod tests {
// We should always get a valid utf8 string // We should always get a valid utf8 string
assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok()); assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok());
} }
#[test]
fn hex_encoding() {
let hex = b"0123456789abcdefABCDEF";
for i in 0..256 {
let c = i as u8;
if hex.contains(&c) {
assert!(from_hex(c).is_some())
} else {
assert!(from_hex(c).is_none())
}
}
let expected = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15,
];
for i in 0..hex.len() {
assert_eq!(from_hex(hex[i]).unwrap(), expected[i]);
}
}
} }

View File

@@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.1.0-beta.11 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.0-beta.10" version = "0.1.0-beta.11"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.17" actix-http = "3.0.0-beta.18"
actix-http-test = "3.0.0-beta.10" actix-http-test = "3.0.0-beta.11"
actix-rt = "2.1" actix-rt = "2.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.18", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] }
awc = { version = "3.0.0-beta.17", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.18", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] } futures-util = { version = "0.3.7", default-features = false, features = [] }

View File

@@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.10 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.0.0-beta.9" version = "4.0.0-beta.10"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web" description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@@ -16,8 +16,8 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.12.0", default-features = false } actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.17" actix-http = "3.0.0-beta.18"
actix-web = { version = "4.0.0-beta.18", default-features = false } actix-web = { version = "4.0.0-beta.20", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
@@ -27,8 +27,8 @@ tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.10" actix-test = "0.1.0-beta.11"
awc = { version = "3.0.0-beta.17", default-features = false } awc = { version = "3.0.0-beta.18", default-features = false }
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@@ -3,11 +3,11 @@
> Actix actors support for Actix Web. > Actix actors support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![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.0.0-beta.9)](https://docs.rs/actix-web-actors/4.0.0-beta.9) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web-actors/4.0.0-beta.10)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9) [![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.5.0-rc.1 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.5.0-beta.6" version = "0.5.0-rc.1"
description = "Routing and runtime macros for Actix Web" description = "Routing and runtime macros for Actix Web"
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web.git"
@@ -15,17 +15,17 @@ edition = "2018"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
actix-router = "0.5.0-beta.4"
proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "parsing"] } syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1"
actix-router = "0.5.0-beta.3"
[dev-dependencies] [dev-dependencies]
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.10" actix-test = "0.1.0-beta.11"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0-beta.18" actix-web = "4.0.0-beta.20"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"

View File

@@ -3,11 +3,11 @@
> Routing and runtime macros for Actix Web. > Routing and runtime macros for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.6)](https://docs.rs/actix-web-codegen/0.5.0-beta.6) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-rc.1)](https://docs.rs/actix-web-codegen/0.5.0-rc.1)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6) [![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.18 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 3.0.0-beta.17 - 2021-12-29 ## 3.0.0-beta.17 - 2021-12-29
### Changed ### Changed
- Update `cookie` dependency (re-exported) to `0.16`. [#2555] - Update `cookie` dependency (re-exported) to `0.16`. [#2555]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.17" version = "3.0.0-beta.18"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@@ -60,7 +60,7 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-http = "3.0.0-beta.17" actix-http = "3.0.0-beta.18"
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0", features = ["connect", "uri"] } actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
@@ -93,21 +93,23 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
trust-dns-resolver = { version = "0.20.0", optional = true } trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3.0.0-beta.17", features = ["openssl"] } actix-http = { version = "3.0.0-beta.18", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
actix-server = "2.0.0-rc.2" actix-server = "2.0.0-rc.2"
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.18", features = ["openssl"] } actix-web = { version = "4.0.0-beta.20", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
const-str = "0.3"
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
static_assertions = "1.1" static_assertions = "1.1"
rcgen = "0.8" rcgen = "0.8"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"
zstd = "0.9"
[[example]] [[example]]
name = "client" name = "client"

View File

@@ -3,9 +3,9 @@
> Async HTTP and WebSocket client library. > Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![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.0.0-beta.17)](https://docs.rs/awc/3.0.0-beta.17) [![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.18)](https://docs.rs/awc/3.0.0-beta.18)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.17/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.17) [![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.18/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.18)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources

View File

@@ -1,5 +1,6 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
convert::Infallible,
io::{Read, Write}, io::{Read, Write},
net::{IpAddr, Ipv4Addr}, net::{IpAddr, Ipv4Addr},
sync::{ sync::{
@@ -15,43 +16,16 @@ use cookie::Cookie;
use futures_util::stream; use futures_util::stream;
use rand::Rng; use rand::Rng;
#[cfg(feature = "compress-brotli")] use actix_http::{HttpService, StatusCode};
use brotli2::write::BrotliEncoder;
#[cfg(feature = "compress-gzip")]
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use actix_http::{ContentEncoding, HttpService, StatusCode};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; use actix_service::{fn_service, map_config, ServiceFactoryExt as _};
use actix_web::{ use actix_web::{dev::AppConfig, http::header, web, App, Error, HttpRequest, HttpResponse};
dev::{AppConfig, BodyEncoding},
http::header,
web, App, Error, HttpRequest, HttpResponse,
};
use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; use awc::error::{JsonPayloadError, PayloadError, SendRequestError};
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ mod utils;
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \ const S: &str = "Hello World ";
Hello World Hello World Hello World Hello World Hello World \ const STR: &str = const_str::repeat!(S, 100);
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World";
#[actix_rt::test] #[actix_rt::test]
async fn test_simple() { async fn test_simple() {
@@ -471,15 +445,12 @@ async fn test_no_decompress() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new() App::new()
.wrap(actix_web::middleware::Compress::default()) .wrap(actix_web::middleware::Compress::default())
.service(web::resource("/").route(web::to(|| { .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
let mut res = HttpResponse::Ok().body(STR);
res.encoding(header::ContentEncoding::Gzip);
res
})))
}); });
let mut res = awc::Client::new() let mut res = awc::Client::new()
.get(srv.url("/")) .get(srv.url("/"))
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
@@ -488,15 +459,12 @@ async fn test_no_decompress() {
// read response // read response
let bytes = res.body().await.unwrap(); let bytes = res.body().await.unwrap();
assert_eq!(utils::gzip::decode(bytes), STR.as_bytes());
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
// POST // POST
let mut res = awc::Client::new() let mut res = awc::Client::new()
.post(srv.url("/")) .post(srv.url("/"))
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
@@ -504,10 +472,7 @@ async fn test_no_decompress() {
assert!(res.status().is_success()); assert!(res.status().is_success());
let bytes = res.body().await.unwrap(); let bytes = res.body().await.unwrap();
let mut e = GzDecoder::new(&bytes[..]); assert_eq!(utils::gzip::decode(bytes), STR.as_bytes());
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
} }
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@@ -515,13 +480,9 @@ async fn test_no_decompress() {
async fn test_client_gzip_encoding() { async fn test_client_gzip_encoding() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|| { App::new().service(web::resource("/").route(web::to(|| {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(STR.as_ref()).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(("content-encoding", "gzip")) .insert_header(header::ContentEncoding::Gzip)
.body(data) .body(utils::gzip::encode(STR))
}))) })))
}); });
@@ -531,7 +492,7 @@ async fn test_client_gzip_encoding() {
// read response // read response
let bytes = response.body().await.unwrap(); let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, STR);
} }
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@@ -539,13 +500,9 @@ async fn test_client_gzip_encoding() {
async fn test_client_gzip_encoding_large() { async fn test_client_gzip_encoding_large() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|| { App::new().service(web::resource("/").route(web::to(|| {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(STR.repeat(10).as_ref()).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(("content-encoding", "gzip")) .insert_header(header::ContentEncoding::Gzip)
.body(data) .body(utils::gzip::encode(STR.repeat(10)))
}))) })))
}); });
@@ -555,7 +512,7 @@ async fn test_client_gzip_encoding_large() {
// read response // read response
let bytes = response.body().await.unwrap(); let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from(STR.repeat(10))); assert_eq!(bytes, STR.repeat(10));
} }
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@@ -569,12 +526,9 @@ async fn test_client_gzip_encoding_large_random() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|data: Bytes| { App::new().service(web::resource("/").route(web::to(|data: Bytes| {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(&data).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(("content-encoding", "gzip")) .insert_header(header::ContentEncoding::Gzip)
.body(data) .body(utils::gzip::encode(data))
}))) })))
}); });
@@ -584,7 +538,7 @@ async fn test_client_gzip_encoding_large_random() {
// read response // read response
let bytes = response.body().await.unwrap(); let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from(data)); assert_eq!(bytes, data);
} }
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
@@ -592,12 +546,9 @@ async fn test_client_gzip_encoding_large_random() {
async fn test_client_brotli_encoding() { async fn test_client_brotli_encoding() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|data: Bytes| { App::new().service(web::resource("/").route(web::to(|data: Bytes| {
let mut e = BrotliEncoder::new(Vec::new(), 5);
e.write_all(&data).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(("content-encoding", "br")) .insert_header(("content-encoding", "br"))
.body(data) .body(utils::brotli::encode(data))
}))) })))
}); });
@@ -621,12 +572,9 @@ async fn test_client_brotli_encoding_large_random() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|data: Bytes| { App::new().service(web::resource("/").route(web::to(|data: Bytes| {
let mut e = BrotliEncoder::new(Vec::new(), 5);
e.write_all(&data).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(("content-encoding", "br")) .insert_header(header::ContentEncoding::Brotli)
.body(data) .body(utils::brotli::encode(&data))
}))) })))
}); });
@@ -636,25 +584,25 @@ async fn test_client_brotli_encoding_large_random() {
// read response // read response
let bytes = response.body().await.unwrap(); let bytes = response.body().await.unwrap();
assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, data);
assert_eq!(bytes, Bytes::from(data));
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_client_deflate_encoding() { async fn test_client_deflate_encoding() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: Bytes| { App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body)
}))
}); });
let req = srv.post("/").send_body(STR); let req = srv
.post("/")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.send_body(STR);
let mut res = req.await.unwrap(); let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
let bytes = res.body().await.unwrap(); let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, STR);
} }
#[actix_rt::test] #[actix_rt::test]
@@ -666,12 +614,13 @@ async fn test_client_deflate_encoding_large_random() {
.collect::<String>(); .collect::<String>();
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: Bytes| { App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body)
}))
}); });
let req = srv.post("/").send_body(data.clone()); let req = srv
.post("/")
.insert_header((header::ACCEPT_ENCODING, "br"))
.send_body(data.clone());
let mut res = req.await.unwrap(); let mut res = req.await.unwrap();
let bytes = res.body().await.unwrap(); let bytes = res.body().await.unwrap();
@@ -684,15 +633,16 @@ async fn test_client_deflate_encoding_large_random() {
async fn test_client_streaming_explicit() { async fn test_client_streaming_explicit() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: web::Payload| { App::new().default_service(web::to(|body: web::Payload| {
HttpResponse::Ok() HttpResponse::Ok().streaming(body)
.encoding(ContentEncoding::Identity)
.streaming(body)
})) }))
}); });
let body = let body =
stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) }); stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) });
let req = srv.post("/").send_stream(Box::pin(body)); let req = srv
.post("/")
.insert_header((header::ACCEPT_ENCODING, "identity"))
.send_stream(Box::pin(body));
let mut res = req.await.unwrap(); let mut res = req.await.unwrap();
assert!(res.status().is_success()); assert!(res.status().is_success());
@@ -705,17 +655,16 @@ async fn test_client_streaming_explicit() {
async fn test_body_streaming_implicit() { async fn test_body_streaming_implicit() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().default_service(web::to(|| { App::new().default_service(web::to(|| {
let body = stream::once(async { let body =
Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) stream::once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_bytes())) });
}); HttpResponse::Ok().streaming(body)
HttpResponse::Ok()
.encoding(ContentEncoding::Gzip)
.streaming(Box::pin(body))
})) }))
}); });
let req = srv.get("/").send(); let req = srv
.get("/")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.send();
let mut res = req.await.unwrap(); let mut res = req.await.unwrap();
assert!(res.status().is_success()); assert!(res.status().is_success());

76
awc/tests/utils.rs Normal file
View File

@@ -0,0 +1,76 @@
// compiling some tests will trigger unused function warnings even though other tests use them
#![allow(dead_code)]
use std::io::{Read as _, Write as _};
pub mod gzip {
use super::*;
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = GzDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod deflate {
use super::*;
use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = ZlibDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod brotli {
use super::*;
use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = BrotliEncoder::new(Vec::new(), 3);
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = BrotliDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod zstd {
use super::*;
use ::zstd::stream::{read::Decoder, write::Encoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = Encoder::new(Vec::new(), 3).unwrap();
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = Decoder::new(bytes.as_ref()).unwrap();
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}

View File

@@ -15,6 +15,7 @@ digraph {
"actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" } "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" }
"awc" -> { "actix-http" } "awc" -> { "actix-http" }
"actix-web-codegen" -> { "actix-router" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" }
"actix-multipart" -> { "actix-web" } "actix-multipart" -> { "actix-web" }
"actix-files" -> { "actix-web" } "actix-files" -> { "actix-web" }

View File

@@ -17,9 +17,18 @@ if [ "$(uname)" = "Darwin" ]; then
fi fi
CARGO_MANIFEST=$DIR/Cargo.toml CARGO_MANIFEST=$DIR/Cargo.toml
CHANGELOG_FILE=$DIR/CHANGES.md
README_FILE=$DIR/README.md README_FILE=$DIR/README.md
# determine changelog file name
if [ -f "$DIR/CHANGES.md" ]; then
CHANGELOG_FILE=$DIR/CHANGES.md
elif [ -f "$DIR/CHANGELOG.md" ]; then
CHANGELOG_FILE=$DIR/CHANGELOG.md
else
echo "No changelog file found"
exit 1
fi
# get current version # get current version
PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)" PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")" CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")"

View File

@@ -12,16 +12,16 @@ save_exit_code() {
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT [ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
} }
save_exit_code cargo test --lib --tests -p=actix-router --all-features save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http --all-features save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=awc --all-features save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-test --all-features save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-files save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
save_exit_code cargo test --workspace --doc save_exit_code cargo test --workspace --doc

View File

@@ -51,7 +51,10 @@ impl App<AppEntry> {
} }
} }
impl<T> App<T> { impl<T> App<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
/// Set application (root level) data. /// Set application (root level) data.
/// ///
/// Application data stored with `App::app_data()` method is available through the /// Application data stored with `App::app_data()` method is available through the
@@ -109,6 +112,7 @@ impl<T> App<T> {
/// .route("/", web::get().to(handler)) /// .route("/", web::get().to(handler))
/// }) /// })
/// ``` /// ```
#[doc(alias = "manage")]
pub fn app_data<U: 'static>(mut self, ext: U) -> Self { pub fn app_data<U: 'static>(mut self, ext: U) -> Self {
self.extensions.insert(ext); self.extensions.insert(ext);
self self
@@ -316,65 +320,63 @@ impl<T> App<T> {
self self
} }
/// Registers middleware, in the form of a middleware component (type), /// Registers an app-wide middleware.
/// that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as
/// necessary, across all requests managed by the *Application*.
/// ///
/// Use middleware when you need to read or modify *every* request or /// Registers middleware, in the form of a middleware compo nen t (type), that runs during
/// response in some way. /// inbound and/or outbound processing in the request life-cycle (request -> response),
/// modifying request/response as necessary, across all requests managed by the `App`.
/// ///
/// Notice that the keyword for registering middleware is `wrap`. As you /// Use middleware when you need to read or modify *every* request or response in some way.
/// register middleware using `wrap` in the App builder, imagine wrapping
/// layers around an inner App. The first middleware layer exposed to a
/// Request is the outermost layer-- the *last* registered in
/// the builder chain. Consequently, the *first* middleware registered
/// in the builder chain is the *last* to execute during request processing.
/// ///
/// Middleware can be applied similarly to individual `Scope`s and `Resource`s.
/// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`].
///
/// # Middleware Order
/// Notice that the keyword for registering middleware is `wrap`. As you register middleware
/// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first
/// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in
/// the builder chain). Consequently, the *first* middleware registered in the builder chain is
/// the *last* to start executing during request processing.
///
/// Ordering is less obvious when wrapped services also have middleware applied. In this case,
/// middlewares are run in reverse order for `App` _and then_ in reverse order for the
/// wrapped service.
///
/// # Examples
/// ``` /// ```
/// use actix_service::Service;
/// use actix_web::{middleware, web, App}; /// use actix_web::{middleware, web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
/// ///
/// async fn index() -> &'static str { /// async fn index() -> &'static str {
/// "Welcome!" /// "Welcome!"
/// } /// }
/// ///
/// fn main() { /// let app = App::new()
/// let app = App::new() /// .wrap(middleware::Logger::default())
/// .wrap(middleware::Logger::default()) /// .route("/index.html", web::get().to(index));
/// .route("/index.html", web::get().to(index));
/// }
/// ``` /// ```
pub fn wrap<M, B, B1>( #[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self, self,
mw: M, mw: M,
) -> App< ) -> App<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
> >
where where
T: ServiceFactory<
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
Config = (),
InitError = (),
>,
B: MessageBody,
M: Transform< M: Transform<
T::Service, T::Service,
ServiceRequest, ServiceRequest,
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, > + 'static,
B1: MessageBody, B: MessageBody,
{ {
App { App {
endpoint: apply(mw, self.endpoint), endpoint: apply(mw, self.endpoint),
@@ -387,61 +389,57 @@ impl<T> App<T> {
} }
} }
/// Registers middleware, in the form of a closure, that runs during inbound /// Registers an app-wide function middleware.
/// and/or outbound processing in the request life-cycle (request -> response), ///
/// modifying request/response as necessary, across all requests managed by /// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// the *Application*. /// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `App`.
/// ///
/// Use middleware when you need to read or modify *every* request or response in some way. /// Use middleware when you need to read or modify *every* request or response in some way.
/// ///
/// Middleware can also be applied to individual `Scope`s and `Resource`s.
///
/// See [`App::wrap`] for details on how middlewares compose with each other.
///
/// # Examples
/// ``` /// ```
/// use actix_service::Service; /// use actix_web::{dev::Service as _, middleware, web, App};
/// use actix_web::{web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue}; /// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
/// ///
/// async fn index() -> &'static str { /// async fn index() -> &'static str {
/// "Welcome!" /// "Welcome!"
/// } /// }
/// ///
/// fn main() { /// let app = App::new()
/// let app = App::new() /// .wrap_fn(|req, srv| {
/// .wrap_fn(|req, srv| { /// let fut = srv.call(req);
/// let fut = srv.call(req); /// async {
/// async { /// let mut res = fut.await?;
/// let mut res = fut.await?; /// res.headers_mut()
/// res.headers_mut().insert( /// .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"), /// Ok(res)
/// ); /// }
/// Ok(res) /// })
/// } /// .route("/index.html", web::get().to(index));
/// })
/// .route("/index.html", web::get().to(index));
/// }
/// ``` /// ```
pub fn wrap_fn<F, R, B, B1>( #[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap_fn<F, R, B>(
self, self,
mw: F, mw: F,
) -> App< ) -> App<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
> >
where where
T: ServiceFactory< F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
ServiceRequest, R: Future<Output = Result<ServiceResponse<B>, Error>>,
Response = ServiceResponse<B>,
Error = Error,
Config = (),
InitError = (),
>,
B: MessageBody, B: MessageBody,
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
R: Future<Output = Result<ServiceResponse<B1>, Error>>,
B1: MessageBody,
{ {
App { App {
endpoint: apply_fn_factory(self.endpoint, mw), endpoint: apply_fn_factory(self.endpoint, mw),
@@ -457,15 +455,14 @@ impl<T> App<T> {
impl<T, B> IntoServiceFactory<AppInit<T, B>, Request> for App<T> impl<T, B> IntoServiceFactory<AppInit<T, B>, Request> for App<T>
where where
B: MessageBody,
T: ServiceFactory< T: ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse<B>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, > + 'static,
T::Future: 'static, B: MessageBody,
{ {
fn into_factory(self) -> AppInit<T, B> { fn into_factory(self) -> AppInit<T, B> {
AppInit { AppInit {

View File

@@ -215,6 +215,17 @@ impl ServiceConfig {
self self
} }
/// Run external configuration as part of the application building process
///
/// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
pub fn configure<F>(&mut self, f: F) -> &mut Self
where
F: FnOnce(&mut ServiceConfig),
{
f(self);
self
}
/// Configure route for a specific path. /// Configure route for a specific path.
/// ///
/// Counterpart to [`App::route()`](crate::App::route). /// Counterpart to [`App::route()`](crate::App::route).
@@ -264,7 +275,7 @@ mod tests {
use super::*; use super::*;
use crate::http::{Method, StatusCode}; use crate::http::{Method, StatusCode};
use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest};
use crate::{web, App, HttpRequest, HttpResponse}; use crate::{web, App, HttpRequest, HttpResponse};
// allow deprecated `ServiceConfig::data` // allow deprecated `ServiceConfig::data`
@@ -288,38 +299,6 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
// #[actix_rt::test]
// async fn test_data_factory() {
// let cfg = |cfg: &mut ServiceConfig| {
// cfg.data_factory(|| {
// sleep(std::time::Duration::from_millis(50)).then(|_| {
// println!("READY");
// Ok::<_, ()>(10usize)
// })
// });
// };
// let srv =
// init_service(App::new().configure(cfg).service(
// web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()),
// ));
// let req = TestRequest::default().to_request();
// let resp = srv.call(req).await.unwrap();
// assert_eq!(resp.status(), StatusCode::OK);
// let cfg2 = |cfg: &mut ServiceConfig| {
// cfg.data_factory(|| Ok::<_, ()>(10u32));
// };
// let srv = init_service(
// App::new()
// .service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()))
// .configure(cfg2),
// );
// let req = TestRequest::default().to_request();
// let resp = srv.call(req).await.unwrap();
// assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
// }
#[actix_rt::test] #[actix_rt::test]
async fn test_external_resource() { async fn test_external_resource() {
let srv = init_service( let srv = init_service(
@@ -363,4 +342,22 @@ mod tests {
let resp = call_service(&srv, req).await; let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[actix_rt::test]
async fn nested_service_configure() {
fn cfg_root(cfg: &mut ServiceConfig) {
cfg.configure(cfg_sub);
}
fn cfg_sub(cfg: &mut ServiceConfig) {
cfg.route("/", web::get().to(|| async { "hello world" }));
}
let srv = init_service(App::new().configure(cfg_root)).await;
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_body_eq!(res, b"hello world");
}
} }

View File

@@ -19,23 +19,32 @@ pub(crate) trait DataFactory {
pub(crate) type FnDataFactory = pub(crate) type FnDataFactory =
Box<dyn Fn() -> LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>; Box<dyn Fn() -> LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>;
/// Application data. /// Application data wrapper and extractor.
/// ///
/// Application level data is a piece of arbitrary data attached to the app, scope, or resource. /// # Setting Data
/// Application data is available to all routes and can be added during the application /// Data is set using the `app_data` methods on `App`, `Scope`, and `Resource`. If data is wrapped
/// configuration process via `App::data()`. /// in this `Data` type for those calls, it can be used as an extractor.
/// ///
/// Application data can be accessed by using `Data<T>` extractor where `T` is data type. /// Note that `Data` should be constructed _outside_ the `HttpServer::new` closure if shared,
/// potentially mutable state is desired. `Data` is cheap to clone; internally, it uses an `Arc`.
/// ///
/// **Note**: HTTP server accepts an application factory rather than an application instance. HTTP /// See also [`App::app_data`](crate::App::app_data), [`Scope::app_data`](crate::Scope::app_data),
/// server constructs an application instance for each thread, thus application data must be /// and [`Resource::app_data`](crate::Resource::app_data).
/// constructed multiple times. If you want to share data between different threads, a shareable ///
/// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send` /// # Extracting `Data`
/// or `Sync`. Internally `Data` contains an `Arc`. /// Since the Actix Web router layers application data, the returned object will reference the
/// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope`
/// also stores a `u32`, and the delegated request handler falls within that `Scope`, then
/// extracting a `web::<Data<u32>>` for that handler will return the `Scope`'s instance.
/// However, using the same router set up and a request that does not get captured by the `Scope`,
/// `web::<Data<u32>>` would return the `App`'s instance.
/// ///
/// If route data is not set for a handler, using `Data<T>` extractor would cause a `500 Internal /// If route data is not set for a handler, using `Data<T>` extractor would cause a `500 Internal
/// Server Error` response. /// Server Error` response.
/// ///
/// See also [`HttpRequest::app_data`]
/// and [`ServiceRequest::app_data`](crate::dev::ServiceRequest::app_data).
///
/// # Unsized Data /// # Unsized Data
/// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first /// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first
/// constructing an `Arc<dyn T>` and using the `From` implementation to convert it. /// constructing an `Arc<dyn T>` and using the `From` implementation to convert it.
@@ -79,6 +88,7 @@ pub(crate) type FnDataFactory =
/// .route("/index.html", web::get().to(index)) /// .route("/index.html", web::get().to(index))
/// .route("/index-alt.html", web::get().to(index_alt)); /// .route("/index-alt.html", web::get().to(index_alt));
/// ``` /// ```
#[doc(alias = "state")]
#[derive(Debug)] #[derive(Debug)]
pub struct Data<T: ?Sized>(Arc<T>); pub struct Data<T: ?Sized>(Arc<T>);
@@ -90,12 +100,12 @@ impl<T> Data<T> {
} }
impl<T: ?Sized> Data<T> { impl<T: ?Sized> Data<T> {
/// Get reference to inner app data. /// Returns reference to inner `T`.
pub fn get_ref(&self) -> &T { pub fn get_ref(&self) -> &T {
self.0.as_ref() self.0.as_ref()
} }
/// Convert to the internal Arc<T> /// Unwraps to the internal `Arc<T>`
pub fn into_inner(self) -> Arc<T> { pub fn into_inner(self) -> Arc<T> {
self.0 self.0
} }
@@ -143,13 +153,16 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
ok(st.clone()) ok(st.clone())
} else { } else {
log::debug!( log::debug!(
"Failed to construct App-level Data extractor. \ "Failed to extract `Data<{}>` for `{}` handler. For the Data extractor to work \
Request path: {:?} (type: {})", correctly, wrap the data with `Data::new()` and pass it to `App::app_data()`. \
req.path(), Ensure that types align in both the set and retrieve calls.",
type_name::<T>(), type_name::<T>(),
req.match_name().unwrap_or_else(|| req.path())
); );
err(ErrorInternalServerError( err(ErrorInternalServerError(
"App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()", "Requested application data is not configured correctly. \
View/enable debug logs for more details.",
)) ))
} }
} }

View File

@@ -20,11 +20,7 @@ pub use crate::info::{ConnectionInfo, PeerAddr};
pub use crate::rmap::ResourceMap; pub use crate::rmap::ResourceMap;
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService};
pub use crate::types::form::UrlEncoded; pub use crate::types::{JsonBody, Readlines, UrlEncoded};
pub use crate::types::json::JsonBody;
pub use crate::types::readlines::Readlines;
use crate::http::header::ContentEncoding;
use actix_router::Patterns; use actix_router::Patterns;
@@ -46,60 +42,3 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
patterns patterns
} }
/// Helper trait that allows to set specific encoding for response.
pub trait BodyEncoding {
/// Get content encoding
fn get_encoding(&self) -> Option<ContentEncoding>;
/// Set content encoding
///
/// Must be used with [`crate::middleware::Compress`] to take effect.
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self;
}
impl BodyEncoding for actix_http::ResponseBuilder {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}
struct Enc(ContentEncoding);
impl<B> BodyEncoding for actix_http::Response<B> {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}
impl BodyEncoding for crate::HttpResponseBuilder {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}
impl<B> BodyEncoding for crate::HttpResponse<B> {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}

View File

@@ -3,6 +3,7 @@
use std::{ use std::{
convert::Infallible, convert::Infallible,
future::Future, future::Future,
marker::PhantomData,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
@@ -124,12 +125,11 @@ pub trait FromRequest: Sized {
/// ); /// );
/// } /// }
/// ``` /// ```
impl<T: 'static> FromRequest for Option<T> impl<T> FromRequest for Option<T>
where where
T: FromRequest, T: FromRequest,
T::Future: 'static,
{ {
type Error = Error; type Error = Infallible;
type Future = FromRequestOptFuture<T::Future>; type Future = FromRequestOptFuture<T::Future>;
#[inline] #[inline]
@@ -152,7 +152,7 @@ where
Fut: Future<Output = Result<T, E>>, Fut: Future<Output = Result<T, E>>,
E: Into<Error>, E: Into<Error>,
{ {
type Output = Result<Option<T>, Error>; type Output = Result<Option<T>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); let this = self.project();
@@ -211,40 +211,42 @@ where
/// ); /// );
/// } /// }
/// ``` /// ```
impl<T> FromRequest for Result<T, T::Error> impl<T, E> FromRequest for Result<T, E>
where where
T: FromRequest + 'static, T: FromRequest,
T::Error: 'static, T::Error: Into<E>,
T::Future: 'static,
{ {
type Error = Error; type Error = Infallible;
type Future = FromRequestResFuture<T::Future>; type Future = FromRequestResFuture<T::Future, E>;
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
FromRequestResFuture { FromRequestResFuture {
fut: T::from_request(req, payload), fut: T::from_request(req, payload),
_phantom: PhantomData,
} }
} }
} }
pin_project! { pin_project! {
pub struct FromRequestResFuture<Fut> { pub struct FromRequestResFuture<Fut, E> {
#[pin] #[pin]
fut: Fut, fut: Fut,
_phantom: PhantomData<E>,
} }
} }
impl<Fut, T, E> Future for FromRequestResFuture<Fut> impl<Fut, T, Ei, E> Future for FromRequestResFuture<Fut, E>
where where
Fut: Future<Output = Result<T, E>>, Fut: Future<Output = Result<T, Ei>>,
Ei: Into<E>,
{ {
type Output = Result<Result<T, E>, Error>; type Output = Result<Result<T, E>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); let this = self.project();
let res = ready!(this.fut.poll(cx)); let res = ready!(this.fut.poll(cx));
Poll::Ready(Ok(res)) Poll::Ready(Ok(res.map_err(Into::into)))
} }
} }
@@ -290,16 +292,6 @@ impl FromRequest for Method {
} }
} }
#[doc(hidden)]
impl FromRequest for () {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(())
}
}
#[doc(hidden)] #[doc(hidden)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
mod tuple_from_req { mod tuple_from_req {
@@ -388,6 +380,15 @@ mod tuple_from_req {
} }
} }
impl FromRequest for () {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(())
}
}
tuple_from_req! { TupleFromRequest1; A } tuple_from_req! { TupleFromRequest1; A }
tuple_from_req! { TupleFromRequest2; A, B } tuple_from_req! { TupleFromRequest2; A, B }
tuple_from_req! { TupleFromRequest3; A, B, C } tuple_from_req! { TupleFromRequest3; A, B, C }
@@ -398,6 +399,8 @@ mod tuple_from_req {
tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H } tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H }
tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I } tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I }
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J } tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
} }
#[cfg(test)] #[cfg(test)]
@@ -480,7 +483,14 @@ mod tests {
.set_payload(Bytes::from_static(b"bye=world")) .set_payload(Bytes::from_static(b"bye=world"))
.to_http_parts(); .to_http_parts();
let r = Result::<Form<Info>, Error>::from_request(&req, &mut pl) struct MyError;
impl From<Error> for MyError {
fn from(_: Error) -> Self {
Self
}
}
let r = Result::<Form<Info>, MyError>::from_request(&req, &mut pl)
.await .await
.unwrap(); .unwrap();
assert!(r.is_err()); assert!(r.is_err());

View File

@@ -54,7 +54,7 @@ use std::{
use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead};
use crate::service::ServiceRequest; use crate::{http::header::Header, service::ServiceRequest};
/// Provides access to request parts that are useful during routing. /// Provides access to request parts that are useful during routing.
#[derive(Debug)] #[derive(Debug)]
@@ -80,6 +80,26 @@ impl<'a> GuardContext<'a> {
pub fn req_data_mut(&self) -> RefMut<'a, Extensions> { pub fn req_data_mut(&self) -> RefMut<'a, Extensions> {
self.req.req_data_mut() self.req.req_data_mut()
} }
/// Extracts a typed header from the request.
///
/// Returns `None` if parsing `H` fails.
///
/// # Examples
/// ```
/// use actix_web::{guard::fn_guard, http::header};
///
/// let image_accept_guard = fn_guard(|ctx| {
/// match ctx.header::<header::Accept>() {
/// Some(hdr) => hdr.preference() == "image/*",
/// None => false,
/// }
/// });
/// ```
#[inline]
pub fn header<H: Header>(&self) -> Option<H> {
H::parse(self.req).ok()
}
} }
/// Interface for routing guards. /// Interface for routing guards.

View File

@@ -12,17 +12,14 @@ use crate::{
/// # What Is A Request Handler /// # What Is A Request Handler
/// A request handler has three requirements: /// A request handler has three requirements:
/// 1. It is an async function (or a function/closure that returns an appropriate future); /// 1. It is an async function (or a function/closure that returns an appropriate future);
/// 1. The function accepts zero or more parameters that implement [`FromRequest`]; /// 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 /// 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 /// # Compiler Errors
/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not /// If you get the error `the trait Handler<_> is not implemented`, then your handler does not
/// fulfill one or more of the above requirements. /// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on
/// /// implementing [`FromRequest`] and [`Responder`], respectively.
/// Unfortunately we cannot provide a better compile error message (while keeping the trait's
/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added
/// to Rust.
/// ///
/// # How Do Handlers Receive Variable Numbers Of Arguments /// # How Do Handlers Receive Variable Numbers Of Arguments
/// Rest assured there is no macro magic here; it's just traits. /// Rest assured there is no macro magic here; it's just traits.
@@ -62,13 +59,15 @@ use crate::{
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the /// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
/// bounds of the handler call after argument extraction: /// bounds of the handler call after argument extraction:
/// ```ignore /// ```ignore
/// impl<Func, Arg1, Arg2, R> Handler<(Arg1, Arg2), R> for Func /// impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func
/// where /// where
/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static, /// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
/// R: Future, /// Fut: Future,
/// R::Output: Responder,
/// { /// {
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R { /// type Output = Fut::Output;
/// type Future = Fut;
///
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future {
/// (self)(arg1, arg2) /// (self)(arg1, arg2)
/// } /// }
/// } /// }
@@ -76,7 +75,6 @@ use crate::{
/// ///
/// [arity]: https://en.wikipedia.org/wiki/Arity /// [arity]: https://en.wikipedia.org/wiki/Arity
/// [`from_request`]: FromRequest::from_request /// [`from_request`]: FromRequest::from_request
/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628
pub trait Handler<Args>: Clone + 'static { pub trait Handler<Args>: Clone + 'static {
type Output; type Output;
type Future: Future<Output = Self::Output>; type Future: Future<Output = Self::Output>;
@@ -121,8 +119,9 @@ where
/// ``` /// ```
macro_rules! factory_tuple ({ $($param:ident)* } => { macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func
where Func: Fn($($param),*) -> Fut + Clone + 'static, where
Fut: Future, Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{ {
type Output = Fut::Output; type Output = Fut::Output;
type Future = Fut; type Future = Fut;
@@ -148,3 +147,25 @@ factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J } factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K } factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L } factory_tuple! { A B C D E F G H I J K L }
#[cfg(test)]
mod tests {
use super::*;
fn assert_impl_handler<T: FromRequest>(_: impl Handler<T>) {}
#[test]
fn arg_number() {
async fn handler_min() {}
#[rustfmt::skip]
#[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits)]
async fn handler_max(
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
) {}
assert_impl_handler(handler_min);
assert_impl_handler(handler_max);
}
}

View File

@@ -2,10 +2,10 @@ use std::cmp::Ordering;
use mime::Mime; use mime::Mime;
use super::QualityItem; use super::{common_header, QualityItem};
use crate::http::header; use crate::http::header;
crate::http::header::common_header! { common_header! {
/// `Accept` header, defined /// `Accept` header, defined
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) /// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
/// ///
@@ -147,6 +147,39 @@ impl Accept {
Accept(vec![QualityItem::max(mime::TEXT_HTML)]) Accept(vec![QualityItem::max(mime::TEXT_HTML)])
} }
// TODO: method for getting best content encoding based on q-factors, available from server side
// and if none are acceptable return None
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
/// list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Mime {
use actix_http::header::Quality;
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(mime::STAR_STAR)
}
/// Returns a sorted list of mime types from highest to lowest preference, accounting for /// Returns a sorted list of mime types from highest to lowest preference, accounting for
/// [q-factor weighting] and specificity. /// [q-factor weighting] and specificity.
/// ///
@@ -196,36 +229,6 @@ impl Accept {
types.into_iter().map(|qitem| qitem.item).collect() types.into_iter().map(|qitem| qitem.item).collect()
} }
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
/// list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Mime {
use actix_http::header::Quality;
let mut max_item = None;
let mut max_pref = Quality::MIN;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(mime::STAR_STAR)
}
} }
#[cfg(test)] #[cfg(test)]
@@ -239,7 +242,7 @@ mod tests {
assert!(test.ranked().is_empty()); assert!(test.ranked().is_empty());
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]); let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON)); assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
let test = Accept(vec![ let test = Accept(vec![
QualityItem::max(mime::TEXT_HTML), QualityItem::max(mime::TEXT_HTML),

View File

@@ -1,8 +1,7 @@
use super::{Charset, QualityItem, ACCEPT_CHARSET}; use super::{common_header, Charset, QualityItem, ACCEPT_CHARSET};
crate::http::header::common_header! { common_header! {
/// `Accept-Charset` header, defined in /// `Accept-Charset` header, defined in [RFC 7231 §5.3.3].
/// [RFC 7231 §5.3.3](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3)
/// ///
/// The `Accept-Charset` header field can be sent by a user agent to /// The `Accept-Charset` header field can be sent by a user agent to
/// indicate what charsets are acceptable in textual response content. /// indicate what charsets are acceptable in textual response content.
@@ -52,10 +51,12 @@ crate::http::header::common_header! {
/// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))]) /// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])
/// ); /// );
/// ``` /// ```
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+ ///
/// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)*
test_parse_and_format { test_parse_and_format {
// Test case from RFC // Test case from RFC
crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
} }
} }

View File

@@ -1,17 +1,15 @@
use actix_http::header::QualityItem; use std::collections::HashSet;
use super::{common_header, Encoding}; use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem};
use crate::http::header; use crate::http::header;
common_header! { common_header! {
/// `Accept-Encoding` header, defined /// `Accept-Encoding` header, defined
/// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
/// ///
/// The `Accept-Encoding` header field can be used by user agents to /// The `Accept-Encoding` header field can be used by user agents to indicate what response
/// indicate what response content-codings are /// content-codings are acceptable in the response. An `identity` token is used as a synonym
/// acceptable in the response. An `identity` token is used as a synonym /// for "no encoding" in order to communicate when no encoding is preferred.
/// for "no encoding" in order to communicate when no encoding is
/// preferred.
/// ///
/// # ABNF /// # ABNF
/// ```plain /// ```plain
@@ -29,11 +27,11 @@ common_header! {
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_web::HttpResponse; /// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem}; /// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)]) /// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))])
/// ); /// );
/// ``` /// ```
/// ///
@@ -44,40 +42,388 @@ common_header! {
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptEncoding(vec![ /// AcceptEncoding(vec![
/// QualityItem::max(Encoding::Chunked), /// "gzip".parse().unwrap(),
/// QualityItem::max(Encoding::Gzip), /// "br".parse().unwrap(),
/// QualityItem::max(Encoding::Deflate),
/// ]) /// ])
/// ); /// );
/// ``` /// ```
/// (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Preference<Encoding>>)*
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptEncoding(vec![
/// QualityItem::max(Encoding::Chunked),
/// QualityItem::new(Encoding::Gzip, q(0.60)),
/// QualityItem::min(Encoding::EncodingExt("*".to_owned())),
/// ])
/// );
/// ```
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Encoding>)*
test_parse_and_format { test_parse_and_format {
// From the RFC common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
common_header_test!(test1, vec![b"compress, gzip"]); common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![])));
common_header_test!(test3, vec![b"*"]); common_header_test!(
order_of_appearance,
vec![b"br, gzip"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::brotli())),
QualityItem::max(Preference::Specific(Encoding::gzip())),
]))
);
common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![
QualityItem::max(Preference::Any),
])));
// Note: Removed quality 1 from gzip // Note: Removed quality 1 from gzip
common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]);
// Note: Removed quality 1 from gzip // Note: Removed quality 1 from gzip
common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]);
common_header_test!(
only_gzip_no_identity,
vec![b"gzip, *; q=0"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::gzip())),
QualityItem::zero(Preference::Any),
]))
);
} }
} }
// TODO: shortcut for EncodingExt(*) = Any impl AcceptEncoding {
/// Selects the most acceptable encoding according to client preference and supported types.
///
/// The "identity" encoding is not assumed and should be included in the `supported` iterator
/// if a non-encoded representation can be selected.
///
/// If `None` is returned, this indicates that none of the supported encodings are acceptable to
/// the client. The caller should generate a 406 Not Acceptable response (unencoded) that
/// includes the server's supported encodings in the body plus a [`Vary`] header.
///
/// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
pub fn negotiate<'a>(
&self,
supported: impl Iterator<Item = &'a Encoding>,
) -> Option<Encoding> {
// 1. If no Accept-Encoding field is in the request, any content-coding is considered
// acceptable by the user agent.
let supported_set = supported.collect::<HashSet<_>>();
if supported_set.is_empty() {
return None;
}
if self.0.is_empty() {
// though it is not recommended to encode in this case, return identity encoding
return Some(Encoding::identity());
}
// 2. If the representation has no content-coding, then it is acceptable by default unless
// specifically excluded by the Accept-Encoding field stating either "identity;q=0" or
// "*;q=0" without a more specific entry for "identity".
let acceptable_items = self.ranked_items().collect::<Vec<_>>();
let identity_acceptable = is_identity_acceptable(&acceptable_items);
let identity_supported = supported_set.contains(&Encoding::identity());
if identity_acceptable && identity_supported && supported_set.len() == 1 {
return Some(Encoding::identity());
}
// 3. If the representation's content-coding is one of the content-codings listed in the
// Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0.
// 4. If multiple content-codings are acceptable, then the acceptable content-coding with
// the highest non-zero qvalue is preferred.
let matched = acceptable_items
.into_iter()
.filter(|q| q.quality > Quality::ZERO)
// search relies on item list being in descending order of quality
.find(|q| {
let enc = &q.item;
matches!(enc, Preference::Specific(enc) if supported_set.contains(enc))
})
.map(|q| q.item);
match matched {
Some(Preference::Specific(enc)) => Some(enc),
_ if identity_acceptable => Some(Encoding::identity()),
_ => None,
}
}
/// Extracts the most preferable encoding, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first encoding is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is
/// returned, it is recommended to use an un-encoded representation.
///
/// If `None` is returned, it means that the client has signalled that no representations
/// are acceptable. This should never occur for a well behaved user-agent.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Option<Preference<Encoding>> {
// empty header indicates no preference
if self.0.is_empty() {
return Some(Preference::Any);
}
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
// Return max_item if any items were above 0 quality...
max_item.or_else(|| {
// ...or else check for "*" or "identity". We can elide quality checks since
// entering this block means all items had "q=0".
match self.0.iter().find(|pref| {
matches!(
pref.item,
Preference::Any
| Preference::Specific(Encoding::Known(ContentEncoding::Identity))
)
}) {
// "identity" or "*" found so no representation is acceptable
Some(_) => None,
// implicit "identity" is acceptable
None => Some(Preference::Specific(Encoding::identity())),
}
})
}
/// Returns a sorted list of encodings from highest to lowest precedence, accounting
/// for [q-factor weighting].
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn ranked(&self) -> Vec<Preference<Encoding>> {
self.ranked_items().map(|q| q.item).collect()
}
fn ranked_items(&self) -> impl Iterator<Item = QualityItem<Preference<Encoding>>> {
if self.0.is_empty() {
return vec![].into_iter();
}
let mut types = self.0.clone();
// use stable sort so items with equal q-factor retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality)
});
types.into_iter()
}
}
/// Returns true if "identity" is an acceptable encoding.
///
/// Internal algorithm relies on item list being in descending order of quality.
fn is_identity_acceptable(items: &'_ [QualityItem<Preference<Encoding>>]) -> bool {
if items.is_empty() {
return true;
}
// Loop algorithm depends on items being sorted in descending order of quality. As such, it
// is sufficient to return (q > 0) when reaching either an "identity" or "*" item.
for q in items {
match (q.quality, &q.item) {
// occurrence of "identity;q=n"; return true if quality is non-zero
(q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => {
return q > Quality::ZERO
}
// occurrence of "*;q=n"; return true if quality is non-zero
(q, Preference::Any) => return q > Quality::ZERO,
_ => {}
}
}
// implicit acceptable identity
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::header::*;
macro_rules! accept_encoding {
() => { AcceptEncoding(vec![]) };
($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) };
}
/// Parses an encoding string.
fn enc(enc: &str) -> Preference<Encoding> {
enc.parse().unwrap()
}
#[test]
fn detect_identity_acceptable() {
macro_rules! accept_encoding_ranked {
() => { accept_encoding!().ranked_items().collect::<Vec<_>>() };
($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::<Vec<_>>() };
}
let test = accept_encoding_ranked!();
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "br");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0");
assert!(!is_identity_acceptable(&test));
}
#[test]
fn encoding_negotiation() {
// no preference
let test = accept_encoding!();
assert_eq!(test.negotiate([].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("identity;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!("*;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("gzip");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
let test = accept_encoding!("gzip", "identity;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "*;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "deflate", "br");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::brotli())
);
assert_eq!(
test.negotiate([Encoding::deflate(), Encoding::identity()].iter()),
Some(Encoding::deflate())
);
assert_eq!(
test.negotiate(
[Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()
),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
}
#[test]
fn ranking_precedence() {
let test = accept_encoding!();
assert!(test.ranked().is_empty());
let test = accept_encoding!("gzip");
assert_eq!(test.ranked(), vec![enc("gzip")]);
let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
}
#[test]
fn preference_selection() {
assert_eq!(accept_encoding!().preference(), Some(Preference::Any));
assert_eq!(accept_encoding!("identity;q=0").preference(), None);
assert_eq!(accept_encoding!("*;q=0").preference(), None);
assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None);
assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None);
let test = accept_encoding!("*;q=0.5");
assert_eq!(test.preference().unwrap(), enc("*"));
let test = accept_encoding!("br;q=0");
assert_eq!(test.preference().unwrap(), enc("identity"));
let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500");
assert_eq!(test.preference().unwrap(), enc("gzip"));
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.preference().unwrap(), enc("br"));
}
}

View File

@@ -37,7 +37,7 @@ common_header! {
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// QualityItem::max("en-US".parse().unwrap()) /// "en-US".parse().unwrap(),
/// ]) /// ])
/// ); /// );
/// ``` /// ```
@@ -49,9 +49,9 @@ common_header! {
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// QualityItem::max("da".parse().unwrap()), /// "da".parse().unwrap(),
/// QualityItem::new("en-GB".parse().unwrap(), q(0.8)), /// "en-GB;q=0.8".parse().unwrap(),
/// QualityItem::new("en".parse().unwrap(), q(0.7)), /// "en;q=0.7".parse().unwrap(),
/// ]) /// ])
/// ); /// );
/// ``` /// ```
@@ -93,6 +93,33 @@ common_header! {
} }
impl AcceptLanguage { impl AcceptLanguage {
/// Extracts the most preferable language, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first language is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Preference<LanguageTag> {
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(Preference::Any)
}
/// Returns a sorted list of languages from highest to lowest precedence, accounting /// Returns a sorted list of languages from highest to lowest precedence, accounting
/// for [q-factor weighting]. /// for [q-factor weighting].
/// ///
@@ -112,33 +139,6 @@ impl AcceptLanguage {
types.into_iter().map(|qitem| qitem.item).collect() types.into_iter().map(|qitem| qitem.item).collect()
} }
/// Extracts the most preferable language, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first language is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Preference<LanguageTag> {
let mut max_item = None;
let mut max_pref = Quality::MIN;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(Preference::Any)
}
} }
#[cfg(test)] #[cfg(test)]
@@ -152,7 +152,7 @@ mod tests {
assert!(test.ranked().is_empty()); assert!(test.ranked().is_empty());
let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]); let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap())); assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]);
let test = AcceptLanguage(vec![ let test = AcceptLanguage(vec![
QualityItem::new("fr".parse().unwrap(), q(0.900)), QualityItem::new("fr".parse().unwrap(), q(0.900)),

View File

@@ -301,7 +301,6 @@ impl DispositionParam {
/// change to match local file system conventions if applicable, and do not use directory path /// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present. /// information that may be present.
/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). /// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
// TODO: think about using private fields and smallvec
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition { pub struct ContentDisposition {
/// The disposition type /// The disposition type

View File

@@ -1,69 +1,55 @@
use std::{fmt, str}; use std::{fmt, str};
pub use self::Encoding::{ use actix_http::ContentEncoding;
Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd,
};
/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header. /// A value to represent an encoding used in the `Accept-Encoding` and `Content-Encoding` header.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Encoding { pub enum Encoding {
/// The `chunked` encoding. /// A supported content encoding. See [`ContentEncoding`] for variants.
Chunked, Known(ContentEncoding),
/// The `br` encoding. /// Some other encoding that is less common, can be any string.
Brotli, Unknown(String),
}
/// The `gzip` encoding. impl Encoding {
Gzip, pub const fn identity() -> Self {
Self::Known(ContentEncoding::Identity)
}
/// The `deflate` encoding. pub const fn brotli() -> Self {
Deflate, Self::Known(ContentEncoding::Brotli)
}
/// The `compress` encoding. pub const fn deflate() -> Self {
Compress, Self::Known(ContentEncoding::Deflate)
}
/// The `identity` encoding. pub const fn gzip() -> Self {
Identity, Self::Known(ContentEncoding::Gzip)
}
/// The `trailers` encoding. pub const fn zstd() -> Self {
Trailers, Self::Known(ContentEncoding::Zstd)
}
/// The `zstd` encoding.
Zstd,
/// Some other encoding that is less common, can be any String.
EncodingExt(String),
} }
impl fmt::Display for Encoding { impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match *self { f.write_str(match self {
Chunked => "chunked", Encoding::Known(enc) => enc.as_str(),
Brotli => "br", Encoding::Unknown(enc) => enc.as_str(),
Gzip => "gzip",
Deflate => "deflate",
Compress => "compress",
Identity => "identity",
Trailers => "trailers",
Zstd => "zstd",
EncodingExt(ref s) => s.as_ref(),
}) })
} }
} }
impl str::FromStr for Encoding { impl str::FromStr for Encoding {
type Err = crate::error::ParseError; type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
match s { fn from_str(enc: &str) -> Result<Self, crate::error::ParseError> {
"chunked" => Ok(Chunked), match enc.parse::<ContentEncoding>() {
"br" => Ok(Brotli), Ok(enc) => Ok(Self::Known(enc)),
"deflate" => Ok(Deflate), Err(_) => Ok(Self::Unknown(enc.to_owned())),
"gzip" => Ok(Gzip),
"compress" => Ok(Compress),
"identity" => Ok(Identity),
"trailers" => Ok(Trailers),
"zstd" => Ok(Zstd),
_ => Ok(EncodingExt(s.to_owned())),
} }
} }
} }

View File

@@ -17,8 +17,7 @@ fn check_slice_validity(slice: &str) -> bool {
slice.bytes().all(entity_validate_char) slice.bytes().all(entity_validate_char)
} }
/// An entity tag, defined /// An entity tag, defined in [RFC 7232 §2.3].
/// in [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3)
/// ///
/// An entity tag consists of a string enclosed by two literal double quotes. /// An entity tag consists of a string enclosed by two literal double quotes.
/// Preceding the first double quote is an optional weakness indicator, /// Preceding the first double quote is an optional weakness indicator,
@@ -48,16 +47,20 @@ fn check_slice_validity(slice: &str) -> bool {
/// | `W/"1"` | `W/"2"` | no match | no match | /// | `W/"1"` | `W/"2"` | no match | no match |
/// | `W/"1"` | `"1"` | no match | match | /// | `W/"1"` | `"1"` | no match | match |
/// | `"1"` | `"1"` | match | match | /// | `"1"` | `"1"` | match | match |
#[derive(Clone, Debug, Eq, PartialEq)] ///
/// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntityTag { pub struct EntityTag {
/// Weakness indicator for the tag /// Weakness indicator for the tag
pub weak: bool, pub weak: bool,
/// The opaque string in between the DQUOTEs /// The opaque string in between the DQUOTEs
tag: String, tag: String,
} }
impl EntityTag { impl EntityTag {
/// Constructs a new EntityTag. /// Constructs a new `EntityTag`.
///
/// # Panics /// # Panics
/// If the tag contains invalid characters. /// If the tag contains invalid characters.
pub fn new(weak: bool, tag: String) -> EntityTag { pub fn new(weak: bool, tag: String) -> EntityTag {
@@ -66,51 +69,64 @@ impl EntityTag {
} }
/// Constructs a new weak EntityTag. /// Constructs a new weak EntityTag.
///
/// # Panics /// # Panics
/// If the tag contains invalid characters. /// If the tag contains invalid characters.
pub fn weak(tag: String) -> EntityTag { pub fn new_weak(tag: String) -> EntityTag {
EntityTag::new(true, tag) EntityTag::new(true, tag)
} }
#[deprecated(since = "3.0.0", note = "Renamed to `new_weak`.")]
pub fn weak(tag: String) -> EntityTag {
Self::new_weak(tag)
}
/// Constructs a new strong EntityTag. /// Constructs a new strong EntityTag.
///
/// # Panics /// # Panics
/// If the tag contains invalid characters. /// If the tag contains invalid characters.
pub fn strong(tag: String) -> EntityTag { pub fn new_strong(tag: String) -> EntityTag {
EntityTag::new(false, tag) EntityTag::new(false, tag)
} }
/// Get the tag. #[deprecated(since = "3.0.0", note = "Renamed to `new_strong`.")]
pub fn strong(tag: String) -> EntityTag {
Self::new_strong(tag)
}
/// Returns tag.
pub fn tag(&self) -> &str { pub fn tag(&self) -> &str {
self.tag.as_ref() self.tag.as_ref()
} }
/// Set the tag. /// Sets tag.
///
/// # Panics /// # Panics
/// If the tag contains invalid characters. /// If the tag contains invalid characters.
pub fn set_tag(&mut self, tag: String) { pub fn set_tag(&mut self, tag: impl Into<String>) {
let tag = tag.into();
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
self.tag = tag self.tag = tag
} }
/// For strong comparison two entity-tags are equivalent if both are not /// For strong comparison two entity-tags are equivalent if both are not weak and their
/// weak and their opaque-tags match character-by-character. /// opaque-tags match character-by-character.
pub fn strong_eq(&self, other: &EntityTag) -> bool { pub fn strong_eq(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag == other.tag !self.weak && !other.weak && self.tag == other.tag
} }
/// For weak comparison two entity-tags are equivalent if their /// For weak comparison two entity-tags are equivalent if their opaque-tags match
/// opaque-tags match character-by-character, regardless of either or /// character-by-character, regardless of either or both being tagged as "weak".
/// both being tagged as "weak".
pub fn weak_eq(&self, other: &EntityTag) -> bool { pub fn weak_eq(&self, other: &EntityTag) -> bool {
self.tag == other.tag self.tag == other.tag
} }
/// The inverse of `EntityTag.strong_eq()`. /// Returns the inverse of `strong_eq()`.
pub fn strong_ne(&self, other: &EntityTag) -> bool { pub fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other) !self.strong_eq(other)
} }
/// The inverse of `EntityTag.weak_eq()`. /// Returns inverse of `weak_eq()`.
pub fn weak_ne(&self, other: &EntityTag) -> bool { pub fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other) !self.weak_eq(other)
} }
@@ -178,23 +194,23 @@ mod tests {
// Expected success // Expected success
assert_eq!( assert_eq!(
"\"foobar\"".parse::<EntityTag>().unwrap(), "\"foobar\"".parse::<EntityTag>().unwrap(),
EntityTag::strong("foobar".to_owned()) EntityTag::new_strong("foobar".to_owned())
); );
assert_eq!( assert_eq!(
"\"\"".parse::<EntityTag>().unwrap(), "\"\"".parse::<EntityTag>().unwrap(),
EntityTag::strong("".to_owned()) EntityTag::new_strong("".to_owned())
); );
assert_eq!( assert_eq!(
"W/\"weaktag\"".parse::<EntityTag>().unwrap(), "W/\"weaktag\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("weaktag".to_owned()) EntityTag::new_weak("weaktag".to_owned())
); );
assert_eq!( assert_eq!(
"W/\"\x65\x62\"".parse::<EntityTag>().unwrap(), "W/\"\x65\x62\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("\x65\x62".to_owned()) EntityTag::new_weak("\x65\x62".to_owned())
); );
assert_eq!( assert_eq!(
"W/\"\"".parse::<EntityTag>().unwrap(), "W/\"\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("".to_owned()) EntityTag::new_weak("".to_owned())
); );
} }
@@ -214,19 +230,19 @@ mod tests {
#[test] #[test]
fn test_etag_fmt() { fn test_etag_fmt() {
assert_eq!( assert_eq!(
format!("{}", EntityTag::strong("foobar".to_owned())), format!("{}", EntityTag::new_strong("foobar".to_owned())),
"\"foobar\"" "\"foobar\""
); );
assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); assert_eq!(format!("{}", EntityTag::new_strong("".to_owned())), "\"\"");
assert_eq!( assert_eq!(
format!("{}", EntityTag::weak("weak-etag".to_owned())), format!("{}", EntityTag::new_weak("weak-etag".to_owned())),
"W/\"weak-etag\"" "W/\"weak-etag\""
); );
assert_eq!( assert_eq!(
format!("{}", EntityTag::weak("\u{0065}".to_owned())), format!("{}", EntityTag::new_weak("\u{0065}".to_owned())),
"W/\"\x65\"" "W/\"\x65\""
); );
assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); assert_eq!(format!("{}", EntityTag::new_weak("".to_owned())), "W/\"\"");
} }
#[test] #[test]
@@ -237,29 +253,29 @@ mod tests {
// | `W/"1"` | `W/"2"` | no match | no match | // | `W/"1"` | `W/"2"` | no match | no match |
// | `W/"1"` | `"1"` | no match | match | // | `W/"1"` | `"1"` | no match | match |
// | `"1"` | `"1"` | match | match | // | `"1"` | `"1"` | match | match |
let mut etag1 = EntityTag::weak("1".to_owned()); let mut etag1 = EntityTag::new_weak("1".to_owned());
let mut etag2 = EntityTag::weak("1".to_owned()); let mut etag2 = EntityTag::new_weak("1".to_owned());
assert!(!etag1.strong_eq(&etag2)); assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2)); assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2)); assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2)); assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned()); etag1 = EntityTag::new_weak("1".to_owned());
etag2 = EntityTag::weak("2".to_owned()); etag2 = EntityTag::new_weak("2".to_owned());
assert!(!etag1.strong_eq(&etag2)); assert!(!etag1.strong_eq(&etag2));
assert!(!etag1.weak_eq(&etag2)); assert!(!etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2)); assert!(etag1.strong_ne(&etag2));
assert!(etag1.weak_ne(&etag2)); assert!(etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned()); etag1 = EntityTag::new_weak("1".to_owned());
etag2 = EntityTag::strong("1".to_owned()); etag2 = EntityTag::new_strong("1".to_owned());
assert!(!etag1.strong_eq(&etag2)); assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2)); assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2)); assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2)); assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::strong("1".to_owned()); etag1 = EntityTag::new_strong("1".to_owned());
etag2 = EntityTag::strong("1".to_owned()); etag2 = EntityTag::new_strong("1".to_owned());
assert!(etag1.strong_eq(&etag2)); assert!(etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2)); assert!(etag1.weak_eq(&etag2));
assert!(!etag1.strong_ne(&etag2)); assert!(!etag1.strong_ne(&etag2));

View File

@@ -31,7 +31,7 @@ crate::http::header::common_header! {
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// ETag(EntityTag::new(false, "xyzzy".to_owned())) /// ETag(EntityTag::new_strong("xyzzy".to_owned()))
/// ); /// );
/// ``` /// ```
/// ///
@@ -41,7 +41,7 @@ crate::http::header::common_header! {
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// ETag(EntityTag::new(true, "xyzzy".to_owned())) /// ETag(EntityTag::new_weak("xyzzy".to_owned()))
/// ); /// );
/// ``` /// ```
(ETag, ETAG) => [EntityTag] (ETag, ETAG) => [EntityTag]
@@ -50,29 +50,29 @@ crate::http::header::common_header! {
// From the RFC // From the RFC
crate::http::header::common_header_test!(test1, crate::http::header::common_header_test!(test1,
vec![b"\"xyzzy\""], vec![b"\"xyzzy\""],
Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); Some(ETag(EntityTag::new_strong("xyzzy".to_owned()))));
crate::http::header::common_header_test!(test2, crate::http::header::common_header_test!(test2,
vec![b"W/\"xyzzy\""], vec![b"W/\"xyzzy\""],
Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); Some(ETag(EntityTag::new_weak("xyzzy".to_owned()))));
crate::http::header::common_header_test!(test3, crate::http::header::common_header_test!(test3,
vec![b"\"\""], vec![b"\"\""],
Some(ETag(EntityTag::new(false, "".to_owned())))); Some(ETag(EntityTag::new_strong("".to_owned()))));
// Own tests // Own tests
crate::http::header::common_header_test!(test4, crate::http::header::common_header_test!(test4,
vec![b"\"foobar\""], vec![b"\"foobar\""],
Some(ETag(EntityTag::new(false, "foobar".to_owned())))); Some(ETag(EntityTag::new_strong("foobar".to_owned()))));
crate::http::header::common_header_test!(test5, crate::http::header::common_header_test!(test5,
vec![b"\"\""], vec![b"\"\""],
Some(ETag(EntityTag::new(false, "".to_owned())))); Some(ETag(EntityTag::new_strong("".to_owned()))));
crate::http::header::common_header_test!(test6, crate::http::header::common_header_test!(test6,
vec![b"W/\"weak-etag\""], vec![b"W/\"weak-etag\""],
Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); Some(ETag(EntityTag::new_weak("weak-etag".to_owned()))));
crate::http::header::common_header_test!(test7, crate::http::header::common_header_test!(test7,
vec![b"W/\"\x65\x62\""], vec![b"W/\"\x65\x62\""],
Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); Some(ETag(EntityTag::new_weak("\u{0065}\u{0062}".to_owned()))));
crate::http::header::common_header_test!(test8, crate::http::header::common_header_test!(test8,
vec![b"W/\"\""], vec![b"W/\"\""],
Some(ETag(EntityTag::new(true, "".to_owned())))); Some(ETag(EntityTag::new_weak("".to_owned()))));
crate::http::header::common_header_test!(test9, crate::http::header::common_header_test!(test9,
vec![b"no-dquotes"], vec![b"no-dquotes"],
None::<ETag>); None::<ETag>);

View File

@@ -54,14 +54,15 @@ common_header! {
test1, test1,
vec![b"\"xyzzy\""], vec![b"\"xyzzy\""],
Some(HeaderField::Items( Some(HeaderField::Items(
vec![EntityTag::new(false, "xyzzy".to_owned())]))); vec![EntityTag::new_strong("xyzzy".to_owned())])));
crate::http::header::common_header_test!( crate::http::header::common_header_test!(
test2, test2,
vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""],
Some(HeaderField::Items( Some(HeaderField::Items(
vec![EntityTag::new(false, "xyzzy".to_owned()), vec![EntityTag::new_strong("xyzzy".to_owned()),
EntityTag::new(false, "r2d2xxxx".to_owned()), EntityTag::new_strong("r2d2xxxx".to_owned()),
EntityTag::new(false, "c3piozzzz".to_owned())]))); EntityTag::new_strong("c3piozzzz".to_owned())])));
crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any));
} }
} }

View File

@@ -82,8 +82,8 @@ mod tests {
if_none_match = Header::parse(&req); if_none_match = Header::parse(&req);
let mut entities: Vec<EntityTag> = Vec::new(); let mut entities: Vec<EntityTag> = Vec::new();
let foobar_etag = EntityTag::new(false, "foobar".to_owned()); let foobar_etag = EntityTag::new_strong("foobar".to_owned());
let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); let weak_etag = EntityTag::new_weak("weak-etag".to_owned());
entities.push(foobar_etag); entities.push(foobar_etag);
entities.push(weak_etag); entities.push(weak_etag);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities)));

View File

@@ -150,11 +150,13 @@ mod tests {
use actix_service::IntoService; use actix_service::IntoService;
use crate::dev::ServiceRequest; use crate::{
use crate::http::StatusCode; dev::ServiceRequest,
use crate::middleware::{self, Condition, Logger}; http::StatusCode,
use crate::test::{call_service, init_service, TestRequest}; middleware::{self, Condition, Logger},
use crate::{web, App, HttpResponse}; test::{self, call_service, init_service, TestRequest},
web, App, HttpResponse,
};
#[actix_rt::test] #[actix_rt::test]
#[cfg(all(feature = "cookies", feature = "__compress"))] #[cfg(all(feature = "cookies", feature = "__compress"))]
@@ -219,4 +221,17 @@ mod tests {
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await; let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
#[actix_rt::test]
async fn compat_noop_is_noop() {
let srv = test::ok_service();
let mw = Compat::noop()
.new_transform(srv.into_service())
.await
.unwrap();
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
}
} }

View File

@@ -1,20 +1,13 @@
//! For middleware documentation, see [`Compress`]. //! For middleware documentation, see [`Compress`].
use std::{ use std::{
cmp,
convert::TryFrom as _,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_http::{ use actix_http::encoding::Encoder;
body::{EitherBody, MessageBody},
encoding::Encoder,
header::{ContentEncoding, ACCEPT_ENCODING},
StatusCode,
};
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use actix_utils::future::{ok, Either, Ready}; use actix_utils::future::{ok, Either, Ready};
use futures_core::ready; use futures_core::ready;
@@ -22,39 +15,65 @@ use once_cell::sync::Lazy;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
dev::BodyEncoding, body::{EitherBody, MessageBody},
http::{
header::{self, AcceptEncoding, Encoding, HeaderValue},
StatusCode,
},
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, HttpResponse, Error, HttpMessage, HttpResponse,
}; };
/// Middleware for compressing response payloads. /// Middleware for compressing response payloads.
/// ///
/// Use `BodyEncoding` trait for overriding response compression. To disable compression set /// # Encoding Negotiation
/// encoding to `ContentEncoding::Identity`. /// `Compress` will read the `Accept-Encoding` header to negotiate which compression codec to use.
/// Payloads are not compressed if the header is not sent. The `compress-*` [feature flags] are also
/// considered in this selection process.
///
/// # Pre-compressed Payload
/// If you are serving some data is already using a compressed representation (e.g., a gzip
/// compressed HTML file from disk) you can signal this to `Compress` by setting an appropriate
/// `Content-Encoding` header. In addition to preventing double compressing the payload, this header
/// is required by the spec when using compressed representations and will inform the client that
/// the content should be uncompressed.
///
/// However, it is not advised to unconditionally serve encoded representations of content because
/// the client may not support it. The [`AcceptEncoding`] typed header has some utilities to help
/// perform manual encoding negotiation, if required. When negotiating content encoding, it is also
/// required by the spec to send a `Vary: Accept-Encoding` header.
///
/// A (naïve) example serving an pre-compressed Gzip file is included below.
/// ///
/// # Examples /// # Examples
/// To enable automatic payload compression just include `Compress` as a top-level middleware:
/// ``` /// ```
/// use actix_web::{web, middleware, App, HttpResponse}; /// use actix_web::{middleware, web, App, HttpResponse};
/// ///
/// let app = App::new() /// let app = App::new()
/// .wrap(middleware::Compress::default()) /// .wrap(middleware::Compress::default())
/// .default_service(web::to(|| HttpResponse::NotFound())); /// .default_service(web::to(|| HttpResponse::Ok().body("hello world")));
/// ``` /// ```
#[derive(Debug, Clone)] ///
pub struct Compress(ContentEncoding); /// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware:
/// ```no_run
impl Compress { /// use actix_web::{middleware, http::header, web, App, HttpResponse, Responder};
/// Create new `Compress` middleware with the specified encoding. ///
pub fn new(encoding: ContentEncoding) -> Self { /// async fn index_handler() -> actix_web::Result<impl Responder> {
Compress(encoding) /// Ok(actix_files::NamedFile::open_async("./assets/index.html.gz").await?
} /// .customize()
} /// .insert_header(header::ContentEncoding::Gzip))
/// }
impl Default for Compress { ///
fn default() -> Self { /// let app = App::new()
Compress::new(ContentEncoding::Auto) /// .wrap(middleware::Compress::default())
} /// .default_service(web::to(index_handler));
} /// ```
///
/// [feature flags]: ../index.html#crate-features
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Compress;
impl<S, B> Transform<S, ServiceRequest> for Compress impl<S, B> Transform<S, ServiceRequest> for Compress
where where
@@ -68,44 +87,14 @@ where
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ok(CompressMiddleware { ok(CompressMiddleware { service })
service,
encoding: self.0,
})
} }
} }
pub struct CompressMiddleware<S> { pub struct CompressMiddleware<S> {
service: S, service: S,
encoding: ContentEncoding,
} }
static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
#[allow(unused_mut)] // only unused when no compress features enabled
let mut encoding: Vec<&str> = vec![];
#[cfg(feature = "compress-brotli")]
{
encoding.push("br");
}
#[cfg(feature = "compress-gzip")]
{
encoding.push("gzip");
encoding.push("deflate");
}
#[cfg(feature = "compress-zstd")]
encoding.push("zstd");
assert!(
!encoding.is_empty(),
"encoding can not be empty unless __compress feature has been explicitly enabled by itself"
);
encoding.join(", ")
});
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S> impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
where where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
@@ -121,39 +110,43 @@ where
#[allow(clippy::borrow_interior_mutable_const)] #[allow(clippy::borrow_interior_mutable_const)]
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
// negotiate content-encoding // negotiate content-encoding
let encoding_result = req let accept_encoding = req.get_header::<AcceptEncoding>();
.headers()
.get(&ACCEPT_ENCODING)
.and_then(|val| val.to_str().ok())
.map(|enc| AcceptEncoding::try_parse(enc, self.encoding));
match encoding_result { let accept_encoding = match accept_encoding {
// Missing header => fallback to identity // missing header; fallback to identity
None => Either::left(CompressResponse { None => {
encoding: ContentEncoding::Identity, return Either::left(CompressResponse {
fut: self.service.call(req), encoding: Encoding::identity(),
_phantom: PhantomData, fut: self.service.call(req),
}), _phantom: PhantomData,
})
}
// Valid encoding // valid accept-encoding header
Some(Ok(encoding)) => Either::left(CompressResponse { Some(accept_encoding) => accept_encoding,
encoding, };
fut: self.service.call(req),
_phantom: PhantomData,
}),
// There is an HTTP header but we cannot match what client as asked for match accept_encoding.negotiate(SUPPORTED_ENCODINGS.iter()) {
Some(Err(_)) => { None => {
let res = HttpResponse::with_body( let mut res = HttpResponse::with_body(
StatusCode::NOT_ACCEPTABLE, StatusCode::NOT_ACCEPTABLE,
SUPPORTED_ALGORITHM_NAMES.clone(), SUPPORTED_ENCODINGS_STRING.as_str(),
); );
res.headers_mut()
.insert(header::VARY, HeaderValue::from_static("Accept-Encoding"));
Either::right(ok(req Either::right(ok(req
.into_response(res) .into_response(res)
.map_into_boxed_body() .map_into_boxed_body()
.map_into_right_body())) .map_into_right_body()))
} }
Some(encoding) => Either::left(CompressResponse {
fut: self.service.call(req),
encoding,
_phantom: PhantomData,
}),
} }
} }
} }
@@ -165,7 +158,7 @@ pin_project! {
{ {
#[pin] #[pin]
fut: S::Future, fut: S::Future,
encoding: ContentEncoding, encoding: Encoding,
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
} }
} }
@@ -182,10 +175,11 @@ where
match ready!(this.fut.poll(cx)) { match ready!(this.fut.poll(cx)) {
Ok(resp) => { Ok(resp) => {
let enc = if let Some(enc) = resp.response().get_encoding() { let enc = match this.encoding {
enc Encoding::Known(enc) => *enc,
} else { Encoding::Unknown(enc) => {
*this.encoding unimplemented!("encoding {} should not be here", enc);
}
}; };
Poll::Ready(Ok(resp.map_body(move |head, body| { Poll::Ready(Ok(resp.map_body(move |head, body| {
@@ -198,178 +192,117 @@ where
} }
} }
struct AcceptEncoding { static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
encoding: ContentEncoding, #[allow(unused_mut)] // only unused when no compress features enabled
// TODO: use Quality or QualityItem<ContentEncoding> let mut encoding: Vec<&str> = vec![];
quality: f64,
}
impl Eq for AcceptEncoding {} #[cfg(feature = "compress-brotli")]
{
impl Ord for AcceptEncoding { encoding.push("br");
#[allow(clippy::comparison_chain)]
fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering {
if self.quality > other.quality {
cmp::Ordering::Less
} else if self.quality < other.quality {
cmp::Ordering::Greater
} else {
cmp::Ordering::Equal
}
}
}
impl PartialOrd for AcceptEncoding {
fn partial_cmp(&self, other: &AcceptEncoding) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for AcceptEncoding {
fn eq(&self, other: &AcceptEncoding) -> bool {
self.encoding == other.encoding && self.quality == other.quality
}
}
/// Parse q-factor from quality strings.
///
/// If parse fail, then fallback to default value which is 1.
/// More details available here: <https://developer.mozilla.org/en-US/docs/Glossary/Quality_values>
fn parse_quality(parts: &[&str]) -> f64 {
for part in parts {
if part.trim().starts_with("q=") {
return part[2..].parse().unwrap_or(1.0);
}
} }
1.0 #[cfg(feature = "compress-gzip")]
} {
encoding.push("gzip");
#[derive(Debug, PartialEq, Eq)] encoding.push("deflate");
enum AcceptEncodingError {
/// This error occurs when client only support compressed response and server do not have any
/// algorithm that match client accepted algorithms.
CompressionAlgorithmMismatch,
}
impl AcceptEncoding {
fn new(tag: &str) -> Option<AcceptEncoding> {
let parts: Vec<&str> = tag.split(';').collect();
let encoding = match parts.len() {
0 => return None,
_ => match ContentEncoding::try_from(parts[0]) {
Err(_) => return None,
Ok(x) => x,
},
};
let quality = parse_quality(&parts[1..]);
if quality <= 0.0 || quality > 1.0 {
return None;
}
Some(AcceptEncoding { encoding, quality })
} }
/// Parse a raw Accept-Encoding header value into an ordered list then return the best match #[cfg(feature = "compress-zstd")]
/// based on middleware configuration. {
pub fn try_parse( encoding.push("zstd");
raw: &str,
encoding: ContentEncoding,
) -> Result<ContentEncoding, AcceptEncodingError> {
let mut encodings = raw
.replace(' ', "")
.split(',')
.filter_map(AcceptEncoding::new)
.collect::<Vec<_>>();
encodings.sort();
for enc in encodings {
if encoding == ContentEncoding::Auto || encoding == enc.encoding {
return Ok(enc.encoding);
}
}
// Special case if user cannot accept uncompressed data.
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
// TODO: account for whitespace
if raw.contains("*;q=0") || raw.contains("identity;q=0") {
return Err(AcceptEncodingError::CompressionAlgorithmMismatch);
}
Ok(ContentEncoding::Identity)
} }
}
assert!(
!encoding.is_empty(),
"encoding can not be empty unless __compress feature has been explicitly enabled by itself"
);
encoding.join(", ")
});
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| {
let mut encodings = vec![Encoding::identity()];
#[cfg(feature = "compress-brotli")]
{
encodings.push(Encoding::brotli());
}
#[cfg(feature = "compress-gzip")]
{
encodings.push(Encoding::gzip());
encodings.push(Encoding::deflate());
}
#[cfg(feature = "compress-zstd")]
{
encodings.push(Encoding::zstd());
}
assert!(
!encodings.is_empty(),
"encodings can not be empty unless __compress feature has been explicitly enabled by itself"
);
encodings
});
// move cfg(feature) to prevents_double_compressing if more tests are added
#[cfg(feature = "compress-gzip")]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{middleware::DefaultHeaders, test, web, App};
macro_rules! assert_parse_eq { pub fn gzip_decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
($raw:expr, $result:expr) => { use std::io::Read as _;
assert_eq!( let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref());
AcceptEncoding::try_parse($raw, ContentEncoding::Auto), let mut buf = Vec::new();
Ok($result) decoder.read_to_end(&mut buf).unwrap();
); buf
};
} }
macro_rules! assert_parse_fail { #[actix_rt::test]
($raw:expr) => { async fn prevents_double_compressing() {
assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err()); const D: &str = "hello world ";
}; const DATA: &str = const_str::repeat!(D, 100);
}
#[test] let app = test::init_service({
fn test_parse_encoding() { App::new()
// Test simple case .wrap(Compress::default())
assert_parse_eq!("br", ContentEncoding::Br); .route(
assert_parse_eq!("gzip", ContentEncoding::Gzip); "/single",
assert_parse_eq!("deflate", ContentEncoding::Deflate); web::get().to(move || HttpResponse::Ok().body(DATA)),
assert_parse_eq!("zstd", ContentEncoding::Zstd); )
.service(
web::resource("/double")
.wrap(Compress::default())
.wrap(DefaultHeaders::new().add(("x-double", "true")))
.route(web::get().to(move || HttpResponse::Ok().body(DATA))),
)
})
.await;
// Test space, trim, missing values let req = test::TestRequest::default()
assert_parse_eq!("br,,,,", ContentEncoding::Br); .uri("/single")
assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip); .insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get("x-double"), None);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = test::read_body(res).await;
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
// Test float number parsing let req = test::TestRequest::default()
assert_parse_eq!("br;q=1 ,", ContentEncoding::Br); .uri("/double")
assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br); .insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
// Test wildcard let res = test::call_service(&app, req).await;
assert_parse_eq!("*", ContentEncoding::Identity); assert_eq!(res.status(), StatusCode::OK);
assert_parse_eq!("*;q=1.0", ContentEncoding::Identity); assert_eq!(res.headers().get("x-double").unwrap(), "true");
} assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = test::read_body(res).await;
#[test] assert_eq!(gzip_decode(bytes), DATA.as_bytes());
fn test_parse_encoding_qfactor_ordering() {
assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip);
assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd);
assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br);
assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip);
}
#[test]
fn test_parse_encoding_qfactor_invalid() {
// Out of range
assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity);
assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity);
// Disabled
assert_parse_eq!("gzip;q=0", ContentEncoding::Identity);
}
#[test]
fn test_parse_compression_required() {
// Check we fallback to identity if there is an unsupported compression algorithm
assert_parse_eq!("compress", ContentEncoding::Identity);
// User do not want any compression
assert_parse_fail!("compress, identity;q=0");
assert_parse_fail!("compress, identity;q=0.0");
assert_parse_fail!("compress, *;q=0");
assert_parse_fail!("compress, *;q=0.0");
} }
} }

View File

@@ -1,4 +1,4 @@
//! Commonly used middleware. //! A collection of common middleware.
mod compat; mod compat;
mod condition; mod condition;

View File

@@ -122,7 +122,7 @@ impl HttpRequest {
/// Returns a reference to the URL parameters container. /// Returns a reference to the URL parameters container.
/// ///
/// A url parameter is specified in the form `{identifier}`, where the identifier can be used /// A URL parameter is specified in the form `{identifier}`, where the identifier can be used
/// later in a request handler to access the matched value for that parameter. /// later in a request handler to access the matched value for that parameter.
/// ///
/// # Percent Encoding and URL Parameters /// # Percent Encoding and URL Parameters
@@ -266,14 +266,34 @@ impl HttpRequest {
self.app_state().config() self.app_state().config()
} }
/// Get an application data object stored with `App::data` or `App::app_data` /// Retrieves a piece of application state.
/// methods during application configuration.
/// ///
/// If `App::data` was used to store object, use `Data<T>`: /// Extracts any object stored with [`App::app_data()`](crate::App::app_data) (or the
/// counterpart methods on [`Scope`](crate::Scope::app_data) and
/// [`Resource`](crate::Resource::app_data)) during application configuration.
/// ///
/// ```ignore /// Since the Actix Web router layers application data, the returned object will reference the
/// let opt_t = req.app_data::<Data<T>>(); /// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope`
/// also stores a `u32`, and the delegated request handler falls within that `Scope`, then
/// calling `.app_data::<u32>()` on an `HttpRequest` within that handler will return the
/// `Scope`'s instance. However, using the same router set up and a request that does not get
/// captured by the `Scope`, `.app_data::<u32>()` would return the `App`'s instance.
///
/// If the state was stored using the [`Data`] wrapper, then it must also be retrieved using
/// this same type.
///
/// See also the [`Data`] extractor.
///
/// # Examples
/// ```no_run
/// # use actix_web::{test::TestRequest, web::Data};
/// # let req = TestRequest::default().to_http_request();
/// # type T = u32;
/// let opt_t: Option<&Data<T>> = req.app_data::<Data<T>>();
/// ``` /// ```
///
/// [`Data`]: crate::web::Data
#[doc(alias = "state")]
pub fn app_data<T: 'static>(&self) -> Option<&T> { pub fn app_data<T: 'static>(&self) -> Option<&T> {
for container in self.inner.app_data.iter().rev() { for container in self.inner.app_data.iter().rev() {
if let Some(data) = container.get::<T>() { if let Some(data) = container.get::<T>() {

View File

@@ -1,6 +1,6 @@
use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; use std::{cell::RefCell, fmt, future::Future, rc::Rc};
use actix_http::{body::BoxBody, Extensions}; use actix_http::Extensions;
use actix_router::{IntoPatterns, Patterns}; use actix_router::{IntoPatterns, Patterns};
use actix_service::{ use actix_service::{
apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory,
@@ -42,7 +42,7 @@ use crate::{
/// ///
/// If no matching route could be found, *405* response code get returned. Default behavior could be /// If no matching route could be found, *405* response code get returned. Default behavior could be
/// overridden with `default_resource()` method. /// overridden with `default_resource()` method.
pub struct Resource<T = ResourceEndpoint, B = BoxBody> { pub struct Resource<T = ResourceEndpoint> {
endpoint: T, endpoint: T,
rdef: Patterns, rdef: Patterns,
name: Option<String>, name: Option<String>,
@@ -51,7 +51,6 @@ pub struct Resource<T = ResourceEndpoint, B = BoxBody> {
guards: Vec<Box<dyn Guard>>, guards: Vec<Box<dyn Guard>>,
default: BoxedHttpServiceFactory, default: BoxedHttpServiceFactory,
factory_ref: Rc<RefCell<Option<ResourceFactory>>>, factory_ref: Rc<RefCell<Option<ResourceFactory>>>,
_phantom: PhantomData<B>,
} }
impl Resource { impl Resource {
@@ -69,21 +68,13 @@ impl Resource {
default: boxed::factory(fn_service(|req: ServiceRequest| async { default: boxed::factory(fn_service(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::MethodNotAllowed())) Ok(req.into_response(HttpResponse::MethodNotAllowed()))
})), })),
_phantom: PhantomData,
} }
} }
} }
impl<T, B> Resource<T, B> impl<T> Resource<T>
where where
T: ServiceFactory< T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B: MessageBody,
{ {
/// Set resource name. /// Set resource name.
/// ///
@@ -195,6 +186,7 @@ where
/// .route(web::get().to(handler)) /// .route(web::get().to(handler))
/// ); /// );
/// ``` /// ```
#[doc(alias = "manage")]
pub fn app_data<U: 'static>(mut self, data: U) -> Self { pub fn app_data<U: 'static>(mut self, data: U) -> Self {
self.app_data self.app_data
.get_or_insert_with(Extensions::new) .get_or_insert_with(Extensions::new)
@@ -240,35 +232,35 @@ where
self self
} }
/// Register a resource middleware. /// Registers a resource middleware.
/// ///
/// This is similar to `App's` middlewares, but middleware get invoked on resource level. /// `mw` is a middleware component (type), that can modify the request and response across all
/// Resource level middlewares are not allowed to change response /// routes managed by this `Resource`.
/// type (i.e modify response's body).
/// ///
/// **Note**: middlewares get called in opposite order of middlewares registration. /// See [`App::wrap`](crate::App::wrap) for more details.
pub fn wrap<M, B1>( #[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self, self,
mw: M, mw: M,
) -> Resource< ) -> Resource<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
B1,
> >
where where
M: Transform< M: Transform<
T::Service, T::Service,
ServiceRequest, ServiceRequest,
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, > + 'static,
B1: MessageBody, B: MessageBody,
{ {
Resource { Resource {
endpoint: apply(mw, self.endpoint), endpoint: apply(mw, self.endpoint),
@@ -279,61 +271,34 @@ where
default: self.default, default: self.default,
app_data: self.app_data, app_data: self.app_data,
factory_ref: self.factory_ref, factory_ref: self.factory_ref,
_phantom: PhantomData,
} }
} }
/// Register a resource middleware function. /// Registers a resource function middleware.
/// ///
/// This function accepts instance of `ServiceRequest` type and /// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// mutable reference to the next middleware in chain. /// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `Resource`.
/// ///
/// This is similar to `App's` middlewares, but middleware get invoked on resource level. /// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details.
/// Resource level middlewares are not allowed to change response #[doc(alias = "middleware")]
/// type (i.e modify response's body). #[doc(alias = "use")] // nodejs terminology
/// pub fn wrap_fn<F, R, B>(
/// ```
/// use actix_service::Service;
/// use actix_web::{web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/index.html")
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut().insert(
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
/// );
/// Ok(res)
/// }
/// })
/// .route(web::get().to(index)));
/// }
/// ```
pub fn wrap_fn<F, R, B1>(
self, self,
mw: F, mw: F,
) -> Resource< ) -> Resource<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
B1,
> >
where where
F: Fn(ServiceRequest, &T::Service) -> R + Clone, F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B1>, Error>>, R: Future<Output = Result<ServiceResponse<B>, Error>>,
B1: MessageBody, B: MessageBody,
{ {
Resource { Resource {
endpoint: apply_fn_factory(self.endpoint, mw), endpoint: apply_fn_factory(self.endpoint, mw),
@@ -344,7 +309,6 @@ where
default: self.default, default: self.default,
app_data: self.app_data, app_data: self.app_data,
factory_ref: self.factory_ref, factory_ref: self.factory_ref,
_phantom: PhantomData,
} }
} }
@@ -372,7 +336,7 @@ where
} }
} }
impl<T, B> HttpServiceFactory for Resource<T, B> impl<T, B> HttpServiceFactory for Resource<T>
where where
T: ServiceFactory< T: ServiceFactory<
ServiceRequest, ServiceRequest,
@@ -516,7 +480,7 @@ mod tests {
header::{self, HeaderValue}, header::{self, HeaderValue},
Method, StatusCode, Method, StatusCode,
}, },
middleware::{Compat, DefaultHeaders}, middleware::DefaultHeaders,
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
test::{call_service, init_service, TestRequest}, test::{call_service, init_service, TestRequest},
web, App, Error, HttpMessage, HttpResponse, web, App, Error, HttpMessage, HttpResponse,
@@ -524,31 +488,35 @@ mod tests {
#[test] #[test]
fn can_be_returned_from_fn() { fn can_be_returned_from_fn() {
fn my_resource() -> Resource { fn my_resource_1() -> Resource {
web::resource("/test").route(web::get().to(|| async { "hello" })) web::resource("/test1").route(web::get().to(|| async { "hello" }))
} }
fn my_compat_resource() -> Resource< fn my_resource_2() -> Resource<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse, Response = ServiceResponse<impl MessageBody>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
> { > {
web::resource("/test-compat") web::resource("/test2")
.wrap_fn(|req, srv| { .wrap_fn(|req, srv| {
let fut = srv.call(req); let fut = srv.call(req);
async { Ok(fut.await?.map_into_right_body::<()>()) } async { Ok(fut.await?.map_into_right_body::<()>()) }
}) })
.wrap(Compat::noop())
.route(web::get().to(|| async { "hello" })) .route(web::get().to(|| async { "hello" }))
} }
fn my_resource_3() -> impl HttpServiceFactory {
web::resource("/test3").route(web::get().to(|| async { "hello" }))
}
App::new() App::new()
.service(my_resource()) .service(my_resource_1())
.service(my_compat_resource()); .service(my_resource_2())
.service(my_resource_3());
} }
#[actix_rt::test] #[actix_rt::test]

View File

@@ -23,7 +23,7 @@ use cookie::{Cookie, CookieJar};
use crate::{ use crate::{
error::{Error, JsonPayloadError}, error::{Error, JsonPayloadError},
BoxError, HttpResponse, BoxError, HttpRequest, HttpResponse, Responder,
}; };
/// An HTTP response builder. /// An HTTP response builder.
@@ -424,6 +424,15 @@ impl Future for HttpResponseBuilder {
} }
} }
impl Responder for HttpResponseBuilder {
type Body = BoxBody;
#[inline]
fn respond_to(mut self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self.finish()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_http::body; use actix_http::body;

View File

@@ -1,12 +1,9 @@
use actix_http::{ use actix_http::{
body::{EitherBody, MessageBody}, body::EitherBody, error::HttpError, header::HeaderMap, header::TryIntoHeaderPair,
error::HttpError,
header::HeaderMap,
header::TryIntoHeaderPair,
StatusCode, StatusCode,
}; };
use crate::{BoxError, HttpRequest, HttpResponse, Responder}; use crate::{HttpRequest, HttpResponse, Responder};
/// Allows overriding status code and headers for a [`Responder`]. /// Allows overriding status code and headers for a [`Responder`].
/// ///
@@ -143,7 +140,6 @@ impl<R: Responder> CustomizeResponder<R> {
impl<T> Responder for CustomizeResponder<T> impl<T> Responder for CustomizeResponder<T>
where where
T: Responder, T: Responder,
<T::Body as MessageBody>::Error: Into<BoxError>,
{ {
type Body = EitherBody<T::Body>; type Body = EitherBody<T::Body>;

View File

@@ -7,7 +7,7 @@ use actix_http::{
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; use crate::{Error, HttpRequest, HttpResponse};
use super::CustomizeResponder; use super::CustomizeResponder;
@@ -57,15 +57,6 @@ pub trait Responder {
} }
} }
impl Responder for HttpResponse {
type Body = BoxBody;
#[inline]
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self
}
}
impl Responder for actix_http::Response<BoxBody> { impl Responder for actix_http::Response<BoxBody> {
type Body = BoxBody; type Body = BoxBody;
@@ -75,15 +66,6 @@ impl Responder for actix_http::Response<BoxBody> {
} }
} }
impl Responder for HttpResponseBuilder {
type Body = BoxBody;
#[inline]
fn respond_to(mut self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self.finish()
}
}
impl Responder for actix_http::ResponseBuilder { impl Responder for actix_http::ResponseBuilder {
type Body = BoxBody; type Body = BoxBody;
@@ -96,7 +78,6 @@ impl Responder for actix_http::ResponseBuilder {
impl<T> Responder for Option<T> impl<T> Responder for Option<T>
where where
T: Responder, T: Responder,
<T::Body as MessageBody>::Error: Into<BoxError>,
{ {
type Body = EitherBody<T::Body>; type Body = EitherBody<T::Body>;
@@ -111,7 +92,6 @@ where
impl<T, E> Responder for Result<T, E> impl<T, E> Responder for Result<T, E>
where where
T: Responder, T: Responder,
<T::Body as MessageBody>::Error: Into<BoxError>,
E: Into<Error>, E: Into<Error>,
{ {
type Body = EitherBody<T::Body>; type Body = EitherBody<T::Body>;

View File

@@ -22,7 +22,7 @@ use {
cookie::Cookie, cookie::Cookie,
}; };
use crate::{error::Error, HttpResponseBuilder}; use crate::{error::Error, HttpRequest, HttpResponseBuilder, Responder};
/// An outgoing response. /// An outgoing response.
pub struct HttpResponse<B = BoxBody> { pub struct HttpResponse<B = BoxBody> {
@@ -311,6 +311,18 @@ impl Future for HttpResponse<BoxBody> {
} }
} }
impl<B> Responder for HttpResponse<B>
where
B: MessageBody + 'static,
{
type Body = B;
#[inline]
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self
}
}
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
pub struct CookieIter<'a> { pub struct CookieIter<'a> {
iter: std::slice::Iter<'a, HeaderValue>, iter: std::slice::Iter<'a, HeaderValue>,
@@ -333,9 +345,16 @@ impl<'a> Iterator for CookieIter<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use static_assertions::assert_impl_all;
use super::*; use super::*;
use crate::http::header::{HeaderValue, COOKIE}; use crate::http::header::{HeaderValue, COOKIE};
assert_impl_all!(HttpResponse: Responder);
assert_impl_all!(HttpResponse<String>: Responder);
assert_impl_all!(HttpResponse<&'static str>: Responder);
assert_impl_all!(HttpResponse<crate::body::None>: Responder);
#[test] #[test]
fn test_debug() { fn test_debug() {
let resp = HttpResponse::Ok() let resp = HttpResponse::Ok()

View File

@@ -1,9 +1,6 @@
use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc}; use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc};
use actix_http::{ use actix_http::{body::MessageBody, Extensions};
body::{BoxBody, MessageBody},
Extensions,
};
use actix_router::{ResourceDef, Router}; use actix_router::{ResourceDef, Router};
use actix_service::{ use actix_service::{
apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory,
@@ -57,7 +54,7 @@ type Guards = Vec<Box<dyn Guard>>;
/// ///
/// [pat]: crate::dev::ResourceDef#prefix-resources /// [pat]: crate::dev::ResourceDef#prefix-resources
/// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments /// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments
pub struct Scope<T = ScopeEndpoint, B = BoxBody> { pub struct Scope<T = ScopeEndpoint> {
endpoint: T, endpoint: T,
rdef: String, rdef: String,
app_data: Option<Extensions>, app_data: Option<Extensions>,
@@ -66,7 +63,6 @@ pub struct Scope<T = ScopeEndpoint, B = BoxBody> {
default: Option<Rc<BoxedHttpServiceFactory>>, default: Option<Rc<BoxedHttpServiceFactory>>,
external: Vec<ResourceDef>, external: Vec<ResourceDef>,
factory_ref: Rc<RefCell<Option<ScopeFactory>>>, factory_ref: Rc<RefCell<Option<ScopeFactory>>>,
_phantom: PhantomData<B>,
} }
impl Scope { impl Scope {
@@ -83,21 +79,13 @@ impl Scope {
default: None, default: None,
external: Vec::new(), external: Vec::new(),
factory_ref, factory_ref,
_phantom: Default::default(),
} }
} }
} }
impl<T, B> Scope<T, B> impl<T> Scope<T>
where where
T: ServiceFactory< T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B: 'static,
{ {
/// Add match guard to a scope. /// Add match guard to a scope.
/// ///
@@ -154,6 +142,7 @@ where
/// .route("/", web::get().to(handler)) /// .route("/", web::get().to(handler))
/// ); /// );
/// ``` /// ```
#[doc(alias = "manage")]
pub fn app_data<U: 'static>(mut self, data: U) -> Self { pub fn app_data<U: 'static>(mut self, data: U) -> Self {
self.app_data self.app_data
.get_or_insert_with(Extensions::new) .get_or_insert_with(Extensions::new)
@@ -295,32 +284,35 @@ where
self self
} }
/// Registers middleware, in the form of a middleware component (type), that runs during inbound /// Registers a scope-wide middleware.
/// processing in the request life-cycle (request -> response), modifying request as necessary,
/// across all requests managed by the *Scope*.
/// ///
/// Use middleware when you need to read or modify *every* request in some way. /// `mw` is a middleware component (type), that can modify the request and response across all
pub fn wrap<M, B1>( /// sub-resources managed by this `Scope`.
///
/// See [`App::wrap`](crate::App::wrap) for more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self, self,
mw: M, mw: M,
) -> Scope< ) -> Scope<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
B1,
> >
where where
M: Transform< M: Transform<
T::Service, T::Service,
ServiceRequest, ServiceRequest,
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, > + 'static,
B: MessageBody,
{ {
Scope { Scope {
endpoint: apply(mw, self.endpoint), endpoint: apply(mw, self.endpoint),
@@ -331,54 +323,34 @@ where
default: self.default, default: self.default,
external: self.external, external: self.external,
factory_ref: self.factory_ref, factory_ref: self.factory_ref,
_phantom: PhantomData,
} }
} }
/// Registers middleware, in the form of a closure, that runs during inbound processing in the /// Registers a scope-wide function middleware.
/// request life-cycle (request -> response), modifying request as necessary, across all
/// requests managed by the *Scope*.
/// ///
/// # Examples /// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// ``` /// life-cycle (request -> response), modifying request/response as necessary, across all
/// use actix_service::Service; /// requests handled by the `Scope`.
/// use actix_web::{web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
/// ///
/// async fn index() -> &'static str { /// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details.
/// "Welcome!" #[doc(alias = "middleware")]
/// } #[doc(alias = "use")] // nodejs terminology
/// pub fn wrap_fn<F, R, B>(
/// let app = App::new().service(
/// web::scope("/app")
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut().insert(
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
/// );
/// Ok(res)
/// }
/// })
/// .route("/index.html", web::get().to(index)));
/// ```
pub fn wrap_fn<F, R, B1>(
self, self,
mw: F, mw: F,
) -> Scope< ) -> Scope<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse<B1>, Response = ServiceResponse<B>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
B1,
> >
where where
F: Fn(ServiceRequest, &T::Service) -> R + Clone, F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B1>, Error>>, R: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody,
{ {
Scope { Scope {
endpoint: apply_fn_factory(self.endpoint, mw), endpoint: apply_fn_factory(self.endpoint, mw),
@@ -389,12 +361,11 @@ where
default: self.default, default: self.default,
external: self.external, external: self.external,
factory_ref: self.factory_ref, factory_ref: self.factory_ref,
_phantom: PhantomData,
} }
} }
} }
impl<T, B> HttpServiceFactory for Scope<T, B> impl<T, B> HttpServiceFactory for Scope<T>
where where
T: ServiceFactory< T: ServiceFactory<
ServiceRequest, ServiceRequest,
@@ -595,7 +566,7 @@ mod tests {
header::{self, HeaderValue}, header::{self, HeaderValue},
Method, StatusCode, Method, StatusCode,
}, },
middleware::{Compat, DefaultHeaders}, middleware::DefaultHeaders,
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, test::{assert_body_eq, call_service, init_service, read_body, TestRequest},
web, App, HttpMessage, HttpRequest, HttpResponse, web, App, HttpMessage, HttpRequest, HttpResponse,
@@ -603,16 +574,16 @@ mod tests {
#[test] #[test]
fn can_be_returned_from_fn() { fn can_be_returned_from_fn() {
fn my_scope() -> Scope { fn my_scope_1() -> Scope {
web::scope("/test") web::scope("/test")
.service(web::resource("").route(web::get().to(|| async { "hello" }))) .service(web::resource("").route(web::get().to(|| async { "hello" })))
} }
fn my_compat_scope() -> Scope< fn my_scope_2() -> Scope<
impl ServiceFactory< impl ServiceFactory<
ServiceRequest, ServiceRequest,
Config = (), Config = (),
Response = ServiceResponse, Response = ServiceResponse<impl MessageBody>,
Error = Error, Error = Error,
InitError = (), InitError = (),
>, >,
@@ -622,11 +593,17 @@ mod tests {
let fut = srv.call(req); let fut = srv.call(req);
async { Ok(fut.await?.map_into_right_body::<()>()) } async { Ok(fut.await?.map_into_right_body::<()>()) }
}) })
.wrap(Compat::noop())
.service(web::resource("").route(web::get().to(|| async { "hello" }))) .service(web::resource("").route(web::get().to(|| async { "hello" })))
} }
App::new().service(my_scope()).service(my_compat_scope()); fn my_scope_3() -> impl HttpServiceFactory {
my_scope_2()
}
App::new()
.service(my_scope_1())
.service(my_scope_2())
.service(my_scope_3());
} }
#[actix_rt::test] #[actix_rt::test]

View File

@@ -307,9 +307,11 @@ impl ServiceRequest {
} }
} }
impl Resource<Url> for ServiceRequest { impl Resource for ServiceRequest {
type Path = Url;
#[inline] #[inline]
fn resource_path(&mut self) -> &mut Path<Url> { fn resource_path(&mut self) -> &mut Path<Self::Path> {
self.match_info_mut() self.match_info_mut()
} }
} }

View File

@@ -1,4 +1,4 @@
use std::fmt; use std::error::Error as StdError;
use actix_http::Request; use actix_http::Request;
use actix_service::IntoServiceFactory; use actix_service::IntoServiceFactory;
@@ -135,7 +135,6 @@ pub async fn call_and_read_body<S, B>(app: &S, req: Request) -> Bytes
where where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>, S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody, B: MessageBody,
B::Error: fmt::Debug,
{ {
let res = call_service(app, req).await; let res = call_service(app, req).await;
read_body(res).await read_body(res).await
@@ -147,7 +146,6 @@ pub async fn read_response<S, B>(app: &S, req: Request) -> Bytes
where where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>, S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody, B: MessageBody,
B::Error: fmt::Debug,
{ {
let res = call_service(app, req).await; let res = call_service(app, req).await;
read_body(res).await read_body(res).await
@@ -186,11 +184,11 @@ where
pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
where where
B: MessageBody, B: MessageBody,
B::Error: fmt::Debug,
{ {
let body = res.into_body(); let body = res.into_body();
body::to_bytes(body) body::to_bytes(body)
.await .await
.map_err(Into::<Box<dyn StdError>>::into)
.expect("error reading test response body") .expect("error reading test response body")
} }
@@ -240,7 +238,6 @@ where
pub async fn read_body_json<T, B>(res: ServiceResponse<B>) -> T pub async fn read_body_json<T, B>(res: ServiceResponse<B>) -> T
where where
B: MessageBody, B: MessageBody,
B::Error: fmt::Debug,
T: DeserializeOwned, T: DeserializeOwned,
{ {
let body = read_body(res).await; let body = read_body(res).await;
@@ -300,7 +297,6 @@ pub async fn call_and_read_body_json<S, B, T>(app: &S, req: Request) -> T
where where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>, S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody, B: MessageBody,
B::Error: fmt::Debug,
T: DeserializeOwned, T: DeserializeOwned,
{ {
let res = call_service(app, req).await; let res = call_service(app, req).await;
@@ -313,7 +309,6 @@ pub async fn read_response_json<S, B, T>(app: &S, req: Request) -> T
where where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>, S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody, B: MessageBody,
B::Error: fmt::Debug,
T: DeserializeOwned, T: DeserializeOwned,
{ {
call_and_read_body_json(app, req).await call_and_read_body_json(app, req).await
@@ -325,7 +320,10 @@ mod tests {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::*; use super::*;
use crate::{http::header, test::TestRequest, web, App, HttpMessage, HttpResponse}; use crate::{
dev::ServiceRequest, http::header, test::TestRequest, web, App, HttpMessage,
HttpResponse,
};
#[actix_rt::test] #[actix_rt::test]
async fn test_request_methods() { async fn test_request_methods() {
@@ -471,4 +469,37 @@ mod tests {
assert_eq!(&result.id, "12345"); assert_eq!(&result.id, "12345");
assert_eq!(&result.name, "User name"); assert_eq!(&result.name, "User name");
} }
#[actix_rt::test]
#[allow(dead_code)]
async fn return_opaque_types() {
fn test_app() -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<impl MessageBody>,
Error = crate::Error,
InitError = (),
>,
> {
App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
))
}
async fn test_service(
) -> impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = crate::Error>
{
init_service(test_app()).await
}
async fn compile_test(mut req: Vec<Request>) {
let svc = test_service().await;
call_service(&svc, req.pop().unwrap()).await;
call_and_read_body(&svc, req.pop().unwrap()).await;
read_body(call_service(&svc, req.pop().unwrap()).await).await;
let _: String = call_and_read_body_json(&svc, req.pop().unwrap()).await;
let _: String = read_body_json(call_service(&svc, req.pop().unwrap()).await).await;
}
}
} }

View File

@@ -11,7 +11,7 @@ use std::{
}; };
use bytes::BytesMut; use bytes::BytesMut;
use futures_core::{ready, stream::Stream as _}; use futures_core::{ready, Stream as _};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use actix_http::Payload; use actix_http::Payload;
@@ -515,7 +515,7 @@ mod tests {
.to_http_parts(); .to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await; let s = Json::<MyObject>::from_request(&req, &mut pl).await;
let resp = HttpResponse::from_error(s.err().unwrap()); let resp = HttpResponse::from_error(s.unwrap_err());
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = body::to_bytes(resp.into_body()).await.unwrap(); let body = body::to_bytes(resp.into_body()).await.unwrap();

View File

@@ -1,19 +1,18 @@
//! Common extractors and responders. //! Common extractors and responders.
// TODO: review visibility
mod either; mod either;
pub(crate) mod form; mod form;
mod header; mod header;
pub(crate) mod json; mod json;
mod path; mod path;
pub(crate) mod payload; mod payload;
mod query; mod query;
pub(crate) mod readlines; mod readlines;
pub use self::either::{Either, EitherExtractError}; pub use self::either::Either;
pub use self::form::{Form, FormConfig}; pub use self::form::{Form, FormConfig, UrlEncoded};
pub use self::header::Header; pub use self::header::Header;
pub use self::json::{Json, JsonConfig}; pub use self::json::{Json, JsonBody, JsonConfig};
pub use self::path::{Path, PathConfig}; pub use self::path::{Path, PathConfig};
pub use self::payload::{Payload, PayloadConfig}; pub use self::payload::{Payload, PayloadConfig};
pub use self::query::{Query, QueryConfig}; pub use self::query::{Query, QueryConfig};

View File

@@ -9,6 +9,7 @@ use serde::de;
use crate::{ use crate::{
dev::Payload, dev::Payload,
error::{Error, ErrorNotFound, PathError}, error::{Error, ErrorNotFound, PathError},
web::Data,
FromRequest, HttpRequest, FromRequest, HttpRequest,
}; };
@@ -102,6 +103,7 @@ where
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let error_handler = req let error_handler = req
.app_data::<PathConfig>() .app_data::<PathConfig>()
.or_else(|| req.app_data::<Data<PathConfig>>().map(Data::get_ref))
.and_then(|c| c.err_handler.clone()); .and_then(|c| c.err_handler.clone());
ready( ready(
@@ -113,6 +115,7 @@ where
Request path: {:?}", Request path: {:?}",
req.path() req.path()
); );
if let Some(error_handler) = error_handler { if let Some(error_handler) = error_handler {
let e = PathError::Deserialize(err); let e = PathError::Deserialize(err);
(error_handler)(e, req) (error_handler)(e, req)
@@ -135,6 +138,7 @@ where
/// enum Folder { /// enum Folder {
/// #[serde(rename = "inbox")] /// #[serde(rename = "inbox")]
/// Inbox, /// Inbox,
///
/// #[serde(rename = "outbox")] /// #[serde(rename = "outbox")]
/// Outbox, /// Outbox,
/// } /// }
@@ -144,19 +148,17 @@ where
/// format!("Selected folder: {:?}!", folder) /// format!("Selected folder: {:?}!", folder)
/// } /// }
/// ///
/// fn main() { /// let app = App::new().service(
/// let app = App::new().service( /// web::resource("/messages/{folder}")
/// web::resource("/messages/{folder}") /// .app_data(PathConfig::default().error_handler(|err, req| {
/// .app_data(PathConfig::default().error_handler(|err, req| { /// error::InternalError::from_response(
/// error::InternalError::from_response( /// err,
/// err, /// HttpResponse::Conflict().into(),
/// HttpResponse::Conflict().into(), /// )
/// ) /// .into()
/// .into() /// }))
/// })) /// .route(web::post().to(index)),
/// .route(web::post().to(index)), /// );
/// );
/// }
/// ``` /// ```
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct PathConfig { pub struct PathConfig {
@@ -164,7 +166,7 @@ pub struct PathConfig {
} }
impl PathConfig { impl PathConfig {
/// Set custom error handler /// Set custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self pub fn error_handler<F>(mut self, f: F) -> Self
where where
F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static, F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static,
@@ -283,6 +285,18 @@ mod tests {
assert_eq!(res[1], "32".to_owned()); assert_eq!(res[1], "32".to_owned());
} }
#[actix_rt::test]
async fn paths_decoded() {
let resource = ResourceDef::new("/{key}/{value}");
let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%251").to_srv_request();
resource.capture_match_info(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let path_items = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
assert_eq!(path_items.key, "na+me");
assert_eq!(path_items.value, "us/er%1");
}
#[actix_rt::test] #[actix_rt::test]
async fn test_custom_err_handler() { async fn test_custom_err_handler() {
let (req, mut pl) = TestRequest::with_uri("/name/user1/") let (req, mut pl) = TestRequest::with_uri("/name/user1/")

View File

@@ -248,6 +248,7 @@ impl PayloadConfig {
} }
} }
} }
Ok(()) Ok(())
} }

View File

@@ -2,13 +2,12 @@
use std::future::Future; use std::future::Future;
use actix_http::Method;
use actix_router::IntoPatterns; use actix_router::IntoPatterns;
pub use bytes::{Buf, BufMut, Bytes, BytesMut}; pub use bytes::{Buf, BufMut, Bytes, BytesMut};
use crate::{ use crate::{
error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource,
route::Route, scope::Scope, service::WebService, Responder, Responder, Route, Scope,
}; };
pub use crate::config::ServiceConfig; pub use crate::config::ServiceConfig;

307
tests/compression.rs Normal file
View File

@@ -0,0 +1,307 @@
use actix_http::ContentEncoding;
use actix_web::{
http::{header, StatusCode},
middleware::Compress,
web, App, HttpResponse,
};
use bytes::Bytes;
mod utils;
static LOREM: &[u8] = include_bytes!("fixtures/lorem.txt");
static LOREM_GZIP: &[u8] = include_bytes!("fixtures/lorem.txt.gz");
static LOREM_BR: &[u8] = include_bytes!("fixtures/lorem.txt.br");
static LOREM_ZSTD: &[u8] = include_bytes!("fixtures/lorem.txt.zst");
static LOREM_XZ: &[u8] = include_bytes!("fixtures/lorem.txt.xz");
macro_rules! test_server {
() => {
actix_test::start(|| {
App::new()
.wrap(Compress::default())
.route("/static", web::to(|| HttpResponse::Ok().body(LOREM)))
.route(
"/static-gzip",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded
.insert_header(ContentEncoding::Gzip)
.body(LOREM_GZIP)
}),
)
.route(
"/static-br",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded
.insert_header(ContentEncoding::Brotli)
.body(LOREM_BR)
}),
)
.route(
"/static-zstd",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded
.insert_header(ContentEncoding::Zstd)
.body(LOREM_ZSTD)
}),
)
.route(
"/static-xz",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded as 7zip
.insert_header((header::CONTENT_ENCODING, "xz"))
.body(LOREM_XZ)
}),
)
.route(
"/echo",
web::to(|body: Bytes| HttpResponse::Ok().body(body)),
)
})
};
}
#[actix_rt::test]
async fn negotiate_encoding_identity() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "identity"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING), None);
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_gzip() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(utils::gzip::decode(bytes), LOREM);
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_br() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(utils::brotli::decode(bytes), LOREM);
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_zstd() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "zstd");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(utils::zstd::decode(bytes), LOREM);
srv.stop().await;
}
#[cfg(all(
feature = "compress-brotli",
feature = "compress-gzip",
feature = "compress-zstd",
))]
#[actix_rt::test]
async fn client_encoding_prefers_brotli() {
let srv = test_server!();
let req = srv.post("/static").send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn gzip_no_decompress() {
let srv = test_server!();
let req = srv
.post("/static-gzip")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_GZIP));
srv.stop().await;
}
#[actix_rt::test]
async fn manual_custom_coding() {
let srv = test_server!();
let req = srv
.post("/static-xz")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "xz"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_XZ));
srv.stop().await;
}
#[actix_rt::test]
async fn deny_identity_coding() {
let srv = test_server!();
let req = srv
.post("/static")
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "br, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn deny_identity_coding_no_decompress() {
let srv = test_server!();
let req = srv
.post("/static-br")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "br, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_BR));
srv.stop().await;
}
// TODO: fix test
// currently fails because negotiation doesn't consider unknown encoding types
#[ignore]
#[actix_rt::test]
async fn deny_identity_for_manual_coding() {
let srv = test_server!();
let req = srv
.post("/static-xz")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "xz, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_XZ));
srv.stop().await;
}

5
tests/fixtures/lorem.txt vendored Normal file
View File

@@ -0,0 +1,5 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin interdum tincidunt lacus, sed tempor lorem consectetur et. Pellentesque et egestas sem, at cursus massa. Nunc feugiat elit sit amet ipsum commodo luctus. Proin auctor dignissim pharetra. Integer iaculis quam a tellus auctor, vitae auctor nisl viverra. Nullam consequat maximus porttitor. Pellentesque tortor enim, molestie at varius non, tempor non nibh. Suspendisse tempus erat lorem, vel faucibus magna blandit vel. Sed pellentesque ligula augue, vitae fermentum eros blandit et. Cras dignissim in massa ut varius. Vestibulum commodo nunc sit amet pellentesque dignissim.
Donec imperdiet blandit lobortis. Suspendisse fringilla nunc quis venenatis tempor. Nunc tempor sed erat sed convallis. Pellentesque aliquet elit lectus, quis vulputate arcu pharetra sed. Etiam laoreet aliquet arcu cursus vehicula. Maecenas odio odio, elementum faucibus sollicitudin vitae, pellentesque ac purus. Donec venenatis faucibus lorem, et finibus lacus tincidunt vitae. Quisque laoreet metus sapien, vitae euismod mauris lobortis malesuada. Integer sit amet elementum turpis. Maecenas ex mauris, dapibus eu placerat vitae, rutrum convallis enim. Nulla vitae orci ultricies, sagittis turpis et, lacinia dui. Praesent egestas urna turpis, sit amet feugiat mauris tristique eu. Quisque id tempor libero. Donec ullamcorper dapibus lorem, vel consequat risus congue a.
Nullam dignissim ut lectus vitae tempor. Pellentesque ut odio fringilla, volutpat mi et, vulputate tellus. Fusce eget diam non odio tincidunt viverra eu vel augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam sed eleifend purus, vitae aliquam orci. Cras fringilla justo eget tempus bibendum. Phasellus venenatis, odio nec pulvinar commodo, quam neque lacinia turpis, ut rutrum tortor massa eu nulla. Vivamus tincidunt ut lectus a gravida. Donec varius mi quis enim interdum ultrices. Sed aliquam consectetur nisi vitae viverra. Praesent nec ligula egestas, porta lectus sed, consectetur augue.

Some files were not shown because too many files have changed in this diff Show More