From 3d7feb28d35e209b4190fd6107c90df348ecb5a4 Mon Sep 17 00:00:00 2001 From: Cathal Date: Fri, 31 Oct 2025 19:03:20 +0000 Subject: [PATCH 1/8] chore: fix clippy warnings, modernize CI (#212) --- .github/workflows/ci.yml | 120 ++++++++-------------------------- .github/workflows/cibench.yml | 24 ++----- Cargo.toml | 2 +- src/lib.rs | 4 +- src/simd/neon.rs | 4 +- 5 files changed, 39 insertions(+), 115 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ea7bac..ead2a6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,32 +48,22 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v5 - name: Install Rust (${{ matrix.rust }}) - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: ${{ matrix.rust }} - override: true - name: no_std - uses: actions-rs/cargo@v1 - with: - command: build - args: --no-default-features + run: cargo build --no-default-features - name: Test - uses: actions-rs/cargo@v1 - with: - command: test + run: cargo test - name: Test all benches if: matrix.benches - uses: actions-rs/cargo@v1 - with: - command: test - args: --benches + run: cargo test --benches simd: name: SIMD ${{ matrix.target_feature }} on ${{ matrix.rust }} @@ -96,72 +86,52 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v5 - name: Install Rust (${{ matrix.rust }}) - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: ${{ matrix.rust }} - override: true - name: Test - uses: actions-rs/cargo@v1 - with: - command: test env: RUSTFLAGS: -C target_feature=${{ matrix.target_feature }} CARGO_CFG_HTTPARSE_DISABLE_SIMD_COMPILETIME: ${{ matrix.disable_compiletime }} + run: cargo test check_x86: name: check x86 runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v5 - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - profile: minimal - toolchain: stable - override: true - target: i686-unknown-linux-musl + targets: i686-unknown-linux-musl - name: Test without SIMD - uses: actions-rs/cargo@v1 - with: - command: test - args: --target i686-unknown-linux-musl env: CARGO_CFG_HTTPARSE_DISABLE_SIMD_COMPILETIME: 1 + run: cargo test --target i686-unknown-linux-musl - name: Test - - uses: actions-rs/cargo@v1 - with: - command: test - args: --target i686-unknown-linux-musl + run: cargo test --target i686-unknown-linux-musl msrv_x64: name: msrv (x64) runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v5 - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: 1.47.0 - override: true + uses: dtolnay/rust-toolchain@1.47.0 # Only build, dev-dependencies don't compile on 1.47.0 - name: Build - uses: actions-rs/cargo@v1 - with: - command: build + run: cargo build # This tests that aarch64 gracefully fallbacks to SWAR if neon_intrinsics aren't available (<1.59) msrv_aarch64: @@ -169,7 +139,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v5 - name: Install cross-compiling dependencies run: | @@ -177,24 +147,21 @@ jobs: sudo apt-get install -y gcc-aarch64-linux-gnu - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@1.47.0 with: - profile: minimal - toolchain: 1.47.0 - override: true target: aarch64-unknown-linux-gnu # Only build, dev-dependencies don't compile on 1.47.0 - name: Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --target aarch64-unknown-linux-gnu + run: cargo build --target aarch64-unknown-linux-gnu clippy_check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy - name: Run clippy run: cargo clippy --all-targets --all-features @@ -204,43 +171,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v5 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@nightly with: - profile: minimal - toolchain: nightly components: miri - override: true - name: Test run: cargo miri test - # - # mirai: - # name: MIRAI static analysis - # runs-on: ubuntu-latest - # - # steps: - # - name: Checkout - # uses: actions/checkout@v1 - # - # - name: Install Rust - # uses: actions-rs/toolchain@v1 - # with: - # profile: minimal - # toolchain: nightly-2023-05-09 - # components: clippy, rustfmt, rustc-dev, rust-src, rust-std, llvm-tools-preview - # override: true - # - # - name: install mirai - # run: cargo install --locked --git https://github.com/facebookexperimental/MIRAI/ mirai - # env: - # # MIRAI_FLAGS: --diag=(default|verify|library|paranoid) - # MIRAI_FLAGS: --diag=default - # - # - name: cargo mirai - # run: cargo mirai --lib aarch64: name: Test aarch64 (neon) @@ -249,15 +188,12 @@ jobs: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - profile: minimal - toolchain: stable - override: true - target: aarch64-unknown-linux-gnu + targets: aarch64-unknown-linux-gnu - name: Install QEMU and dependencies run: | diff --git a/.github/workflows/cibench.yml b/.github/workflows/cibench.yml index 83133af..647b91a 100644 --- a/.github/workflows/cibench.yml +++ b/.github/workflows/cibench.yml @@ -12,16 +12,12 @@ jobs: name: Run benchmarks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + uses: dtolnay/rust-toolchain@stable - name: Install critcmp run: cargo install critcmp @@ -47,16 +43,12 @@ jobs: matrix: feature: [swar, sse42, avx2] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + uses: dtolnay/rust-toolchain@stable - name: Install critcmp run: cargo install critcmp @@ -88,16 +80,12 @@ jobs: matrix: feature: [swar, neon] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + uses: dtolnay/rust-toolchain@stable - name: Install critcmp run: cargo install critcmp diff --git a/Cargo.toml b/Cargo.toml index 4aee710..6cc7d42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ default = ["std"] std = [] [dev-dependencies] -criterion = "0.3.5" +criterion = { version = "0.3.5", features = ["html_reports"] } rand = "0.8.5" [lib] diff --git a/src/lib.rs b/src/lib.rs index 1a99bc9..1af4b20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(not(any(test, feature = "std")), no_std)] #![deny( missing_docs, clippy::missing_safety_doc, @@ -1795,7 +1795,7 @@ mod tests { let mut headers = [EMPTY_HEADER; NUM_OF_HEADERS]; let mut req = Request::new(&mut headers[..]); for i in 0..req_str.len() { - let status = req.parse(req_str[..i].as_bytes()); + let status = req.parse(&req_str.as_bytes()[..i]); assert_eq!( status, Ok(Status::Partial), diff --git a/src/simd/neon.rs b/src/simd/neon.rs index b059efb..2994a57 100644 --- a/src/simd/neon.rs +++ b/src/simd/neon.rs @@ -182,9 +182,9 @@ unsafe fn offsetnz(x: uint8x16_t) -> u32 { if low != 0 { clz(low) } else if high != 0 { - return 8 + clz(high); + 8 + clz(high) } else { - return 16; + 16 } } From a51172dd480e5b24d03069a90e6ae6fc610aafb0 Mon Sep 17 00:00:00 2001 From: Cathal Date: Mon, 3 Nov 2025 16:07:46 +0000 Subject: [PATCH 2/8] remove build script, bump MSRV to use standard cfg attributes for SIMD (#213) --- .github/workflows/ci.yml | 44 ++--------- .github/workflows/cibench.yml | 35 +++++---- Cargo.toml | 9 +-- build.rs | 133 ---------------------------------- clippy.toml | 1 - src/macros.rs | 6 +- src/simd/mod.rs | 90 ++++++++++++----------- 7 files changed, 83 insertions(+), 235 deletions(-) delete mode 100644 build.rs delete mode 100644 clippy.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ead2a6f..599ea56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,7 @@ jobs: - simd - check_x86 - aarch64 - - msrv_x64 - - msrv_aarch64 + - msrv - miri - clippy_check steps: @@ -44,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} env: - CARGO_CFG_HTTPARSE_DISABLE_SIMD: 1 + RUSTFLAGS: --cfg httparse_disable_simd steps: - name: Checkout @@ -76,14 +75,10 @@ jobs: - nightly target_feature: + - "" # runtime detection - "+sse4.2" - "+avx2" - "+sse4.2,+avx2" - - disable_compiletime: - - 0 - - 1 - steps: - name: Checkout uses: actions/checkout@v5 @@ -96,7 +91,6 @@ jobs: - name: Test env: RUSTFLAGS: -C target_feature=${{ matrix.target_feature }} - CARGO_CFG_HTTPARSE_DISABLE_SIMD_COMPILETIME: ${{ matrix.disable_compiletime }} run: cargo test check_x86: @@ -113,48 +107,26 @@ jobs: - name: Test without SIMD env: - CARGO_CFG_HTTPARSE_DISABLE_SIMD_COMPILETIME: 1 + RUSTFLAGS: --cfg httparse_disable_simd run: cargo test --target i686-unknown-linux-musl - name: Test run: cargo test --target i686-unknown-linux-musl - msrv_x64: - name: msrv (x64) + msrv: + name: msrv runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5 - name: Install Rust - uses: dtolnay/rust-toolchain@1.47.0 + uses: dtolnay/rust-toolchain@1.59.0 - # Only build, dev-dependencies don't compile on 1.47.0 + # Only build, dev-dependencies don't compile on 1.59.0 - name: Build run: cargo build - # This tests that aarch64 gracefully fallbacks to SWAR if neon_intrinsics aren't available (<1.59) - msrv_aarch64: - name: msrv (aarch64) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Install cross-compiling dependencies - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu - - - name: Setup Rust - uses: dtolnay/rust-toolchain@1.47.0 - with: - target: aarch64-unknown-linux-gnu - - # Only build, dev-dependencies don't compile on 1.47.0 - - name: Build - run: cargo build --target aarch64-unknown-linux-gnu - clippy_check: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/cibench.yml b/.github/workflows/cibench.yml index 647b91a..323e5d4 100644 --- a/.github/workflows/cibench.yml +++ b/.github/workflows/cibench.yml @@ -5,12 +5,13 @@ on: env: CARGO_TERM_COLOR: always - RUSTFLAGS: "-C target-cpu=native" jobs: benchmark: name: Run benchmarks runs-on: ubuntu-latest + env: + RUSTFLAGS: "-C target-cpu=native" steps: - uses: actions/checkout@v5 with: @@ -41,7 +42,17 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - feature: [swar, sse42, avx2] + include: + - feature: runtime + rustflags: "" + - feature: swar + rustflags: "--cfg httparse_disable_simd" + - feature: sse4.2 + rustflags: "-C target-feature=+sse4.2" + - feature: avx2 + rustflags: "-C target-feature=+avx2" + env: + RUSTFLAGS: ${{ matrix.rustflags }} steps: - uses: actions/checkout@v5 with: @@ -57,17 +68,11 @@ jobs: run: | git checkout master cargo bench --bench parse -- --save-baseline master-${{ matrix.feature }} - env: - CARGO_CFG_HTTPARSE_DISABLE_SIMD: ${{ matrix.feature == 'swar' && '1' || '0' }} - RUSTFLAGS: ${{ matrix.feature != 'swar' && format('-C target-feature=+{0}', matrix.feature) || '' }} - name: Run benchmarks (PR) run: | git checkout ${{ github.event.pull_request.head.sha }} cargo bench --bench parse -- --save-baseline pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}-${{ matrix.feature }} - env: - CARGO_CFG_HTTPARSE_DISABLE_SIMD: ${{ matrix.feature == 'swar' && '1' || '0' }} - RUSTFLAGS: ${{ matrix.feature != 'swar' && format('-C target-feature=+{0}', matrix.feature) || '' }} - name: Compare benchmarks run: | @@ -78,7 +83,13 @@ jobs: runs-on: macos-latest strategy: matrix: - feature: [swar, neon] + include: + - feature: swar + rustflags: "--cfg httparse_disable_simd" + - feature: neon + rustflags: "-C target-feature=+neon" + env: + RUSTFLAGS: ${{ matrix.rustflags }} steps: - uses: actions/checkout@v5 with: @@ -94,17 +105,11 @@ jobs: run: | git checkout master cargo bench --bench parse -- --save-baseline master-aarch64-${{ matrix.feature }} - env: - CARGO_CFG_HTTPARSE_DISABLE_SIMD: ${{ matrix.feature == 'swar' && '1' || '0' }} - RUSTFLAGS: ${{ matrix.feature == 'neon' && '-C target-feature=+neon' || '' }} - name: Run benchmarks (PR) run: | git checkout ${{ github.event.pull_request.head.sha }} cargo bench --bench parse -- --save-baseline pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}-aarch64-${{ matrix.feature }} - env: - CARGO_CFG_HTTPARSE_DISABLE_SIMD: ${{ matrix.feature == 'swar' && '1' || '0' }} - RUSTFLAGS: ${{ matrix.feature == 'neon' && '-C target-feature=+neon' || '' }} - name: Compare benchmarks run: | diff --git a/Cargo.toml b/Cargo.toml index 6cc7d42..7d60497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ documentation = "https://docs.rs/httparse" readme = "README.md" keywords = ["http", "parser", "no_std"] categories = ["network-programming", "no-std", "parser-implementations", "web-programming"] -edition = "2018" -build = "build.rs" +edition = "2021" +rust-version = "1.59" [features] default = ["std"] @@ -34,8 +34,5 @@ opt-level = 3 [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(httparse_simd)', - 'cfg(httparse_simd_target_feature_avx2)', - 'cfg(httparse_simd_target_feature_sse42)', - 'cfg(httparse_simd_neon_intrinsics)', + 'cfg(httparse_disable_simd)', ] } diff --git a/build.rs b/build.rs deleted file mode 100644 index 6c45358..0000000 --- a/build.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::env; -use std::ffi::OsString; -use std::process::Command; - -fn main() { - // We check rustc version to enable features beyond MSRV, such as: - // - 1.59 => neon_intrinsics - let rustc = env::var_os("RUSTC").unwrap_or(OsString::from("rustc")); - let output = Command::new(rustc) - .arg("--version") - .output() - .expect("failed to check 'rustc --version'") - .stdout; - - let raw_version = String::from_utf8(output) - .expect("rustc version output should be utf-8"); - - let version = match Version::parse(&raw_version) { - Ok(version) => version, - Err(err) => { - println!("cargo:warning=failed to parse `rustc --version`: {}", err); - return; - } - }; - - enable_new_features(version); -} - -fn enable_new_features(version: Version) { - enable_simd(version); -} - -fn enable_simd(version: Version) { - if env::var_os("CARGO_FEATURE_STD").is_none() { - println!("cargo:warning=building for no_std disables httparse SIMD"); - return; - } - if env::var_os("CARGO_CFG_MIRI").is_some() { - println!("cargo:warning=building for Miri disables httparse SIMD"); - return; - } - - let env_disable = "CARGO_CFG_HTTPARSE_DISABLE_SIMD"; - if var_is(env_disable, "1") { - println!("cargo:warning=detected {} environment variable, disabling SIMD", env_disable); - return; - } - - // 1.59.0 is the first version to support neon_intrinsics - if version >= Version(1, 59, 0) { - println!("cargo:rustc-cfg=httparse_simd_neon_intrinsics"); - } - - println!("cargo:rustc-cfg=httparse_simd"); - - // cfg(target_feature) isn't stable yet, but CARGO_CFG_TARGET_FEATURE has - // a list... We aren't doing anything unsafe, since the is_x86_feature_detected - // macro still checks in the actual lib, BUT! - // - // By peeking at the list here, we can change up slightly how we do feature - // detection in the lib. If our features aren't in the feature list, we - // stick with a cached runtime detection strategy. - // - // But if the features *are* in the list, we benefit from removing our cache, - // since the compiler will eliminate several branches with its internal - // cfg(target_feature) usage. - - - let env_runtime_only = "CARGO_CFG_HTTPARSE_DISABLE_SIMD_COMPILETIME"; - if var_is(env_runtime_only, "1") { - println!("cargo:warning=detected {} environment variable, using runtime SIMD detection only", env_runtime_only); - return; - } - let feature_list = match env::var_os("CARGO_CFG_TARGET_FEATURE") { - Some(var) => match var.into_string() { - Ok(s) => s, - Err(_) => { - println!("cargo:warning=CARGO_CFG_TARGET_FEATURE was not valid utf-8"); - return; - }, - }, - None => { - println!("cargo:warning=CARGO_CFG_TARGET_FEATURE was not set"); - return - }, - }; - - let features = feature_list.split(',').map(|s| s.trim()); - if features.clone().any(|f| f == "sse4.2") { - println!("cargo:rustc-cfg=httparse_simd_target_feature_sse42"); - } - if features.clone().any(|f| f == "avx2") { - println!("cargo:rustc-cfg=httparse_simd_target_feature_avx2"); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -struct Version (u32, u32, u32); - -impl Version { - fn parse(s: &str) -> Result { - let s = match s.strip_prefix("rustc ") { - Some(s) => s, - None => return Err(format!("unrecognized version string: {}", s)), - }; - - let mut iter = s - .split('.') - .take(3) - .map(|s| match s.find(|c: char| !c.is_ascii_digit()) { - Some(end) => &s[..end], - None => s, - }) - .map(|s| s.parse::().map_err(|e| e.to_string())); - - if iter.clone().count() != 3 { - return Err(format!("not enough version parts: {:?}", s)); - } - - let major = iter.next().unwrap()?; - let minor = iter.next().unwrap()?; - let patch = iter.next().unwrap()?; - - Ok(Version(major, minor, patch)) - } -} - -fn var_is(key: &str, val: &str) -> bool { - match env::var(key) { - Ok(v) => v == val, - Err(_) => false, - } -} diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index 7846a3e..0000000 --- a/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -msrv = "1.47" diff --git a/src/macros.rs b/src/macros.rs index 751f60b..50ab156 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -10,10 +10,10 @@ macro_rules! next { } macro_rules! expect { - ($bytes:ident.next() == $pat:pat => $ret:expr) => { + ($bytes:ident.next() == $pat:pat_param => $ret:expr) => { expect!(next!($bytes) => $pat |? $ret) }; - ($e:expr => $pat:pat |? $ret:expr) => { + ($e:expr => $pat:pat_param |? $ret:expr) => { match $e { v@$pat => v, _ => return $ret @@ -31,7 +31,7 @@ macro_rules! complete { } macro_rules! byte_map { - ($($p:pat)|+) => {{ + ($($p:pat_param)|+) => {{ const fn make_map() -> [bool; 256] { let mut ret = [false; 256]; let mut i = 0; diff --git a/src/simd/mod.rs b/src/simd/mod.rs index 0e4493a..39e61ce 100644 --- a/src/simd/mod.rs +++ b/src/simd/mod.rs @@ -1,21 +1,24 @@ mod swar; -#[cfg(not(all( - httparse_simd, - any( +#[cfg(any( + httparse_disable_simd, + miri, + not(feature = "std"), + not(any( target_arch = "x86", target_arch = "x86_64", all( target_arch = "aarch64", - httparse_simd_neon_intrinsics, + target_feature = "neon", ) - ), -)))] + )) +))] pub use self::swar::*; #[cfg(all( - httparse_simd, - not(httparse_simd_target_feature_avx2), + not(any(httparse_disable_simd, miri)), + feature = "std", + not(target_feature = "avx2"), any( target_arch = "x86", target_arch = "x86_64", @@ -24,10 +27,11 @@ pub use self::swar::*; mod sse42; #[cfg(all( - httparse_simd, + not(any(httparse_disable_simd, miri)), + feature = "std", any( - httparse_simd_target_feature_avx2, - not(httparse_simd_target_feature_sse42), + target_feature = "avx2", + not(target_feature = "sse4.2"), ), any( target_arch = "x86", @@ -37,10 +41,11 @@ mod sse42; mod avx2; #[cfg(all( - httparse_simd, + not(any(httparse_disable_simd, miri)), + feature = "std", not(any( - httparse_simd_target_feature_sse42, - httparse_simd_target_feature_avx2, + target_feature = "sse4.2", + target_feature = "avx2", )), any( target_arch = "x86", @@ -50,10 +55,11 @@ mod avx2; mod runtime; #[cfg(all( - httparse_simd, + not(any(httparse_disable_simd, miri)), + feature = "std", not(any( - httparse_simd_target_feature_sse42, - httparse_simd_target_feature_avx2, + target_feature = "sse4.2", + target_feature = "avx2", )), any( target_arch = "x86", @@ -63,9 +69,10 @@ mod runtime; pub use self::runtime::*; #[cfg(all( - httparse_simd, - httparse_simd_target_feature_sse42, - not(httparse_simd_target_feature_avx2), + not(any(httparse_disable_simd, miri)), + feature = "std", + target_feature = "sse4.2", + not(target_feature = "avx2"), any( target_arch = "x86", target_arch = "x86_64", @@ -79,21 +86,20 @@ mod sse42_compile_time { #[inline(always)] pub fn match_uri_vectored(b: &mut crate::iter::Bytes<'_>) { - // SAFETY: calls are guarded by a compile time feature check - unsafe { crate::simd::sse42::match_uri_vectored(b) } + unsafe { super::sse42::match_uri_vectored(b) } } - + #[inline(always)] pub fn match_header_value_vectored(b: &mut crate::iter::Bytes<'_>) { - // SAFETY: calls are guarded by a compile time feature check - unsafe { crate::simd::sse42::match_header_value_vectored(b) } + unsafe { super::sse42::match_header_value_vectored(b) } } } #[cfg(all( - httparse_simd, - httparse_simd_target_feature_sse42, - not(httparse_simd_target_feature_avx2), + not(any(httparse_disable_simd, miri)), + feature = "std", + target_feature = "sse4.2", + not(target_feature = "avx2"), any( target_arch = "x86", target_arch = "x86_64", @@ -102,8 +108,9 @@ mod sse42_compile_time { pub use self::sse42_compile_time::*; #[cfg(all( - httparse_simd, - httparse_simd_target_feature_avx2, + not(any(httparse_disable_simd, miri)), + feature = "std", + target_feature = "avx2", any( target_arch = "x86", target_arch = "x86_64", @@ -117,20 +124,19 @@ mod avx2_compile_time { #[inline(always)] pub fn match_uri_vectored(b: &mut crate::iter::Bytes<'_>) { - // SAFETY: calls are guarded by a compile time feature check - unsafe { crate::simd::avx2::match_uri_vectored(b) } + unsafe { super::avx2::match_uri_vectored(b) } } - + #[inline(always)] pub fn match_header_value_vectored(b: &mut crate::iter::Bytes<'_>) { - // SAFETY: calls are guarded by a compile time feature check - unsafe { crate::simd::avx2::match_header_value_vectored(b) } + unsafe { super::avx2::match_header_value_vectored(b) } } } #[cfg(all( - httparse_simd, - httparse_simd_target_feature_avx2, + not(any(httparse_disable_simd, miri)), + feature = "std", + target_feature = "avx2", any( target_arch = "x86", target_arch = "x86_64", @@ -139,15 +145,17 @@ mod avx2_compile_time { pub use self::avx2_compile_time::*; #[cfg(all( - httparse_simd, + not(any(httparse_disable_simd, miri)), + feature = "std", target_arch = "aarch64", - httparse_simd_neon_intrinsics, + target_feature = "neon", ))] mod neon; #[cfg(all( - httparse_simd, + not(any(httparse_disable_simd, miri)), + feature = "std", target_arch = "aarch64", - httparse_simd_neon_intrinsics, + target_feature = "neon", ))] pub use self::neon::*; From 21d28056a1497cb2cbf2a13e698c106da6ad5ad6 Mon Sep 17 00:00:00 2001 From: Cathal Date: Mon, 10 Nov 2025 13:44:49 +0000 Subject: [PATCH 3/8] Support no_std compile-time SIMD (#214) --- src/lib.rs | 2 +- src/simd/mod.rs | 31 ++++++++++++++++++++----------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1af4b20..dadb63b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ //! parsing internals use an `Iterator` instead of direct indexing, while //! skipping bounds checks. //! -//! With Rust 1.27.0 or later, support for SIMD is enabled automatically. +//! SIMD optimizations are enabled automatically when available. //! If building an executable to be run on multiple platforms, and thus //! not passing `target_feature` or `target_cpu` flags to the compiler, //! runtime detection can still detect SSE4.2 or AVX2 support to provide diff --git a/src/simd/mod.rs b/src/simd/mod.rs index 39e61ce..9635551 100644 --- a/src/simd/mod.rs +++ b/src/simd/mod.rs @@ -3,7 +3,6 @@ mod swar; #[cfg(any( httparse_disable_simd, miri, - not(feature = "std"), not(any( target_arch = "x86", target_arch = "x86_64", @@ -11,13 +10,27 @@ mod swar; target_arch = "aarch64", target_feature = "neon", ) - )) + )), + all( + not(feature = "std"), + not(any( + target_feature = "sse4.2", + target_feature = "avx2", + )), + any( + target_arch = "x86", + target_arch = "x86_64", + ), + ) ))] pub use self::swar::*; #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", + any( + feature = "std", + target_feature = "sse4.2", + ), not(target_feature = "avx2"), any( target_arch = "x86", @@ -28,10 +41,12 @@ mod sse42; #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", any( target_feature = "avx2", - not(target_feature = "sse4.2"), + all( + feature = "std", + not(target_feature = "sse4.2"), + ), ), any( target_arch = "x86", @@ -70,7 +85,6 @@ pub use self::runtime::*; #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", target_feature = "sse4.2", not(target_feature = "avx2"), any( @@ -97,7 +111,6 @@ mod sse42_compile_time { #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", target_feature = "sse4.2", not(target_feature = "avx2"), any( @@ -109,7 +122,6 @@ pub use self::sse42_compile_time::*; #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", target_feature = "avx2", any( target_arch = "x86", @@ -135,7 +147,6 @@ mod avx2_compile_time { #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", target_feature = "avx2", any( target_arch = "x86", @@ -146,7 +157,6 @@ pub use self::avx2_compile_time::*; #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", target_arch = "aarch64", target_feature = "neon", ))] @@ -154,7 +164,6 @@ mod neon; #[cfg(all( not(any(httparse_disable_simd, miri)), - feature = "std", target_arch = "aarch64", target_feature = "neon", ))] From dc64fbeaa0419604091256b1bab2549682d32bae Mon Sep 17 00:00:00 2001 From: jasmy <3776356370@qq.com> Date: Wed, 21 Jan 2026 20:43:38 +0800 Subject: [PATCH 4/8] update copyright year (#216) --- LICENSE-MIT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE-MIT b/LICENSE-MIT index 5e854c5..d59eca4 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2015-2025 Sean McArthur +Copyright (c) 2015-2026 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 4b902ecfa09087c54e5b81e3dc674da5fff19722 Mon Sep 17 00:00:00 2001 From: thesmartshadow Date: Fri, 6 Feb 2026 00:38:35 +0300 Subject: [PATCH 5/8] docs: warn about retry loops on Status::Partial (#217) --- src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index dadb63b..005fbca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -545,10 +545,13 @@ impl<'h, 'b> Request<'h, 'b> { } } } - /// Try to parse a buffer of bytes into the Request. /// /// Returns byte offset in `buf` to start of HTTP body. + /// + /// # Warning + /// In streaming code, if this returns `Status::Partial`, avoid retry loops + /// on a growing buffer without size/time limits; CPU usage may grow fast. pub fn parse(&mut self, buf: &'b [u8]) -> Result { self.parse_with_config(buf, &Default::default()) } @@ -625,8 +628,11 @@ impl<'h, 'b> Response<'h, 'b> { headers, } } - /// Try to parse a buffer of bytes into this `Response`. + /// + /// # Warning + /// In streaming code, if this returns `Status::Partial`, avoid retry loops + /// on a growing buffer without size/time limits; CPU usage may grow fast. pub fn parse(&mut self, buf: &'b [u8]) -> Result { self.parse_with_config(buf, &ParserConfig::default()) } From ac44a38a6b3d1f2dde01c250f622def4aa89f5e0 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Fri, 20 Mar 2026 12:34:12 -0400 Subject: [PATCH 6/8] refactor: remove code duplication in chunk size parsing (#219) The logic for uppercase letters and lowercase letters can be merged. --- src/lib.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 005fbca..e8c38a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1287,7 +1287,7 @@ pub fn parse_chunk_size(buf: &[u8]) size *= RADIX; size += (b - b'0') as u64; }, - b'a' ..= b'f' if in_chunk_size => { + b'a' ..= b'f' | b'A' ..= b'F' if in_chunk_size => { if count > 15 { return Err(InvalidChunkSize); } @@ -1296,18 +1296,7 @@ pub fn parse_chunk_size(buf: &[u8]) return Err(InvalidChunkSize); } size *= RADIX; - size += (b + 10 - b'a') as u64; - } - b'A' ..= b'F' if in_chunk_size => { - if count > 15 { - return Err(InvalidChunkSize); - } - count += 1; - if cfg!(debug_assertions) && size > (u64::MAX / RADIX) { - return Err(InvalidChunkSize); - } - size *= RADIX; - size += (b + 10 - b'A') as u64; + size += ((b | 0x20) + 10 - b'a') as u64; } b'\r' => { match next!(bytes) { From e48b2488f13927c064b06e4073173afdc115357a Mon Sep 17 00:00:00 2001 From: Thalia Archibald Date: Sun, 21 Jun 2026 14:54:40 -0600 Subject: [PATCH 7/8] refactor: use const generics from Rust 1.51 (#220) --- src/iter.rs | 8 ++------ src/lib.rs | 6 +++--- src/simd/swar.rs | 6 +++--- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/iter.rs b/src/iter.rs index 06f78c3..d8d5adf 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -1,4 +1,3 @@ -use core::convert::TryFrom; use core::convert::TryInto; #[allow(missing_docs)] @@ -63,11 +62,8 @@ impl<'a> Bytes<'a> { } #[inline] - pub fn peek_n<'b: 'a, U: TryFrom<&'a [u8]>>(&'b self, n: usize) -> Option { - // TODO: once we bump MSRV, use const generics to allow only [u8; N] reads - // TODO: drop `n` arg in favour of const - // let n = core::mem::size_of::(); - self.as_ref().get(..n)?.try_into().ok() + pub fn peek_n(&self) -> Option<[u8; N]> { + self.as_ref().get(..N)?.try_into().ok() } /// Advance by 1, equivalent to calling `advance(1)`. diff --git a/src/lib.rs b/src/lib.rs index e8c38a6..7333bf1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -762,10 +762,10 @@ pub const EMPTY_HEADER: Header<'static> = Header { name: "", value: b"" }; #[allow(missing_docs)] // WARNING: Exported for internal benchmarks, not fit for public consumption pub fn parse_version(bytes: &mut Bytes) -> Result { - if let Some(eight) = bytes.peek_n::<[u8; 8]>(8) { + if let Some(eight) = bytes.peek_n::<8>() { const H10: u64 = u64::from_ne_bytes(*b"HTTP/1.0"); const H11: u64 = u64::from_ne_bytes(*b"HTTP/1.1"); - // SAFETY: peek_n(8) before ensure within bounds + // SAFETY: peek_n before ensures within bounds unsafe { bytes.advance(8); } @@ -797,7 +797,7 @@ pub fn parse_version(bytes: &mut Bytes) -> Result { pub fn parse_method<'a>(bytes: &mut Bytes<'a>) -> Result<&'a str> { const GET: [u8; 4] = *b"GET "; const POST: [u8; 4] = *b"POST"; - match bytes.peek_n::<[u8; 4]>(4) { + match bytes.peek_n::<4>() { Some(GET) => { // SAFETY: we matched "GET " which has 4 bytes and is ASCII let method = unsafe { diff --git a/src/simd/swar.rs b/src/simd/swar.rs index ef7435f..67f421c 100644 --- a/src/simd/swar.rs +++ b/src/simd/swar.rs @@ -9,7 +9,7 @@ type ByteBlock = [u8; BLOCK_SIZE]; #[inline] pub fn match_uri_vectored(bytes: &mut Bytes) { loop { - if let Some(bytes8) = bytes.peek_n::(BLOCK_SIZE) { + if let Some(bytes8) = bytes.peek_n::() { let n = match_uri_char_8_swar(bytes8); // SAFETY: using peek_n to retrieve the bytes ensures that there are at least n more bytes // in `bytes`, so calling `advance(n)` is safe. @@ -37,7 +37,7 @@ pub fn match_uri_vectored(bytes: &mut Bytes) { #[inline] pub fn match_header_value_vectored(bytes: &mut Bytes) { loop { - if let Some(bytes8) = bytes.peek_n::(BLOCK_SIZE) { + if let Some(bytes8) = bytes.peek_n::() { let n = match_header_value_char_8_swar(bytes8); // SAFETY: using peek_n to retrieve the bytes ensures that there are at least n more bytes // in `bytes`, so calling `advance(n)` is safe. @@ -64,7 +64,7 @@ pub fn match_header_value_vectored(bytes: &mut Bytes) { #[inline] pub fn match_header_name_vectored(bytes: &mut Bytes) { - while let Some(block) = bytes.peek_n::(BLOCK_SIZE) { + while let Some(block) = bytes.peek_n::() { let n = match_block(is_header_name_token, block); // SAFETY: using peek_n to retrieve the bytes ensures that there are at least n more bytes // in `bytes`, so calling `advance(n)` is safe. From a0fa552e4e0fa9a5914a736ef3a556748fef709b Mon Sep 17 00:00:00 2001 From: Thalia Archibald Date: Tue, 30 Jun 2026 07:13:39 -0600 Subject: [PATCH 8/8] refactor: share invalid header handling (#221) handle_invalid_char! duplicates the invalid char recovery loop at all macro expansions. Share it with a labeled block. This reduces code size by about 1.7-3KiB. --- src/lib.rs | 281 ++++++++++++++++++++++++++--------------------------- 1 file changed, 138 insertions(+), 143 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7333bf1..98b37c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1072,178 +1072,173 @@ fn parse_headers_iter_uninit<'a>( } 'headers: loop { - // Return the error `$err` if `ignore_invalid_headers_in_responses` - // is false, otherwise find the end of the current line and resume - // parsing on the next one. - macro_rules! handle_invalid_char { - ($bytes:ident, $b:ident, $err:ident) => { - if !config.ignore_invalid_headers { - return Err(Error::$err); - } - - let mut b = $b; + let mut b; - loop { - if b == b'\r' { - expect!(bytes.next() == b'\n' => Err(Error::$err)); - break; - } - if b == b'\n' { - break; - } - if b == b'\0' { - return Err(Error::$err); + #[allow(clippy::never_loop)] + let invalid_header_err = 'header: loop { + // a newline here means the head is over! + b = next!(bytes); + if b == b'\r' { + expect!(bytes.next() == b'\n' => Err(Error::NewLine)); + let end = bytes.as_ref().as_ptr() as usize; + result = Ok(Status::Complete(end - start)); + break 'headers; + } + if b == b'\n' { + let end = bytes.as_ref().as_ptr() as usize; + result = Ok(Status::Complete(end - start)); + break 'headers; + } + if !is_header_name_token(b) { + if config.allow_space_before_first_header_name + && autoshrink.num_headers == 0 + && (b == b' ' || b == b'\t') + { + //advance past white space and then try parsing header again + while let Some(peek) = bytes.peek() { + if peek == b' ' || peek == b'\t' { + next!(bytes); + } else { + break; + } } - b = next!($bytes); + bytes.slice(); + continue 'headers; + } else { + break 'header Error::HeaderName; } + } - $bytes.slice(); + #[allow(clippy::never_loop)] + // parse header name until colon + let header_name: &str = 'name: loop { + simd::match_header_name_vectored(bytes); + b = next!(bytes); - continue 'headers; - }; - } + // SAFETY: previously bumped by 1 with next! -> always safe. + let bslice = unsafe { bytes.slice_skip(1) }; + // SAFETY: previous call to match_header_name_vectored ensured all bytes are valid + // header name chars, and as such also valid utf-8. + let name = unsafe { str::from_utf8_unchecked(bslice) }; - // a newline here means the head is over! - let b = next!(bytes); - if b == b'\r' { - expect!(bytes.next() == b'\n' => Err(Error::NewLine)); - let end = bytes.as_ref().as_ptr() as usize; - result = Ok(Status::Complete(end - start)); - break; - } - if b == b'\n' { - let end = bytes.as_ref().as_ptr() as usize; - result = Ok(Status::Complete(end - start)); - break; - } - if !is_header_name_token(b) { - if config.allow_space_before_first_header_name - && autoshrink.num_headers == 0 - && (b == b' ' || b == b'\t') - { - //advance past white space and then try parsing header again - while let Some(peek) = bytes.peek() { - if peek == b' ' || peek == b'\t' { - next!(bytes); - } else { - break; + if b == b':' { + break 'name name; + } + + if config.allow_spaces_after_header_name { + while b == b' ' || b == b'\t' { + b = next!(bytes); + + if b == b':' { + bytes.slice(); + break 'name name; + } } } - bytes.slice(); - continue 'headers; - } else { - handle_invalid_char!(bytes, b, HeaderName); - } - } - #[allow(clippy::never_loop)] - // parse header name until colon - let header_name: &str = 'name: loop { - simd::match_header_name_vectored(bytes); - let mut b = next!(bytes); - - // SAFETY: previously bumped by 1 with next! -> always safe. - let bslice = unsafe { bytes.slice_skip(1) }; - // SAFETY: previous call to match_header_name_vectored ensured all bytes are valid - // header name chars, and as such also valid utf-8. - let name = unsafe { str::from_utf8_unchecked(bslice) }; - - if b == b':' { - break 'name name; - } + break 'header Error::HeaderName; + }; - if config.allow_spaces_after_header_name { - while b == b' ' || b == b'\t' { + #[allow(clippy::never_loop)] + let value_slice = 'value: loop { + // eat white space between colon and value + 'whitespace_after_colon: loop { b = next!(bytes); - - if b == b':' { + if b == b' ' || b == b'\t' { bytes.slice(); - break 'name name; + continue 'whitespace_after_colon; + } + if is_header_value_token(b) { + break 'whitespace_after_colon; } - } - } - handle_invalid_char!(bytes, b, HeaderName); - }; + if b == b'\r' { + expect!(bytes.next() == b'\n' => Err(Error::HeaderValue)); + } else if b != b'\n' { + break 'header Error::HeaderValue; + } - let mut b; + maybe_continue_after_obsolete_line_folding!(bytes, 'whitespace_after_colon); - #[allow(clippy::never_loop)] - let value_slice = 'value: loop { - // eat white space between colon and value - 'whitespace_after_colon: loop { - b = next!(bytes); - if b == b' ' || b == b'\t' { - bytes.slice(); - continue 'whitespace_after_colon; - } - if is_header_value_token(b) { - break 'whitespace_after_colon; - } + let whitespace_slice = bytes.slice(); - if b == b'\r' { - expect!(bytes.next() == b'\n' => Err(Error::HeaderValue)); - } else if b != b'\n' { - handle_invalid_char!(bytes, b, HeaderValue); + // This produces an empty slice that points to the beginning + // of the whitespace. + break 'value &whitespace_slice[0..0]; } - maybe_continue_after_obsolete_line_folding!(bytes, 'whitespace_after_colon); - - let whitespace_slice = bytes.slice(); + 'value_lines: loop { + // parse value till EOL - // This produces an empty slice that points to the beginning - // of the whitespace. - break 'value &whitespace_slice[0..0]; - } + simd::match_header_value_vectored(bytes); + b = next!(bytes); - 'value_lines: loop { - // parse value till EOL + //found_ctl + let skip = if b == b'\r' { + expect!(bytes.next() == b'\n' => Err(Error::HeaderValue)); + 2 + } else if b == b'\n' { + 1 + } else { + break 'header Error::HeaderValue; + }; - simd::match_header_value_vectored(bytes); - let b = next!(bytes); + maybe_continue_after_obsolete_line_folding!(bytes, 'value_lines); - //found_ctl - let skip = if b == b'\r' { - expect!(bytes.next() == b'\n' => Err(Error::HeaderValue)); - 2 - } else if b == b'\n' { - 1 - } else { - handle_invalid_char!(bytes, b, HeaderValue); - }; + // SAFETY: having just checked that a newline exists, it's safe to skip it. + unsafe { + break 'value bytes.slice_skip(skip); + } + } + }; - maybe_continue_after_obsolete_line_folding!(bytes, 'value_lines); + let uninit_header = match iter.next() { + Some(header) => header, + None => break 'headers + }; - // SAFETY: having just checked that a newline exists, it's safe to skip it. - unsafe { - break 'value bytes.slice_skip(skip); - } - } - }; + // trim trailing whitespace in the header + let header_value = if let Some(last_visible) = value_slice + .iter() + .rposition(|b| *b != b' ' && *b != b'\t' && *b != b'\r' && *b != b'\n') + { + // There is at least one non-whitespace character. + &value_slice[0..last_visible+1] + } else { + // There is no non-whitespace character. This can only happen when value_slice is + // empty. + value_slice + }; - let uninit_header = match iter.next() { - Some(header) => header, - None => break 'headers + *uninit_header = MaybeUninit::new(Header { + name: header_name, + value: header_value, + }); + autoshrink.num_headers += 1; + continue 'headers; }; - // trim trailing whitespace in the header - let header_value = if let Some(last_visible) = value_slice - .iter() - .rposition(|b| *b != b' ' && *b != b'\t' && *b != b'\r' && *b != b'\n') - { - // There is at least one non-whitespace character. - &value_slice[0..last_visible+1] - } else { - // There is no non-whitespace character. This can only happen when value_slice is - // empty. - value_slice - }; + // The header contains an invalid character. Reject the header if + // `ignore_invalid_headers_in_responses` is false; otherwise find the + // end of the current line and resume parsing on the next one. + if !config.ignore_invalid_headers { + return Err(invalid_header_err); + } - *uninit_header = MaybeUninit::new(Header { - name: header_name, - value: header_value, - }); - autoshrink.num_headers += 1; + loop { + if b == b'\r' { + expect!(bytes.next() == b'\n' => Err(invalid_header_err)); + break; + } + if b == b'\n' { + break; + } + if b == b'\0' { + return Err(invalid_header_err); + } + b = next!(bytes); + } + bytes.slice(); } result