From 4c812699cad5d33b249b60963f8892b21687c8e3 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 6 May 2025 20:33:42 +0800 Subject: [PATCH 01/20] feat: add fasthttp feature --- Cargo.toml | 3 + src/ext/fasthttp/consts.rs | 87 +++++++++++++++++++++ src/ext/fasthttp/header_name.rs | 133 ++++++++++++++++++++++++++++++++ src/ext/fasthttp/mod.rs | 2 + src/ext/mod.rs | 2 + src/header/map.rs | 92 +++++++++++++++++----- src/header/name.rs | 91 +++++++++++++++++----- src/header/value.rs | 8 +- src/lib.rs | 1 + src/method.rs | 4 +- src/uri/authority.rs | 4 +- src/uri/mod.rs | 2 +- src/uri/path.rs | 4 +- tests/header_map.rs | 15 +++- 14 files changed, 396 insertions(+), 52 deletions(-) create mode 100644 src/ext/fasthttp/consts.rs create mode 100644 src/ext/fasthttp/header_name.rs create mode 100644 src/ext/fasthttp/mod.rs create mode 100644 src/ext/mod.rs diff --git a/Cargo.toml b/Cargo.toml index fd20b117..d100aa3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,11 +36,14 @@ exclude = [ default = ["std"] std = [] double-write = ["default"] +fasthttp = ["default"] # 对齐老网关 fasthttp 行为 [dependencies] bytes = "1" fnv = "1.0.5" itoa = "1" +trie-rs = "0.4.2" +lazy_static = "1.5" [dev-dependencies] quickcheck = "1" diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs new file mode 100644 index 00000000..b1620078 --- /dev/null +++ b/src/ext/fasthttp/consts.rs @@ -0,0 +1,87 @@ +use crate::header::{ + ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, AUTHORIZATION, CONNECTION, CONTENT_ENCODING, + CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, COOKIE, DATE, HOST, IF_MODIFIED_SINCE, + LAST_MODIFIED, LOCATION, ORIGIN, RANGE, REFERER, SERVER, SET_COOKIE, TRANSFER_ENCODING, + USER_AGENT, +}; +use lazy_static::lazy_static; +use trie_rs::{Trie, TrieBuilder}; + +pub(crate) struct StandardHeaders(Trie); + +impl StandardHeaders { + fn new() -> StandardHeaders { + // FIXME: 是否需要将 "Content-type" 也加入到 trie 中? + let mut builder = TrieBuilder::new(); + + // RFC标准header + builder.push(ACCEPT_RANGES); + builder.push(ACCEPT_RANGES.as_str().to_ascii_lowercase()); + builder.push(ACCEPT_LANGUAGE); + builder.push(ACCEPT_LANGUAGE.as_str().to_ascii_lowercase()); + builder.push(ACCEPT_ENCODING); + builder.push(ACCEPT_ENCODING.as_str().to_ascii_lowercase()); + builder.push(AUTHORIZATION); + builder.push(AUTHORIZATION.as_str().to_ascii_lowercase()); + builder.push("Expect"); + builder.push("expect"); + builder.push(CONTENT_TYPE); + builder.push(CONTENT_TYPE.as_str().to_ascii_lowercase()); + builder.push(CONTENT_ENCODING); + builder.push(CONTENT_ENCODING.as_str().to_ascii_lowercase()); + builder.push(CONTENT_RANGE); + builder.push(CONTENT_RANGE.as_str().to_ascii_lowercase()); + builder.push(CONTENT_LENGTH); + builder.push(CONTENT_LENGTH.as_str().to_ascii_lowercase()); + builder.push(COOKIE); + builder.push(COOKIE.as_str().to_ascii_lowercase()); + builder.push(CONNECTION); + builder.push(CONNECTION.as_str().to_ascii_lowercase()); + builder.push(DATE); + builder.push(DATE.as_str().to_ascii_lowercase()); + builder.push(HOST); + builder.push(HOST.as_str().to_ascii_lowercase()); + builder.push(IF_MODIFIED_SINCE); + builder.push(IF_MODIFIED_SINCE.as_str().to_ascii_lowercase()); + builder.push(LOCATION); + builder.push(LOCATION.as_str().to_ascii_lowercase()); + builder.push(LAST_MODIFIED); + builder.push(LAST_MODIFIED.as_str().to_ascii_lowercase()); + builder.push(REFERER); + builder.push(REFERER.as_str().to_ascii_lowercase()); + builder.push(RANGE); + builder.push(RANGE.as_str().to_ascii_lowercase()); + builder.push(SERVER); + builder.push(SERVER.as_str().to_ascii_lowercase()); + builder.push(SET_COOKIE); + builder.push(SET_COOKIE.as_str().to_ascii_lowercase()); + builder.push(TRANSFER_ENCODING); + builder.push(TRANSFER_ENCODING.as_str().to_ascii_lowercase()); + builder.push(USER_AGENT); + builder.push(USER_AGENT.as_str().to_ascii_lowercase()); + builder.push(ORIGIN); + builder.push(ORIGIN.as_str().to_ascii_lowercase()); + + // 内部标准header + builder.push("X-Common-Params"); + builder.push("x-common-params"); + builder.push("Use-Ppe"); + builder.push("use-ppe"); + builder.push("Tt-Logid"); + builder.push("tt-logid"); + builder.push("Tt-Env"); + builder.push("tt-env"); + + let trie = builder.build(); + + StandardHeaders(trie) + } + + pub fn is_std_header(&self, header_name: &str) -> bool { + self.0.exact_match(header_name) + } +} + +lazy_static! { + pub(crate) static ref STANDARD_HEADERS: StandardHeaders = StandardHeaders::new(); +} diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs new file mode 100644 index 00000000..772720ea --- /dev/null +++ b/src/ext/fasthttp/header_name.rs @@ -0,0 +1,133 @@ +use crate::HeaderName; + +const TO_LOWER: u8 = b'a' - b'A'; + +lazy_static::lazy_static! { + static ref TO_LOWER_TABLE: [u8; 256] = { + let mut a = [0u8; 256]; + for i in 0..256 { + let mut c = i as u8; + if (b'A'..=b'Z').contains(&c){ + c += TO_LOWER; + } + a[i] = c; + } + a + }; + + static ref TO_UPPER_TABLE: [u8; 256] = { + let mut a = [0u8; 256]; + for i in 0..256 { + let mut c = i as u8; + if (b'a'..=b'z').contains(&c){ + c -= TO_LOWER; + } + a[i] = c; + } + a + }; +} + +pub(crate) unsafe fn normalize_header_key2(header_name: &K) -> impl Into +where + K: Into + Clone, +{ + let header_name: HeaderName = header_name.clone().into(); + let mut header_name_str = header_name.as_raw_str().to_string(); + // let normalized_header_name = header_name.as_str(); + let n = header_name_str.len(); + if n == 0 { + return header_name; + } + + let header_name_bytes = header_name_str.as_bytes_mut(); + header_name_bytes[0] = TO_UPPER_TABLE[header_name_bytes[0] as usize]; + let mut i = 1; + while i < n { + let p = &mut header_name_bytes[i]; + if *p == b'-' { + i += 1; + if i < n { + header_name_bytes[i] = TO_UPPER_TABLE[header_name_bytes[i] as usize]; + i += 1; + } + continue; + } + *p = TO_LOWER_TABLE[*p as usize]; + i += 1; + } + + HeaderName::from_bytes(header_name_bytes).unwrap() +} + +#[allow(dead_code)] +#[cfg(test)] +mod tests { + use crate::ext::fasthttp::consts::STANDARD_HEADERS; + use crate::ext::fasthttp::header_name::normalize_header_key2; + use crate::HeaderName; + use std::str::FromStr; + + fn normalize_header_key(header_name: &mut HeaderName, disable_normalizing: bool) -> HeaderName { + if disable_normalizing { + return header_name.clone(); + } + + // 只normalize标准header,其他自定义header保持原样透传 + if !STANDARD_HEADERS.is_std_header(header_name.as_str()) { + return header_name.clone(); + } + + unsafe { normalize_header_key2(header_name).into() } + } + + #[test] + fn test_normalize_header_key() { + let mut header_name = HeaderName::from_str("content-type").unwrap(); + assert_eq!( + normalize_header_key(&mut header_name, false).as_raw_str(), + "Content-Type" + ); + + let mut header_name = HeaderName::from_str("Content-Type").unwrap(); + assert_eq!( + normalize_header_key(&mut header_name, false).as_raw_str(), + "Content-Type" + ); + + // 非标准header,不做处理 + let mut header_name = HeaderName::from_str("x-tt-agw").unwrap(); + assert_eq!( + normalize_header_key(&mut header_name, false).as_raw_str(), + "x-tt-agw" + ); + } + + #[test] + fn test_normalize_header_key2() { + let mut header_name = HeaderName::from_str("content-type").unwrap(); + unsafe { + assert_eq!( + normalize_header_key2(&mut header_name).into().as_raw_str(), + "Content-Type" + ); + } + + let mut header_name = HeaderName::from_str("Content-Type").unwrap(); + unsafe { + assert_eq!( + normalize_header_key2(&mut header_name).into().as_raw_str(), + "Content-Type" + ); + } + + // 非标准header,不做处理 + let mut header_name = HeaderName::from_str("x-tt-agw").unwrap(); + unsafe { + assert_eq!( + normalize_header_key2(&mut header_name).into().as_raw_str(), + "X-Tt-Agw" + ); + } + } +} diff --git a/src/ext/fasthttp/mod.rs b/src/ext/fasthttp/mod.rs new file mode 100644 index 00000000..30cf9b3a --- /dev/null +++ b/src/ext/fasthttp/mod.rs @@ -0,0 +1,2 @@ +mod consts; +pub mod header_name; diff --git a/src/ext/mod.rs b/src/ext/mod.rs new file mode 100644 index 00000000..ff9558b6 --- /dev/null +++ b/src/ext/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "fasthttp")] +pub mod fasthttp; diff --git a/src/header/map.rs b/src/header/map.rs index a668b311..c0ca543e 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -1,3 +1,10 @@ +pub use self::as_header_name::AsHeaderName; +pub use self::into_header_name::IntoHeaderName; +use super::name::{HdrName, HeaderName, InvalidHeaderName}; +use super::HeaderValue; +#[cfg(feature = "fasthttp")] +use crate::ext::fasthttp::header_name::normalize_header_key2; +use crate::Error; use std::collections::hash_map::RandomState; use std::collections::HashMap; use std::convert::TryFrom; @@ -6,14 +13,6 @@ use std::iter::{FromIterator, FusedIterator}; use std::marker::PhantomData; use std::{fmt, mem, ops, ptr, vec}; -use crate::Error; - -use super::name::{HdrName, HeaderName, InvalidHeaderName}; -use super::HeaderValue; - -pub use self::as_header_name::AsHeaderName; -pub use self::into_header_name::IntoHeaderName; - /// A set of HTTP headers /// /// `HeaderMap` is a multimap of [`HeaderName`] to values. @@ -49,6 +48,17 @@ pub struct HeaderMap { entries: Vec>, extra_values: Vec>, danger: Danger, + + #[cfg(feature = "fasthttp")] + keys: HashMap, // normalized_key -> original_key +} + +#[cfg(feature = "fasthttp")] +pub fn normalize_key(key: &K) -> impl Into +where + K: Into + Clone, +{ + unsafe { normalize_header_key2(key) } } // # Implementation notes @@ -507,6 +517,8 @@ impl HeaderMap { entries: Vec::new(), extra_values: Vec::new(), danger: Danger::Green, + #[cfg(feature = "fasthttp")] + keys: HashMap::new(), }) } else { let raw_cap = match to_raw_capacity(capacity).checked_next_power_of_two() { @@ -524,6 +536,8 @@ impl HeaderMap { entries: Vec::with_capacity(raw_cap), extra_values: Vec::new(), danger: Danger::Green, + #[cfg(feature = "fasthttp")] + keys: HashMap::new(), }) } } @@ -621,6 +635,8 @@ impl HeaderMap { self.entries.clear(); self.extra_values.clear(); self.danger = Danger::Green; + #[cfg(feature = "fasthttp")] + self.keys.clear(); for e in self.indices.iter_mut() { *e = Pos::none(); @@ -1268,6 +1284,17 @@ impl HeaderMap { key.try_insert(self, val) } + #[inline] + #[cfg(feature = "fasthttp")] + fn update_key_map(&mut self, key: &HeaderName) { + let normalized_key = normalize_key(key).into(); + if let Some(original_key) = self.keys.get(&normalized_key) { + self.remove(original_key.clone()); + } + + self.keys.insert(normalized_key, key.clone()); + } + #[inline] fn try_insert2(&mut self, key: K, value: T) -> Result, MaxSizeReached> where @@ -1286,8 +1313,8 @@ impl HeaderMap { // Vacant { let _ = danger; // Make lint happy - let index = self.entries.len(); self.try_insert_entry(hash, key.into(), value)?; + let index = self.entries.len() - 1; self.indices[probe] = Pos::new(index, hash); None }, @@ -1451,13 +1478,33 @@ impl HeaderMap { #[inline] fn find(&self, key: &K) -> Option<(usize, usize)> where - K: Hash + Into + ?Sized, + K: Hash + Into + Clone, HeaderName: PartialEq, { if self.entries.is_empty() { return None; } + self.find2(key).or({ + #[cfg(feature = "fasthttp")] + { + match self.keys.get(&normalize_key(key).into()) { + Some(original_key) => self.find2::(original_key), + None => None, + } + } + + #[cfg(not(feature = "fasthttp"))] + None + }) + } + + #[inline] + fn find2(&self, key: &K) -> Option<(usize, usize)> + where + K: Hash + Into, + HeaderName: PartialEq, + { let hash = hash_elem_using(&self.danger, key); let mask = self.mask; let mut probe = desired_pos(mask, hash); @@ -1489,6 +1536,8 @@ impl HeaderMap { probe: usize, danger: bool, ) -> Result { + #[cfg(feature = "fasthttp")] + self.update_key_map(&key); // Push the value and get the index let index = self.entries.len(); self.try_insert_entry(hash, key, value)?; @@ -1628,6 +1677,9 @@ impl HeaderMap { key: HeaderName, value: T, ) -> Result<(), MaxSizeReached> { + #[cfg(feature = "fasthttp")] + self.update_key_map(&key); + if self.entries.len() >= MAX_SIZE { return Err(MaxSizeReached::new()); } @@ -3687,7 +3739,7 @@ mod into_header_name { impl IntoHeaderName for HeaderName {} - impl<'a> Sealed for &'a HeaderName { + impl Sealed for &HeaderName { #[inline] fn try_insert( self, @@ -3707,7 +3759,7 @@ mod into_header_name { } } - impl<'a> IntoHeaderName for &'a HeaderName {} + impl IntoHeaderName for &HeaderName {} impl Sealed for &'static str { #[inline] @@ -3791,13 +3843,13 @@ mod as_header_name { } fn as_str(&self) -> &str { - ::as_str(self) + ::as_raw_str(self) } } impl AsHeaderName for HeaderName {} - impl<'a> Sealed for &'a HeaderName { + impl Sealed for &HeaderName { #[inline] fn try_entry(self, map: &mut HeaderMap) -> Result, TryEntryError> { Ok(map.try_entry2(self)?) @@ -3809,13 +3861,13 @@ mod as_header_name { } fn as_str(&self) -> &str { - ::as_str(self) + ::as_raw_str(self) } } - impl<'a> AsHeaderName for &'a HeaderName {} + impl AsHeaderName for &HeaderName {} - impl<'a> Sealed for &'a str { + impl Sealed for &str { #[inline] fn try_entry(self, map: &mut HeaderMap) -> Result, TryEntryError> { Ok(HdrName::from_bytes(self.as_bytes(), move |hdr| { @@ -3833,7 +3885,7 @@ mod as_header_name { } } - impl<'a> AsHeaderName for &'a str {} + impl AsHeaderName for &str {} impl Sealed for String { #[inline] @@ -3853,7 +3905,7 @@ mod as_header_name { impl AsHeaderName for String {} - impl<'a> Sealed for &'a String { + impl Sealed for &String { #[inline] fn try_entry(self, map: &mut HeaderMap) -> Result, TryEntryError> { self.as_str().try_entry(map) @@ -3869,7 +3921,7 @@ mod as_header_name { } } - impl<'a> AsHeaderName for &'a String {} + impl AsHeaderName for &String {} } #[test] diff --git a/src/header/name.rs b/src/header/name.rs index ce3c0161..83acfd61 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -37,7 +37,7 @@ pub struct HeaderName { } // Almost a full `HeaderName` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct HdrName<'a> { inner: Repr>, original: Option<&'a [u8]>, @@ -46,7 +46,7 @@ pub struct HdrName<'a> { impl<'a> Hash for HdrName<'a> { fn hash(&self, state: &mut H) { #[cfg(feature = "double-write")] - if let Some(original)= self.original{ + if let Some(original) = self.original { original.hash(state); return; } @@ -87,27 +87,22 @@ impl Hash for Repr { impl Hash for Repr { fn hash(&self, state: &mut H) { match self { - Repr::Standard(inner, src) => { - match src { - Some(src) => src.hash(state), - None => { - inner.hash(state); - } + Repr::Standard(inner, src) => match src { + Some(src) => src.hash(state), + None => { + inner.hash(state); } - } - Repr::Custom(inner, src) => { - match src { - Some(src) => src.hash(state), - None => { - inner.hash(state); - } + }, + Repr::Custom(inner, src) => match src { + Some(src) => src.hash(state), + None => { + inner.hash(state); } }, } } } - // Used to hijack the Hash impl #[derive(Debug, Clone, Eq, PartialEq)] struct Custom(ByteStr); @@ -1580,7 +1575,7 @@ impl<'a> PartialEq<&'a HeaderName> for HeaderName { } } -impl<'a> PartialEq for &'a HeaderName { +impl PartialEq for &HeaderName { #[inline] fn eq(&self, other: &HeaderName) -> bool { *other == *self @@ -1634,7 +1629,7 @@ impl<'a> PartialEq<&'a str> for HeaderName { } } -impl<'a> PartialEq for &'a str { +impl PartialEq for &str { /// Performs a case-insensitive comparison of the string against the header /// name #[inline] @@ -1837,6 +1832,7 @@ unsafe fn slice_assume_init(slice: &[MaybeUninit]) -> &[T] { mod tests { use self::StandardHeader::Vary; use super::*; + use crate::HeaderMap; #[test] fn test_bounds() { @@ -2123,4 +2119,63 @@ mod tests { HeaderName::from_lowercase(&[0x1; 100]).unwrap_err(); HeaderName::from_lowercase(&[0xFF; 100]).unwrap_err(); } + + #[test] + fn test_insert() { + let mut header_map = HeaderMap::new(); + + header_map.insert("x-tt-agw-key1", "1".parse().unwrap()); + assert_eq!(header_map.get("x-tt-agw-key1").unwrap(), "1"); + + header_map.insert("X-Tt-agw-key1", "2".parse().unwrap()); + assert_eq!(header_map.get("X-Tt-agw-key1").unwrap(), "2"); + } +} + +#[cfg(test)] +#[cfg(feature = "fasthttp")] +mod fasthttp_tests { + use crate::header::HeaderMap; + use crate::{HeaderName, HeaderValue}; + + #[test] + fn test_insert() { + let mut header_map = HeaderMap::new(); + + header_map.insert( + HeaderName::from_bytes("x-tt-agw-key1".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + assert_eq!(header_map.get("x-tt-agw-key1").unwrap(), "1"); + assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "1"); + assert_eq!(header_map.len(), 1); + + header_map.insert( + HeaderName::from_bytes("X-Tt-agw-key1".as_bytes()).unwrap(), + HeaderValue::from_static("2"), + ); + assert_eq!(header_map.get("X-Tt-agw-key1").unwrap(), "2"); + assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "2"); + assert_eq!(header_map.len(), 1); + + header_map.insert( + HeaderName::from_bytes("x-tt-agw-key2".as_bytes()).unwrap(), + HeaderValue::from_static("3"), + ); + assert_eq!(header_map.len(), 2); + + let mut iter = header_map.iter(); + + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("X-Tt-agw-key1", "2") + ); + + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("x-tt-agw-key2", "3") + ); + } } diff --git a/src/header/value.rs b/src/header/value.rs index 4813f6fd..0d17143c 100644 --- a/src/header/value.rs +++ b/src/header/value.rs @@ -745,14 +745,14 @@ impl PartialOrd for String { } } -impl<'a> PartialEq for &'a HeaderValue { +impl PartialEq for &HeaderValue { #[inline] fn eq(&self, other: &HeaderValue) -> bool { **self == *other } } -impl<'a> PartialOrd for &'a HeaderValue { +impl PartialOrd for &HeaderValue { #[inline] fn partial_cmp(&self, other: &HeaderValue) -> Option { (**self).partial_cmp(other) @@ -779,14 +779,14 @@ where } } -impl<'a> PartialEq for &'a str { +impl PartialEq for &str { #[inline] fn eq(&self, other: &HeaderValue) -> bool { *other == *self } } -impl<'a> PartialOrd for &'a str { +impl PartialOrd for &str { #[inline] fn partial_cmp(&self, other: &HeaderValue) -> Option { self.as_bytes().partial_cmp(other.as_bytes()) diff --git a/src/lib.rs b/src/lib.rs index d43d51a7..d9518323 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -183,6 +183,7 @@ pub mod version; mod byte_str; mod error; +mod ext; mod extensions; pub use crate::error::{Error, Result}; diff --git a/src/method.rs b/src/method.rs index 3d45ef9f..54bd2978 100644 --- a/src/method.rs +++ b/src/method.rs @@ -194,7 +194,7 @@ impl<'a> PartialEq<&'a Method> for Method { } } -impl<'a> PartialEq for &'a Method { +impl PartialEq for &Method { #[inline] fn eq(&self, other: &Method) -> bool { *self == other @@ -222,7 +222,7 @@ impl<'a> PartialEq<&'a str> for Method { } } -impl<'a> PartialEq for &'a str { +impl PartialEq for &str { #[inline] fn eq(&self, other: &Method) -> bool { *self == other.as_ref() diff --git a/src/uri/authority.rs b/src/uri/authority.rs index dab6dcd0..c290c529 100644 --- a/src/uri/authority.rs +++ b/src/uri/authority.rs @@ -302,7 +302,7 @@ impl PartialEq for str { } } -impl<'a> PartialEq for &'a str { +impl PartialEq for &str { fn eq(&self, other: &Authority) -> bool { self.eq_ignore_ascii_case(other.as_str()) } @@ -360,7 +360,7 @@ impl PartialOrd for str { } } -impl<'a> PartialOrd for &'a str { +impl PartialOrd for &str { fn partial_cmp(&self, other: &Authority) -> Option { let left = self.as_bytes().iter().map(|b| b.to_ascii_lowercase()); let right = other.data.as_bytes().iter().map(|b| b.to_ascii_lowercase()); diff --git a/src/uri/mod.rs b/src/uri/mod.rs index 767f0743..ff9908e9 100644 --- a/src/uri/mod.rs +++ b/src/uri/mod.rs @@ -1001,7 +1001,7 @@ impl<'a> PartialEq<&'a str> for Uri { } } -impl<'a> PartialEq for &'a str { +impl PartialEq for &str { fn eq(&self, uri: &Uri) -> bool { uri == *self } diff --git a/src/uri/path.rs b/src/uri/path.rs index 6a2f1e1e..eb1c45b9 100644 --- a/src/uri/path.rs +++ b/src/uri/path.rs @@ -369,7 +369,7 @@ impl PartialEq for PathAndQuery { } } -impl<'a> PartialEq for &'a str { +impl PartialEq for &str { #[inline] fn eq(&self, other: &PathAndQuery) -> bool { self == &other.as_str() @@ -432,7 +432,7 @@ impl<'a> PartialOrd<&'a str> for PathAndQuery { } } -impl<'a> PartialOrd for &'a str { +impl PartialOrd for &str { #[inline] fn partial_cmp(&self, other: &PathAndQuery) -> Option { self.partial_cmp(&other.as_str()) diff --git a/tests/header_map.rs b/tests/header_map.rs index fa2f1efb..9e8ca507 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -678,10 +678,19 @@ fn feature_double_write() { assert_eq!(headers.get("foo"), Some(&HeaderValue::from_static("bar"))); assert_eq!(headers.get("Foo"), Some(&HeaderValue::from_static("Bar"))); - assert_eq!(headers.get(HeaderName::from_bytes("Foo".as_bytes()).unwrap()), Some(&HeaderValue::from_static("Bar"))); + assert_eq!( + headers.get(HeaderName::from_bytes("Foo".as_bytes()).unwrap()), + Some(&HeaderValue::from_static("Bar")) + ); assert_eq!(headers.get("foo1"), Some(&HeaderValue::from_static("baz1"))); - assert_eq!(headers.get_all("Foo1").into_iter().collect::>(), vec![&HeaderValue::from_static("baz2"), &HeaderValue::from_static("baz3")]); + assert_eq!( + headers.get_all("Foo1").into_iter().collect::>(), + vec![ + &HeaderValue::from_static("baz2"), + &HeaderValue::from_static("baz3") + ] + ); headers.remove("Foo1"); assert_eq!(headers.get("foo1"), Some(&HeaderValue::from_static("baz1"))); assert_eq!(headers.get("Foo1"), None); -} \ No newline at end of file +} From 86b6d7aa8e0e4f284e7ecfa3ebb71c46bc164c66 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 8 May 2025 16:58:18 +0800 Subject: [PATCH 02/20] feat: add fasthttp feature --- Cargo.toml | 2 +- src/ext/fasthttp/consts.rs | 4 +- src/ext/fasthttp/header_name.rs | 95 ++++++------ src/header/map.rs | 264 ++++++++++++++++++++++++++++---- src/header/name.rs | 46 +++++- tests/header_map.rs | 2 + 6 files changed, 331 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 170576fc..09e0fd3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ exclude = [ default = ["std"] std = [] double-write = ["default"] -fasthttp = ["default"] # 对齐老网关 fasthttp 行为 +fasthttp = ["double-write"] # 对齐老网关 fasthttp 行为 [dependencies] bytes = "1" diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index b1620078..0eab8caf 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -4,6 +4,7 @@ use crate::header::{ LAST_MODIFIED, LOCATION, ORIGIN, RANGE, REFERER, SERVER, SET_COOKIE, TRANSFER_ENCODING, USER_AGENT, }; +use crate::HeaderName; use lazy_static::lazy_static; use trie_rs::{Trie, TrieBuilder}; @@ -77,7 +78,8 @@ impl StandardHeaders { StandardHeaders(trie) } - pub fn is_std_header(&self, header_name: &str) -> bool { + pub fn is_std_header(&self, header_name: &HeaderName) -> bool + { self.0.exact_match(header_name) } } diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs index 772720ea..57e0ddb1 100644 --- a/src/ext/fasthttp/header_name.rs +++ b/src/ext/fasthttp/header_name.rs @@ -1,3 +1,4 @@ +use crate::ext::fasthttp::consts::STANDARD_HEADERS; use crate::HeaderName; const TO_LOWER: u8 = b'a' - b'A'; @@ -28,7 +29,23 @@ lazy_static::lazy_static! { }; } -pub(crate) unsafe fn normalize_header_key2(header_name: &K) -> impl Into +/// 只normalize标准header,其他自定义header保持原样透传 +pub(crate) fn normalize_header_key( + header_name: &HeaderName, + disable_normalizing: bool, +) -> HeaderName { + if disable_normalizing { + return header_name.clone(); + } + + if !STANDARD_HEADERS.is_std_header(&(header_name.clone())) { + return header_name.clone(); + } + + normalize_header_key2(&header_name).into() +} + +pub(crate) fn normalize_header_key2(header_name: &K) -> impl Into where K: Into + Clone, { @@ -40,47 +57,35 @@ where return header_name; } - let header_name_bytes = header_name_str.as_bytes_mut(); - header_name_bytes[0] = TO_UPPER_TABLE[header_name_bytes[0] as usize]; - let mut i = 1; - while i < n { - let p = &mut header_name_bytes[i]; - if *p == b'-' { - i += 1; - if i < n { - header_name_bytes[i] = TO_UPPER_TABLE[header_name_bytes[i] as usize]; + unsafe { + let header_name_bytes = header_name_str.as_bytes_mut(); + header_name_bytes[0] = TO_UPPER_TABLE[header_name_bytes[0] as usize]; + let mut i = 1; + while i < n { + let p = &mut header_name_bytes[i]; + if *p == b'-' { i += 1; + if i < n { + header_name_bytes[i] = TO_UPPER_TABLE[header_name_bytes[i] as usize]; + i += 1; + } + continue; } - continue; + *p = TO_LOWER_TABLE[*p as usize]; + i += 1; } - *p = TO_LOWER_TABLE[*p as usize]; - i += 1; - } - HeaderName::from_bytes(header_name_bytes).unwrap() + HeaderName::from_bytes(header_name_bytes).unwrap() + } } #[allow(dead_code)] #[cfg(test)] mod tests { - use crate::ext::fasthttp::consts::STANDARD_HEADERS; - use crate::ext::fasthttp::header_name::normalize_header_key2; + use crate::ext::fasthttp::header_name::{normalize_header_key, normalize_header_key2}; use crate::HeaderName; use std::str::FromStr; - fn normalize_header_key(header_name: &mut HeaderName, disable_normalizing: bool) -> HeaderName { - if disable_normalizing { - return header_name.clone(); - } - - // 只normalize标准header,其他自定义header保持原样透传 - if !STANDARD_HEADERS.is_std_header(header_name.as_str()) { - return header_name.clone(); - } - - unsafe { normalize_header_key2(header_name).into() } - } - #[test] fn test_normalize_header_key() { let mut header_name = HeaderName::from_str("content-type").unwrap(); @@ -106,28 +111,22 @@ mod tests { #[test] fn test_normalize_header_key2() { let mut header_name = HeaderName::from_str("content-type").unwrap(); - unsafe { - assert_eq!( - normalize_header_key2(&mut header_name).into().as_raw_str(), - "Content-Type" - ); - } + assert_eq!( + normalize_header_key2(&mut header_name).into().as_raw_str(), + "Content-Type" + ); let mut header_name = HeaderName::from_str("Content-Type").unwrap(); - unsafe { - assert_eq!( - normalize_header_key2(&mut header_name).into().as_raw_str(), - "Content-Type" - ); - } + assert_eq!( + normalize_header_key2(&mut header_name).into().as_raw_str(), + "Content-Type" + ); // 非标准header,不做处理 let mut header_name = HeaderName::from_str("x-tt-agw").unwrap(); - unsafe { - assert_eq!( - normalize_header_key2(&mut header_name).into().as_raw_str(), - "X-Tt-Agw" - ); - } + assert_eq!( + normalize_header_key2(&mut header_name).into().as_raw_str(), + "X-Tt-Agw" + ); } } diff --git a/src/header/map.rs b/src/header/map.rs index 7e8f2ee5..16296f91 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -3,7 +3,11 @@ pub use self::into_header_name::IntoHeaderName; use super::name::{HdrName, HeaderName, InvalidHeaderName}; use super::HeaderValue; #[cfg(feature = "fasthttp")] -use crate::ext::fasthttp::header_name::normalize_header_key2; +use super::{ + CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, HOST, TRANSFER_ENCODING, USER_AGENT, +}; +#[cfg(feature = "fasthttp")] +use crate::ext::fasthttp::header_name::{normalize_header_key, normalize_header_key2}; use crate::Error; use std::collections::hash_map::RandomState; use std::collections::HashMap; @@ -50,15 +54,7 @@ pub struct HeaderMap { danger: Danger, #[cfg(feature = "fasthttp")] - keys: HashMap, // normalized_key -> original_key -} - -#[cfg(feature = "fasthttp")] -pub fn normalize_key(key: &K) -> impl Into -where - K: Into + Clone, -{ - unsafe { normalize_header_key2(key) } + mapped_keys: HashMap>, // normalized_key -> original_keys } // # Implementation notes @@ -461,6 +457,38 @@ impl HeaderMap { } impl HeaderMap { + #[cfg(feature = "fasthttp")] + fn append_key(&mut self, original_key: HeaderName) { + let normalized_key = normalize_header_key2(&original_key).into(); + match self.mapped_keys.get_mut(&normalized_key) { + Some(original_keys) => original_keys.push(original_key), + None => { + let mut original_keys = Vec::new(); + original_keys.push(original_key.clone()); + self.mapped_keys.insert(normalized_key, original_keys); + } + } + } + + #[cfg(feature = "fasthttp")] + fn clean_insensitive_keys(&mut self, original_key: &HeaderName) { + let normalized_key = normalize_header_key2(&original_key).into(); + + let mut keys = Vec::new(); + + if let Some(original_keys) = self.mapped_keys.get(&normalized_key) { + for key in original_keys { + keys.push(key.clone()); + } + } + + for key in keys { + self.remove(key); + } + + self.mapped_keys.remove(&normalized_key); + } + /// Create an empty `HeaderMap` with the specified capacity. /// /// The returned map will allocate internal storage in order to hold about @@ -518,7 +546,7 @@ impl HeaderMap { extra_values: Vec::new(), danger: Danger::Green, #[cfg(feature = "fasthttp")] - keys: HashMap::new(), + mapped_keys: HashMap::new(), }) } else { let raw_cap = match to_raw_capacity(capacity).checked_next_power_of_two() { @@ -537,7 +565,7 @@ impl HeaderMap { extra_values: Vec::new(), danger: Danger::Green, #[cfg(feature = "fasthttp")] - keys: HashMap::new(), + mapped_keys: HashMap::new(), }) } } @@ -636,7 +664,7 @@ impl HeaderMap { self.extra_values.clear(); self.danger = Danger::Green; #[cfg(feature = "fasthttp")] - self.keys.clear(); + self.mapped_keys.clear(); for e in self.indices.iter_mut() { *e = Pos::none(); @@ -1181,6 +1209,38 @@ impl HeaderMap { // Ensure that there is space in the map self.try_reserve_one()?; + #[cfg(feature = "fasthttp")] + { + Ok(insert_phase_one!( + self, + key, + probe, + pos, + hash, + danger, + Entry::Vacant(VacantEntry { + map: self, + hash, + key: key.into(), + probe, + danger, + }), + Entry::Occupied(OccupiedEntry { + map: self, + index: pos, + probe, + }), + Entry::Vacant(VacantEntry { + map: self, + hash, + key: key.into(), + probe, + danger, + }) + )) + } + + #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, key, @@ -1286,17 +1346,6 @@ impl HeaderMap { key.try_insert(self, val) } - #[inline] - #[cfg(feature = "fasthttp")] - fn update_key_map(&mut self, key: &HeaderName) { - let normalized_key = normalize_key(key).into(); - if let Some(original_key) = self.keys.get(&normalized_key) { - self.remove(original_key.clone()); - } - - self.keys.insert(normalized_key, key.clone()); - } - #[inline] fn try_insert2(&mut self, key: K, value: T) -> Result, MaxSizeReached> where @@ -1305,6 +1354,34 @@ impl HeaderMap { { self.try_reserve_one()?; + #[cfg(feature = "fasthttp")] + { + Ok(insert_phase_one!( + self, + key, + probe, + pos, + hash, + danger, + // Vacant + { + let _ = danger; // Make lint happy + self.try_insert_entry(hash, key.into(), value)?; + let index = self.entries.len() - 1; + self.indices[probe] = Pos::new(index, hash); + None + }, + // Occupied + Some(self.insert_occupied(pos, value)), + // Robinhood + { + self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + None + } + )) + } + + #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, key, @@ -1326,7 +1403,8 @@ impl HeaderMap { { self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; None - } + }, + true )) } @@ -1448,6 +1526,38 @@ impl HeaderMap { { self.try_reserve_one()?; + #[cfg(feature = "fasthttp")] + { + Ok(insert_phase_one!( + self, + key, + probe, + pos, + hash, + danger, + // Vacant + { + let _ = danger; + let index = self.entries.len(); + self.try_insert_entry_for_append(hash, key.into(), value)?; + self.indices[probe] = Pos::new(index, hash); + false + }, + // Occupied + { + Some(self.insert_occupied(pos, value)); + true + }, + // Robinhood + { + self.try_insert_phase_two_for_append(key.into(), value, hash, probe, danger)?; + + false + } + )) + } + + #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, key, @@ -1473,7 +1583,8 @@ impl HeaderMap { self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; false - } + }, + None )) } @@ -1490,8 +1601,16 @@ impl HeaderMap { self.find2(key).or({ #[cfg(feature = "fasthttp")] { - match self.keys.get(&normalize_key(key).into()) { - Some(original_key) => self.find2::(original_key), + match self.mapped_keys.get(&normalize_header_key2(key).into()) { + Some(original_keys) => { + // 取第一个 key + match original_keys.first() { + Some(first_original_key) => { + self.find2::(first_original_key) + } + None => None, + } + } None => None, } } @@ -1538,12 +1657,60 @@ impl HeaderMap { probe: usize, danger: bool, ) -> Result { + // Push the value and get the index + let index = self.entries.len(); + + #[cfg(feature = "fasthttp")] + self.try_insert_entry(hash, key.clone(), value)?; + #[cfg(not(feature = "fasthttp"))] + self.try_insert_entry(hash, key, value)?; + #[cfg(feature = "fasthttp")] - self.update_key_map(&key); + { + self.clean_insensitive_keys(&key); + self.append_key(key) + } + + let num_displaced = do_insert_phase_two(&mut self.indices, probe, Pos::new(index, hash)); + + if danger || num_displaced >= DISPLACEMENT_THRESHOLD { + // Increase danger level + self.danger.set_yellow(); + } + + Ok(index) + } + + #[inline] + #[cfg(feature = "fasthttp")] + fn try_insert_phase_two_for_append( + &mut self, + key: HeaderName, + value: T, + hash: HashValue, + probe: usize, + danger: bool, + ) -> Result { // Push the value and get the index let index = self.entries.len(); + + #[cfg(feature = "fasthttp")] + self.try_insert_entry(hash, key.clone(), value)?; + #[cfg(not(feature = "fasthttp"))] self.try_insert_entry(hash, key, value)?; + let normalized_key = normalize_header_key(&(key.clone()), false); + match normalized_key { + HOST | CONTENT_TYPE | USER_AGENT | COOKIE | CONTENT_LENGTH | CONNECTION + | TRANSFER_ENCODING => { + // 清理掉所有的 insensitive key + self.clean_insensitive_keys(&key); + } + _ => {} + } + + self.append_key(key); + let num_displaced = do_insert_phase_two(&mut self.indices, probe, Pos::new(index, hash)); if danger || num_displaced >= DISPLACEMENT_THRESHOLD { @@ -1679,13 +1846,50 @@ impl HeaderMap { key: HeaderName, value: T, ) -> Result<(), MaxSizeReached> { + if self.entries.len() >= MAX_SIZE { + return Err(MaxSizeReached::new()); + } + #[cfg(feature = "fasthttp")] - self.update_key_map(&key); + { + self.clean_insensitive_keys(&key); + self.append_key(key.clone()); + } + + self.entries.push(Bucket { + hash, + key, + value, + links: None, + }); + + Ok(()) + } + #[inline] + #[cfg(feature = "fasthttp")] + fn try_insert_entry_for_append( + &mut self, + hash: HashValue, + key: HeaderName, + value: T, + ) -> Result<(), MaxSizeReached> { if self.entries.len() >= MAX_SIZE { return Err(MaxSizeReached::new()); } + let normalized_key = normalize_header_key(&(key.clone()), false).into(); + match normalized_key { + HOST | CONTENT_TYPE | USER_AGENT | COOKIE | CONTENT_LENGTH | CONNECTION + | TRANSFER_ENCODING => { + // 清理掉所有的 insensitive key + self.clean_insensitive_keys(&key); + } + _ => {} + } + + self.append_key(key.clone()); + self.entries.push(Bucket { hash, key, diff --git a/src/header/name.rs b/src/header/name.rs index ccfc99a2..02be13fc 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -1830,9 +1830,9 @@ unsafe fn slice_assume_init(slice: &[MaybeUninit]) -> &[T] { #[cfg(test)] mod tests { + use crate::{HeaderMap}; use self::StandardHeader::Vary; use super::*; - use crate::HeaderMap; #[test] fn test_bounds() { @@ -2136,7 +2136,7 @@ mod tests { #[cfg(feature = "fasthttp")] mod fasthttp_tests { use crate::header::HeaderMap; - use crate::{HeaderName, HeaderValue}; + use crate::{HeaderName, HeaderValue, Request}; #[test] fn test_insert() { @@ -2178,4 +2178,46 @@ mod fasthttp_tests { ("x-tt-agw-key2", "3") ); } + + #[test] + fn test_append() { + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("x-tt-agw-key1".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + header_map.append( + HeaderName::from_bytes("X-Tt-Agw-Key1".as_bytes()).unwrap(), + HeaderValue::from_static("2"), + ); + + assert_eq!(header_map.len(), 2); + + assert_eq!(header_map.get("x-tt-agw-key1").unwrap(), "1"); + assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "2"); + + let mut iter = header_map.iter(); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("x-tt-agw-key1", "1") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("X-Tt-Agw-Key1", "2") + ); + } + + #[test] + fn test_header_insert() { + let request = Request::builder() + .uri("/") + .header("X-Tt-Log-Id", "logid") + .body(()) + .unwrap(); + let logid = request.headers().get("x-tt-log-id").unwrap().to_str().unwrap(); + assert_eq!(logid, "logid".to_string()); + } + } diff --git a/tests/header_map.rs b/tests/header_map.rs index 64b8af9f..e9b35d4f 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -687,6 +687,8 @@ fn feature_double_write() { HeaderValue::from_static("Bar"), ); + assert_eq!(headers.len(), 2); + headers.append( HeaderName::from_bytes("foo1".as_bytes()).unwrap(), HeaderValue::from_static("baz1"), From 112fe0e429724fc945635e64ecc3682f5a967e55 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 9 May 2025 10:45:42 +0800 Subject: [PATCH 03/20] feat: add fasthttp feature --- src/header/map.rs | 44 +++++++++----------------------------------- src/header/name.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/header/map.rs b/src/header/map.rs index 16296f91..fffa84a9 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -1403,8 +1403,7 @@ impl HeaderMap { { self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; None - }, - true + } )) } @@ -1583,8 +1582,7 @@ impl HeaderMap { self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; false - }, - None + } )) } @@ -1660,17 +1658,8 @@ impl HeaderMap { // Push the value and get the index let index = self.entries.len(); - #[cfg(feature = "fasthttp")] - self.try_insert_entry(hash, key.clone(), value)?; - #[cfg(not(feature = "fasthttp"))] self.try_insert_entry(hash, key, value)?; - #[cfg(feature = "fasthttp")] - { - self.clean_insensitive_keys(&key); - self.append_key(key) - } - let num_displaced = do_insert_phase_two(&mut self.indices, probe, Pos::new(index, hash)); if danger || num_displaced >= DISPLACEMENT_THRESHOLD { @@ -1694,22 +1683,7 @@ impl HeaderMap { // Push the value and get the index let index = self.entries.len(); - #[cfg(feature = "fasthttp")] - self.try_insert_entry(hash, key.clone(), value)?; - #[cfg(not(feature = "fasthttp"))] - self.try_insert_entry(hash, key, value)?; - - let normalized_key = normalize_header_key(&(key.clone()), false); - match normalized_key { - HOST | CONTENT_TYPE | USER_AGENT | COOKIE | CONTENT_LENGTH | CONNECTION - | TRANSFER_ENCODING => { - // 清理掉所有的 insensitive key - self.clean_insensitive_keys(&key); - } - _ => {} - } - - self.append_key(key); + self.try_insert_entry_for_append(hash, key.clone(), value)?; let num_displaced = do_insert_phase_two(&mut self.indices, probe, Pos::new(index, hash)); @@ -2571,7 +2545,7 @@ impl<'a, T> IterMut<'a, T> { self.cursor = Some(Cursor::Head); } - let entry = unsafe { &mut (*self.map).entries[self.entry] }; + let entry = unsafe { &mut (&mut (*self.map).entries)[self.entry] }; match self.cursor.unwrap() { Head => { @@ -2579,7 +2553,7 @@ impl<'a, T> IterMut<'a, T> { Some((&entry.key, &mut entry.value as *mut _)) } Values(idx) => { - let extra = unsafe { &mut (*self.map).extra_values[idx] }; + let extra = unsafe { &mut (&mut (*self.map).extra_values)[idx] }; match extra.next { Link::Entry(_) => self.cursor = None, @@ -3219,7 +3193,7 @@ impl<'a, T: 'a> Iterator for ValueIterMut<'a, T> { fn next(&mut self) -> Option { use self::Cursor::*; - let entry = unsafe { &mut (*self.map).entries[self.index] }; + let entry = unsafe { &mut (&mut (*self.map).entries)[self.index] }; match self.front { Some(Head) => { @@ -3239,7 +3213,7 @@ impl<'a, T: 'a> Iterator for ValueIterMut<'a, T> { Some(&mut entry.value) } Some(Values(idx)) => { - let extra = unsafe { &mut (*self.map).extra_values[idx] }; + let extra = unsafe { &mut (&mut (*self.map).extra_values)[idx] }; if self.front == self.back { self.front = None; @@ -3262,7 +3236,7 @@ impl<'a, T: 'a> DoubleEndedIterator for ValueIterMut<'a, T> { fn next_back(&mut self) -> Option { use self::Cursor::*; - let entry = unsafe { &mut (*self.map).entries[self.index] }; + let entry = unsafe { &mut (&mut (*self.map).entries)[self.index] }; match self.back { Some(Head) => { @@ -3271,7 +3245,7 @@ impl<'a, T: 'a> DoubleEndedIterator for ValueIterMut<'a, T> { Some(&mut entry.value) } Some(Values(idx)) => { - let extra = unsafe { &mut (*self.map).extra_values[idx] }; + let extra = unsafe { &mut (&mut (*self.map).extra_values)[idx] }; if self.front == self.back { self.front = None; diff --git a/src/header/name.rs b/src/header/name.rs index 02be13fc..3bf04823 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -2209,6 +2209,36 @@ mod fasthttp_tests { ); } + #[test] + fn test_append2() { + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("rpc-persist-lane-p-aid".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + header_map.append( + HeaderName::from_bytes("rpc-persist-Lane-P-Aid".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + + assert_eq!(header_map.len(), 2); + + assert_eq!(header_map.get("rpc-persist-lane-p-aid").unwrap(), "1"); + assert_eq!(header_map.get("rpc-persist-Lane-P-Aid").unwrap(), "1"); + + let mut iter = header_map.iter(); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("rpc-persist-lane-p-aid", "1") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("rpc-persist-Lane-P-Aid", "1") + ); + } + #[test] fn test_header_insert() { let request = Request::builder() From 949c3a8f1c2ea2e29df4921166afae63ab07aa79 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 15 May 2025 16:35:42 +0800 Subject: [PATCH 04/20] feat: add fasthttp feature --- Cargo.toml | 14 ++--- src/ext/fasthttp/consts.rs | 3 +- src/header/name.rs | 10 ++- src/request.rs | 1 + src/response.rs | 1 + src/uri/mod.rs | 1 + tests/header_map.rs | 124 +++++++++++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 09e0fd3a..a0fda57f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,9 @@ documentation = "https://docs.rs/http" repository = "https://github.com/hyperium/http" license = "MIT OR Apache-2.0" authors = [ - "Alex Crichton ", - "Carl Lerche ", - "Sean McArthur ", + "Alex Crichton ", + "Carl Lerche ", + "Sean McArthur ", ] description = """ A set of types for representing HTTP requests and responses. @@ -25,18 +25,18 @@ rust-version = "1.49.0" [workspace] members = [ - ".", + ".", ] exclude = [ - "fuzz", - "benches" + "fuzz", + "benches" ] [features] default = ["std"] std = [] double-write = ["default"] -fasthttp = ["double-write"] # 对齐老网关 fasthttp 行为 +fasthttp = ["double-write"] # compatible with fasthttp go version [dependencies] bytes = "1" diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index 0eab8caf..5ae727d8 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -78,8 +78,7 @@ impl StandardHeaders { StandardHeaders(trie) } - pub fn is_std_header(&self, header_name: &HeaderName) -> bool - { + pub fn is_std_header(&self, header_name: &HeaderName) -> bool { self.0.exact_match(header_name) } } diff --git a/src/header/name.rs b/src/header/name.rs index 3bf04823..fa274201 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -1830,9 +1830,9 @@ unsafe fn slice_assume_init(slice: &[MaybeUninit]) -> &[T] { #[cfg(test)] mod tests { - use crate::{HeaderMap}; use self::StandardHeader::Vary; use super::*; + use crate::HeaderMap; #[test] fn test_bounds() { @@ -2246,8 +2246,12 @@ mod fasthttp_tests { .header("X-Tt-Log-Id", "logid") .body(()) .unwrap(); - let logid = request.headers().get("x-tt-log-id").unwrap().to_str().unwrap(); + let logid = request + .headers() + .get("x-tt-log-id") + .unwrap() + .to_str() + .unwrap(); assert_eq!(logid, "logid".to_string()); } - } diff --git a/src/request.rs b/src/request.rs index 324b676c..9f10479f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -164,6 +164,7 @@ pub struct Request { /// /// The HTTP request head consists of a method, uri, version, and a set of /// header fields. +#[allow(warnings)] #[derive(Clone)] pub struct Parts { /// The request's method diff --git a/src/response.rs b/src/response.rs index ab9e49bc..504eaeb4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -186,6 +186,7 @@ pub struct Response { /// /// The HTTP response head consists of a status, version, and a set of /// header fields. +#[allow(warnings)] #[derive(Clone)] pub struct Parts { /// The response's status diff --git a/src/uri/mod.rs b/src/uri/mod.rs index ff9908e9..c6b3f2d5 100644 --- a/src/uri/mod.rs +++ b/src/uri/mod.rs @@ -102,6 +102,7 @@ pub struct Uri { /// The various parts of a URI. /// /// This struct is used to provide to and retrieve from a URI. +#[allow(warnings)] #[derive(Debug, Default)] pub struct Parts { /// The scheme component of a URI diff --git a/tests/header_map.rs b/tests/header_map.rs index e9b35d4f..b1e4ae19 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -720,3 +720,127 @@ fn feature_double_write() { assert_eq!(headers.get("foo1"), Some(&HeaderValue::from_static("baz1"))); assert_eq!(headers.get("Foo1"), None); } + +#[cfg(test)] +#[cfg(feature = "fasthttp")] +mod fasthttp_tests { + use crate::header::HeaderMap; + use crate::{HeaderName, HeaderValue, Request}; + + #[test] + fn test_insert() { + let mut header_map = HeaderMap::new(); + + header_map.insert( + HeaderName::from_bytes("x-tt-agw-key1".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + assert_eq!(header_map.get("x-tt-agw-key1").unwrap(), "1"); + assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "1"); + assert_eq!(header_map.len(), 1); + + header_map.insert( + HeaderName::from_bytes("X-Tt-agw-key1".as_bytes()).unwrap(), + HeaderValue::from_static("2"), + ); + assert_eq!(header_map.get("X-Tt-agw-key1").unwrap(), "2"); + assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "2"); + assert_eq!(header_map.len(), 1); + + header_map.insert( + HeaderName::from_bytes("x-tt-agw-key2".as_bytes()).unwrap(), + HeaderValue::from_static("3"), + ); + assert_eq!(header_map.len(), 2); + + let mut iter = header_map.iter(); + + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("X-Tt-agw-key1", "2") + ); + + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("x-tt-agw-key2", "3") + ); + } + + #[test] + fn test_append() { + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("x-tt-agw-key1".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + header_map.append( + HeaderName::from_bytes("X-Tt-Agw-Key1".as_bytes()).unwrap(), + HeaderValue::from_static("2"), + ); + + assert_eq!(header_map.len(), 2); + + assert_eq!(header_map.get("x-tt-agw-key1").unwrap(), "1"); + assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "2"); + + let mut iter = header_map.iter(); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("x-tt-agw-key1", "1") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("X-Tt-Agw-Key1", "2") + ); + } + + #[test] + fn test_append2() { + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("rpc-persist-lane-p-aid".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + header_map.append( + HeaderName::from_bytes("rpc-persist-Lane-P-Aid".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + + assert_eq!(header_map.len(), 2); + + assert_eq!(header_map.get("rpc-persist-lane-p-aid").unwrap(), "1"); + assert_eq!(header_map.get("rpc-persist-Lane-P-Aid").unwrap(), "1"); + + let mut iter = header_map.iter(); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("rpc-persist-lane-p-aid", "1") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("rpc-persist-Lane-P-Aid", "1") + ); + } + + #[test] + fn test_header_insert() { + let request = Request::builder() + .uri("/") + .header("X-Tt-Log-Id", "logid") + .body(()) + .unwrap(); + let logid = request + .headers() + .get("x-tt-log-id") + .unwrap() + .to_str() + .unwrap(); + assert_eq!(logid, "logid".to_string()); + } +} From ffc72ab530042c8ef7719f6e1bcdb067ac87bbd6 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 15 May 2025 18:54:18 +0800 Subject: [PATCH 05/20] feat: add fasthttp feature --- src/header/map.rs | 16 +++++++++------- tests/header_map.rs | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/header/map.rs b/src/header/map.rs index fffa84a9..17288f4e 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -457,8 +457,10 @@ impl HeaderMap { } impl HeaderMap { + /// + /// #[cfg(feature = "fasthttp")] - fn append_key(&mut self, original_key: HeaderName) { + fn append_to_mapped_keys(&mut self, original_key: HeaderName) { let normalized_key = normalize_header_key2(&original_key).into(); match self.mapped_keys.get_mut(&normalized_key) { Some(original_keys) => original_keys.push(original_key), @@ -471,7 +473,7 @@ impl HeaderMap { } #[cfg(feature = "fasthttp")] - fn clean_insensitive_keys(&mut self, original_key: &HeaderName) { + fn remove_key_insensitively(&mut self, original_key: &HeaderName) { let normalized_key = normalize_header_key2(&original_key).into(); let mut keys = Vec::new(); @@ -1392,8 +1394,8 @@ impl HeaderMap { // Vacant { let _ = danger; // Make lint happy + let index = self.entries.len(); self.try_insert_entry(hash, key.into(), value)?; - let index = self.entries.len() - 1; self.indices[probe] = Pos::new(index, hash); None }, @@ -1826,8 +1828,8 @@ impl HeaderMap { #[cfg(feature = "fasthttp")] { - self.clean_insensitive_keys(&key); - self.append_key(key.clone()); + self.remove_key_insensitively(&key); + self.append_to_mapped_keys(key.clone()); } self.entries.push(Bucket { @@ -1857,12 +1859,12 @@ impl HeaderMap { HOST | CONTENT_TYPE | USER_AGENT | COOKIE | CONTENT_LENGTH | CONNECTION | TRANSFER_ENCODING => { // 清理掉所有的 insensitive key - self.clean_insensitive_keys(&key); + self.remove_key_insensitively(&key); } _ => {} } - self.append_key(key.clone()); + self.append_to_mapped_keys(key.clone()); self.entries.push(Bucket { hash, diff --git a/tests/header_map.rs b/tests/header_map.rs index b1e4ae19..eac08781 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -828,6 +828,27 @@ mod fasthttp_tests { ); } + #[test] + fn test_get() { + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + } + + #[test] + fn test_get_with_request() { + let mut request = Request::builder() + .uri("/") + .header("Content-Type", "application/json") + .body(()) + .unwrap(); + + request.headers().get("content-type").unwrap(); + } + #[test] fn test_header_insert() { let request = Request::builder() From f76f435254ae802416788b3016abdb82463e6433 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Thu, 15 May 2025 19:27:25 +0800 Subject: [PATCH 06/20] feat: add fasthttp feature --- src/header/map.rs | 34 +--------------------------------- tests/header_map.rs | 2 +- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/src/header/map.rs b/src/header/map.rs index 17288f4e..af899c65 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -1211,38 +1211,6 @@ impl HeaderMap { // Ensure that there is space in the map self.try_reserve_one()?; - #[cfg(feature = "fasthttp")] - { - Ok(insert_phase_one!( - self, - key, - probe, - pos, - hash, - danger, - Entry::Vacant(VacantEntry { - map: self, - hash, - key: key.into(), - probe, - danger, - }), - Entry::Occupied(OccupiedEntry { - map: self, - index: pos, - probe, - }), - Entry::Vacant(VacantEntry { - map: self, - hash, - key: key.into(), - probe, - danger, - }) - )) - } - - #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, key, @@ -1546,7 +1514,7 @@ impl HeaderMap { }, // Occupied { - Some(self.insert_occupied(pos, value)); + append_value(pos, &mut self.entries[pos], &mut self.extra_values, value); true }, // Robinhood diff --git a/tests/header_map.rs b/tests/header_map.rs index eac08781..5195b7a0 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -840,7 +840,7 @@ mod fasthttp_tests { #[test] fn test_get_with_request() { - let mut request = Request::builder() + let request = Request::builder() .uri("/") .header("Content-Type", "application/json") .body(()) From 4423ffd3fa2012bb88bdfc6f008c2ae7a8eb2298 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 16 May 2025 17:40:02 +0800 Subject: [PATCH 07/20] feat: add fasthttp feature --- src/ext/fasthttp/header_name.rs | 20 +-- src/header/map.rs | 111 ++++++++++++++++- src/header/name.rs | 6 +- tests/double_write.rs | 52 ++++++++ tests/fasthttp.rs | 208 ++++++++++++++++++++++++++++++++ tests/header_map.rs | 193 ----------------------------- 6 files changed, 380 insertions(+), 210 deletions(-) create mode 100644 tests/double_write.rs create mode 100644 tests/fasthttp.rs diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs index 57e0ddb1..8a39f298 100644 --- a/src/ext/fasthttp/header_name.rs +++ b/src/ext/fasthttp/header_name.rs @@ -30,7 +30,7 @@ lazy_static::lazy_static! { } /// 只normalize标准header,其他自定义header保持原样透传 -pub(crate) fn normalize_header_key( +pub(crate) fn normalize_header_key_for_std_header( header_name: &HeaderName, disable_normalizing: bool, ) -> HeaderName { @@ -42,10 +42,10 @@ pub(crate) fn normalize_header_key( return header_name.clone(); } - normalize_header_key2(&header_name).into() + normalize_header_key(&header_name).into() } -pub(crate) fn normalize_header_key2(header_name: &K) -> impl Into +pub(crate) fn normalize_header_key(header_name: &K) -> impl Into where K: Into + Clone, { @@ -82,7 +82,7 @@ where #[allow(dead_code)] #[cfg(test)] mod tests { - use crate::ext::fasthttp::header_name::{normalize_header_key, normalize_header_key2}; + use crate::ext::fasthttp::header_name::{normalize_header_key_for_std_header, normalize_header_key}; use crate::HeaderName; use std::str::FromStr; @@ -90,20 +90,20 @@ mod tests { fn test_normalize_header_key() { let mut header_name = HeaderName::from_str("content-type").unwrap(); assert_eq!( - normalize_header_key(&mut header_name, false).as_raw_str(), + normalize_header_key_for_std_header(&mut header_name, false).as_raw_str(), "Content-Type" ); let mut header_name = HeaderName::from_str("Content-Type").unwrap(); assert_eq!( - normalize_header_key(&mut header_name, false).as_raw_str(), + normalize_header_key_for_std_header(&mut header_name, false).as_raw_str(), "Content-Type" ); // 非标准header,不做处理 let mut header_name = HeaderName::from_str("x-tt-agw").unwrap(); assert_eq!( - normalize_header_key(&mut header_name, false).as_raw_str(), + normalize_header_key_for_std_header(&mut header_name, false).as_raw_str(), "x-tt-agw" ); } @@ -112,20 +112,20 @@ mod tests { fn test_normalize_header_key2() { let mut header_name = HeaderName::from_str("content-type").unwrap(); assert_eq!( - normalize_header_key2(&mut header_name).into().as_raw_str(), + normalize_header_key(&mut header_name).into().as_raw_str(), "Content-Type" ); let mut header_name = HeaderName::from_str("Content-Type").unwrap(); assert_eq!( - normalize_header_key2(&mut header_name).into().as_raw_str(), + normalize_header_key(&mut header_name).into().as_raw_str(), "Content-Type" ); // 非标准header,不做处理 let mut header_name = HeaderName::from_str("x-tt-agw").unwrap(); assert_eq!( - normalize_header_key2(&mut header_name).into().as_raw_str(), + normalize_header_key(&mut header_name).into().as_raw_str(), "X-Tt-Agw" ); } diff --git a/src/header/map.rs b/src/header/map.rs index af899c65..973e6f61 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -7,7 +7,10 @@ use super::{ CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, HOST, TRANSFER_ENCODING, USER_AGENT, }; #[cfg(feature = "fasthttp")] -use crate::ext::fasthttp::header_name::{normalize_header_key, normalize_header_key2}; +use crate::ext::fasthttp::header_name::{ + normalize_header_key, normalize_header_key_for_std_header, +}; +use crate::header::map::as_header_name::Sealed; use crate::Error; use std::collections::hash_map::RandomState; use std::collections::HashMap; @@ -15,6 +18,8 @@ use std::convert::TryFrom; use std::hash::{BuildHasher, Hash, Hasher}; use std::iter::{FromIterator, FusedIterator}; use std::marker::PhantomData; +#[cfg(feature = "fasthttp")] +use std::str::FromStr; use std::{fmt, mem, ops, ptr, vec}; /// A set of HTTP headers @@ -461,7 +466,7 @@ impl HeaderMap { /// #[cfg(feature = "fasthttp")] fn append_to_mapped_keys(&mut self, original_key: HeaderName) { - let normalized_key = normalize_header_key2(&original_key).into(); + let normalized_key = normalize_header_key(&original_key).into(); match self.mapped_keys.get_mut(&normalized_key) { Some(original_keys) => original_keys.push(original_key), None => { @@ -474,7 +479,7 @@ impl HeaderMap { #[cfg(feature = "fasthttp")] fn remove_key_insensitively(&mut self, original_key: &HeaderName) { - let normalized_key = normalize_header_key2(&original_key).into(); + let normalized_key = normalize_header_key(&original_key).into(); let mut keys = Vec::new(); @@ -1569,9 +1574,9 @@ impl HeaderMap { self.find2(key).or({ #[cfg(feature = "fasthttp")] { - match self.mapped_keys.get(&normalize_header_key2(key).into()) { + match self.mapped_keys.get(&normalize_header_key(key).into()) { Some(original_keys) => { - // 取第一个 key + // get first original key match original_keys.first() { Some(first_original_key) => { self.find2::(first_original_key) @@ -1689,6 +1694,63 @@ impl HeaderMap { where K: AsHeaderName, { + #[cfg(feature = "fasthttp")] + { + let header_name = match HeaderName::from_str(key.as_str()) { + Ok(header_name) => header_name, + Err(_) => return None, + }; + + let normalized_key = normalize_header_key(&header_name).into(); + + let mut i; + let original_key = match self.mapped_keys.get(&normalized_key) { + Some(original_keys) => match original_keys.iter().next_back() { + Some(return_original_key) => { + i = original_keys.len() - 1; + let mut return_original_key = return_original_key; + let _a = return_original_key.clone().as_raw_str(); + let _b = header_name.clone().as_raw_str(); + if return_original_key == header_name { + return_original_key.clone() + } else { + for original_key in original_keys.into_iter() { + i -= 1; + return_original_key = original_key; + if original_key == header_name { + break; + } + } + return_original_key.clone() + } + } + None => return None, + }, + None => { + return None; + } + }; + + match original_key.find(self) { + Some((probe, idx)) => { + if let Some(links) = self.entries[idx].links { + self.remove_all_extra_values(links.next); + } + + let entry = self.remove_found(probe, idx); + + self.mapped_keys + .get_mut(&normalized_key) + .map(|original_keys| { + original_keys.remove(i); + }); + Some(entry.value) + } + None => None, + } + } + + #[cfg(not(feature = "fasthttp"))] match key.find(self) { Some((probe, idx)) => { if let Some(links) = self.entries[idx].links { @@ -1703,6 +1765,43 @@ impl HeaderMap { } } + /// Remove all keys that case-insensitively equal + #[cfg(feature = "fasthttp")] + pub fn remove_all(&mut self, key: K) -> Option> + where + K: AsHeaderName, + { + let header_name = HeaderName::from_str(key.as_str()).ok(); + + let normalized_key = match header_name { + Some(header_name) => normalize_header_key(&header_name).into(), + None => { + return None; + } + }; + + let mut return_headers = vec![]; + + let original_keys = match self.mapped_keys.get(&normalized_key) { + Some(original_keys) => original_keys.clone(), + None => { + return None; + } + }; + + for original_key in original_keys { + if let Some((probe, idx)) = self.find(&original_key) { + if let Some(links) = self.entries[idx].links { + self.remove_all_extra_values(links.next); + } + let entry = self.remove_found(probe, idx); + return_headers.push(entry.value); + } + } + + Some(return_headers) + } + /// Remove an entry from the map. /// /// Warning: To avoid inconsistent state, extra values _must_ be removed @@ -1822,7 +1921,7 @@ impl HeaderMap { return Err(MaxSizeReached::new()); } - let normalized_key = normalize_header_key(&(key.clone()), false).into(); + let normalized_key = normalize_header_key_for_std_header(&(key.clone()), false).into(); match normalized_key { HOST | CONTENT_TYPE | USER_AGENT | COOKIE | CONTENT_LENGTH | CONNECTION | TRANSFER_ENCODING => { diff --git a/src/header/name.rs b/src/header/name.rs index fa274201..23f27096 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -65,7 +65,11 @@ impl StructuralPartialEq for Repr {} impl PartialEq for Repr { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::Standard(l, _), Self::Standard(r, _)) => l == r, + (Self::Standard(la, lb), Self::Standard(ra, rb)) => match (lb, rb) { + (Some(lb), Some(rb)) => lb == rb, + (None, None) => la == ra, + _ => false, + }, (Self::Custom(l, _), Self::Custom(r, _)) => l == r, _ => false, } diff --git a/tests/double_write.rs b/tests/double_write.rs new file mode 100644 index 00000000..cdc53948 --- /dev/null +++ b/tests/double_write.rs @@ -0,0 +1,52 @@ +#[cfg(feature = "double-write")] +#[cfg(not(feature = "fasthttp"))] +mod double_write_tests { + use http::{HeaderMap, HeaderName, HeaderValue}; + + #[test] + fn feature_double_write() { + let mut headers = HeaderMap::new(); + headers.insert( + HeaderName::from_static("foo"), + HeaderValue::from_static("bar"), + ); + + headers.insert( + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("Bar"), + ); + + assert_eq!(headers.len(), 2); + + headers.append( + HeaderName::from_bytes("foo1".as_bytes()).unwrap(), + HeaderValue::from_static("baz1"), + ); + headers.append( + HeaderName::from_bytes("Foo1".as_bytes()).unwrap(), + HeaderValue::from_static("baz2"), + ); + headers.append( + HeaderName::from_bytes("Foo1".as_bytes()).unwrap(), + HeaderValue::from_static("baz3"), + ); + + assert_eq!(headers.get("foo"), Some(&HeaderValue::from_static("bar"))); + assert_eq!(headers.get("Foo"), Some(&HeaderValue::from_static("Bar"))); + assert_eq!( + headers.get(HeaderName::from_bytes("Foo".as_bytes()).unwrap()), + Some(&HeaderValue::from_static("Bar")) + ); + assert_eq!(headers.get("foo1"), Some(&HeaderValue::from_static("baz1"))); + assert_eq!( + headers.get_all("Foo1").into_iter().collect::>(), + vec![ + &HeaderValue::from_static("baz2"), + &HeaderValue::from_static("baz3") + ] + ); + headers.remove("Foo1"); + assert_eq!(headers.get("foo1"), Some(&HeaderValue::from_static("baz1"))); + assert_eq!(headers.get("Foo1"), None); + } +} diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs new file mode 100644 index 00000000..ff7e0561 --- /dev/null +++ b/tests/fasthttp.rs @@ -0,0 +1,208 @@ +#[cfg(feature = "fasthttp")] +mod fasthttp_tests { + use http::{HeaderMap, HeaderName, HeaderValue, Request}; + + #[test] + fn test_insert() { + let mut header_map = HeaderMap::new(); + + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + assert_eq!(header_map.get("Content-Type").unwrap(), "application/json"); + + header_map.insert( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + + header_map.insert( + HeaderName::from_bytes("content-length".as_bytes()).unwrap(), + HeaderValue::from_static("-1"), + ); + assert_eq!(header_map.len(), 2); + + let mut iter = header_map.iter(); + + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("Content-Type", "text/html") + ); + + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("content-length", "-1") + ); + } + + #[test] + fn test_append() { + let mut header_map = HeaderMap::new(); + + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 2); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + + header_map.append( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("text/plain"), + ); + assert_eq!(header_map.len(), 3); + let mut header_value_iter = header_map.get_all("content-type").iter(); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "application/json"); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "text/plain"); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("image/jpeg"), + ); + assert_eq!(header_map.len(), 4); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + let mut header_value_iter = header_map.get_all("Content-Type").iter(); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "text/html"); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "image/jpeg"); + + let mut iter = header_map.iter(); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("content-type", "application/json") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("content-type", "text/plain") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("Content-Type", "text/html") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("Content-Type", "image/jpeg") + ) + } + + #[test] + fn test_remove() { + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + header_map.remove(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + assert_eq!(header_map.len(), 0); + assert_eq!(header_map.get("content-type"), None); + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 2); + header_map.remove(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-type").unwrap(), "text/html"); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 2); + header_map.remove(HeaderName::from_bytes("Content-Type".as_bytes()).unwrap()); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + assert_eq!(header_map.get("Content-Type").unwrap(), "application/json"); + } + + #[test] + fn test_remove_all() { + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + header_map.remove_all(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + assert_eq!(header_map.len(), 0); + assert_eq!(header_map.get("content-type"), None); + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 2); + header_map.remove_all(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + assert_eq!(header_map.len(), 0); + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 2); + header_map.remove_all(HeaderName::from_bytes("Content-Type".as_bytes()).unwrap()); + assert_eq!(header_map.len(), 0); + } + + #[test] + fn test_get_with_request() { + let request = Request::builder() + .uri("/") + .header("Content-Type", "application/json") + .header("Content-Type", "text/html") + .header("content-type", "text/plain") + .body(()) + .unwrap(); + + assert_eq!(request.headers().get("content-type").unwrap(), "text/plain"); + assert_eq!(request.headers().get("Content-Type").unwrap(), "application/json"); + let mut header_value_iter = request.headers().get_all("Content-Type").iter(); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "application/json"); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "text/html"); + } +} diff --git a/tests/header_map.rs b/tests/header_map.rs index 5195b7a0..9a9d7e12 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -672,196 +672,3 @@ fn ensure_miri_sharedreadonly_not_violated() { let _foo = &headers.iter().next(); } - -#[cfg(feature = "double-write")] -#[test] -fn feature_double_write() { - let mut headers = HeaderMap::new(); - headers.insert( - HeaderName::from_static("foo"), - HeaderValue::from_static("bar"), - ); - - headers.insert( - HeaderName::from_bytes("Foo".as_bytes()).unwrap(), - HeaderValue::from_static("Bar"), - ); - - assert_eq!(headers.len(), 2); - - headers.append( - HeaderName::from_bytes("foo1".as_bytes()).unwrap(), - HeaderValue::from_static("baz1"), - ); - headers.append( - HeaderName::from_bytes("Foo1".as_bytes()).unwrap(), - HeaderValue::from_static("baz2"), - ); - headers.append( - HeaderName::from_bytes("Foo1".as_bytes()).unwrap(), - HeaderValue::from_static("baz3"), - ); - - assert_eq!(headers.get("foo"), Some(&HeaderValue::from_static("bar"))); - assert_eq!(headers.get("Foo"), Some(&HeaderValue::from_static("Bar"))); - assert_eq!( - headers.get(HeaderName::from_bytes("Foo".as_bytes()).unwrap()), - Some(&HeaderValue::from_static("Bar")) - ); - assert_eq!(headers.get("foo1"), Some(&HeaderValue::from_static("baz1"))); - assert_eq!( - headers.get_all("Foo1").into_iter().collect::>(), - vec![ - &HeaderValue::from_static("baz2"), - &HeaderValue::from_static("baz3") - ] - ); - headers.remove("Foo1"); - assert_eq!(headers.get("foo1"), Some(&HeaderValue::from_static("baz1"))); - assert_eq!(headers.get("Foo1"), None); -} - -#[cfg(test)] -#[cfg(feature = "fasthttp")] -mod fasthttp_tests { - use crate::header::HeaderMap; - use crate::{HeaderName, HeaderValue, Request}; - - #[test] - fn test_insert() { - let mut header_map = HeaderMap::new(); - - header_map.insert( - HeaderName::from_bytes("x-tt-agw-key1".as_bytes()).unwrap(), - HeaderValue::from_static("1"), - ); - assert_eq!(header_map.get("x-tt-agw-key1").unwrap(), "1"); - assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "1"); - assert_eq!(header_map.len(), 1); - - header_map.insert( - HeaderName::from_bytes("X-Tt-agw-key1".as_bytes()).unwrap(), - HeaderValue::from_static("2"), - ); - assert_eq!(header_map.get("X-Tt-agw-key1").unwrap(), "2"); - assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "2"); - assert_eq!(header_map.len(), 1); - - header_map.insert( - HeaderName::from_bytes("x-tt-agw-key2".as_bytes()).unwrap(), - HeaderValue::from_static("3"), - ); - assert_eq!(header_map.len(), 2); - - let mut iter = header_map.iter(); - - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("X-Tt-agw-key1", "2") - ); - - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("x-tt-agw-key2", "3") - ); - } - - #[test] - fn test_append() { - let mut header_map = HeaderMap::new(); - header_map.insert( - HeaderName::from_bytes("x-tt-agw-key1".as_bytes()).unwrap(), - HeaderValue::from_static("1"), - ); - header_map.append( - HeaderName::from_bytes("X-Tt-Agw-Key1".as_bytes()).unwrap(), - HeaderValue::from_static("2"), - ); - - assert_eq!(header_map.len(), 2); - - assert_eq!(header_map.get("x-tt-agw-key1").unwrap(), "1"); - assert_eq!(header_map.get("X-Tt-Agw-Key1").unwrap(), "2"); - - let mut iter = header_map.iter(); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("x-tt-agw-key1", "1") - ); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("X-Tt-Agw-Key1", "2") - ); - } - - #[test] - fn test_append2() { - let mut header_map = HeaderMap::new(); - header_map.insert( - HeaderName::from_bytes("rpc-persist-lane-p-aid".as_bytes()).unwrap(), - HeaderValue::from_static("1"), - ); - header_map.append( - HeaderName::from_bytes("rpc-persist-Lane-P-Aid".as_bytes()).unwrap(), - HeaderValue::from_static("1"), - ); - - assert_eq!(header_map.len(), 2); - - assert_eq!(header_map.get("rpc-persist-lane-p-aid").unwrap(), "1"); - assert_eq!(header_map.get("rpc-persist-Lane-P-Aid").unwrap(), "1"); - - let mut iter = header_map.iter(); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("rpc-persist-lane-p-aid", "1") - ); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("rpc-persist-Lane-P-Aid", "1") - ); - } - - #[test] - fn test_get() { - let mut header_map = HeaderMap::new(); - header_map.insert( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), - ); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - } - - #[test] - fn test_get_with_request() { - let request = Request::builder() - .uri("/") - .header("Content-Type", "application/json") - .body(()) - .unwrap(); - - request.headers().get("content-type").unwrap(); - } - - #[test] - fn test_header_insert() { - let request = Request::builder() - .uri("/") - .header("X-Tt-Log-Id", "logid") - .body(()) - .unwrap(); - let logid = request - .headers() - .get("x-tt-log-id") - .unwrap() - .to_str() - .unwrap(); - assert_eq!(logid, "logid".to_string()); - } -} From b334dc2a890480e7984953e0413efddb93c14699 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 16 May 2025 17:45:05 +0800 Subject: [PATCH 08/20] feat: add fasthttp feature --- src/header/map.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/header/map.rs b/src/header/map.rs index 973e6f61..fac0c5a6 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -1714,13 +1714,18 @@ impl HeaderMap { if return_original_key == header_name { return_original_key.clone() } else { + let mut is_found = false; for original_key in original_keys.into_iter() { i -= 1; return_original_key = original_key; if original_key == header_name { + is_found = true; break; } } + if !is_found { + return None; + } return_original_key.clone() } } From a307d20a60e70386faa03ac8a1ae514633f4e324 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 16 May 2025 19:54:33 +0800 Subject: [PATCH 09/20] feat: add fasthttp feature --- src/ext/fasthttp/consts.rs | 41 ++++++++--------------- src/header/map.rs | 10 +++--- tests/header_map.rs | 67 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index 5ae727d8..2e1ecd89 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -17,60 +17,33 @@ impl StandardHeaders { // RFC标准header builder.push(ACCEPT_RANGES); - builder.push(ACCEPT_RANGES.as_str().to_ascii_lowercase()); builder.push(ACCEPT_LANGUAGE); - builder.push(ACCEPT_LANGUAGE.as_str().to_ascii_lowercase()); builder.push(ACCEPT_ENCODING); - builder.push(ACCEPT_ENCODING.as_str().to_ascii_lowercase()); builder.push(AUTHORIZATION); - builder.push(AUTHORIZATION.as_str().to_ascii_lowercase()); - builder.push("Expect"); builder.push("expect"); builder.push(CONTENT_TYPE); - builder.push(CONTENT_TYPE.as_str().to_ascii_lowercase()); builder.push(CONTENT_ENCODING); - builder.push(CONTENT_ENCODING.as_str().to_ascii_lowercase()); builder.push(CONTENT_RANGE); - builder.push(CONTENT_RANGE.as_str().to_ascii_lowercase()); builder.push(CONTENT_LENGTH); - builder.push(CONTENT_LENGTH.as_str().to_ascii_lowercase()); builder.push(COOKIE); - builder.push(COOKIE.as_str().to_ascii_lowercase()); builder.push(CONNECTION); - builder.push(CONNECTION.as_str().to_ascii_lowercase()); builder.push(DATE); - builder.push(DATE.as_str().to_ascii_lowercase()); builder.push(HOST); - builder.push(HOST.as_str().to_ascii_lowercase()); builder.push(IF_MODIFIED_SINCE); - builder.push(IF_MODIFIED_SINCE.as_str().to_ascii_lowercase()); builder.push(LOCATION); - builder.push(LOCATION.as_str().to_ascii_lowercase()); builder.push(LAST_MODIFIED); - builder.push(LAST_MODIFIED.as_str().to_ascii_lowercase()); builder.push(REFERER); - builder.push(REFERER.as_str().to_ascii_lowercase()); builder.push(RANGE); - builder.push(RANGE.as_str().to_ascii_lowercase()); builder.push(SERVER); - builder.push(SERVER.as_str().to_ascii_lowercase()); builder.push(SET_COOKIE); - builder.push(SET_COOKIE.as_str().to_ascii_lowercase()); builder.push(TRANSFER_ENCODING); - builder.push(TRANSFER_ENCODING.as_str().to_ascii_lowercase()); builder.push(USER_AGENT); - builder.push(USER_AGENT.as_str().to_ascii_lowercase()); builder.push(ORIGIN); - builder.push(ORIGIN.as_str().to_ascii_lowercase()); // 内部标准header - builder.push("X-Common-Params"); builder.push("x-common-params"); - builder.push("Use-Ppe"); builder.push("use-ppe"); - builder.push("Tt-Logid"); builder.push("tt-logid"); - builder.push("Tt-Env"); builder.push("tt-env"); let trie = builder.build(); @@ -79,10 +52,22 @@ impl StandardHeaders { } pub fn is_std_header(&self, header_name: &HeaderName) -> bool { - self.0.exact_match(header_name) + let lowercase_header = header_name.as_str().to_ascii_lowercase(); + self.0.exact_match(lowercase_header) } } lazy_static! { pub(crate) static ref STANDARD_HEADERS: StandardHeaders = StandardHeaders::new(); } + +#[test] +fn test_is_std_header() { + assert!(STANDARD_HEADERS.is_std_header(&"Content-Type".parse().unwrap())); + assert!(STANDARD_HEADERS.is_std_header(&"content-type".parse().unwrap())); + assert!(STANDARD_HEADERS.is_std_header(&"CONTENT-TYPE".parse().unwrap())); + assert_eq!( + STANDARD_HEADERS.is_std_header(&"content_type".parse().unwrap()), + false + ); +} diff --git a/src/header/map.rs b/src/header/map.rs index fac0c5a6..5385a00d 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -10,6 +10,7 @@ use super::{ use crate::ext::fasthttp::header_name::{ normalize_header_key, normalize_header_key_for_std_header, }; +#[cfg(feature = "fasthttp")] use crate::header::map::as_header_name::Sealed; use crate::Error; use std::collections::hash_map::RandomState; @@ -1576,13 +1577,12 @@ impl HeaderMap { { match self.mapped_keys.get(&normalize_header_key(key).into()) { Some(original_keys) => { - // get first original key - match original_keys.first() { - Some(first_original_key) => { - self.find2::(first_original_key) + for original_key in original_keys { + if original_key != key { + return self.find2::(original_key); } - None => None, } + None } None => None, } diff --git a/tests/header_map.rs b/tests/header_map.rs index 9a9d7e12..26c50ca8 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -672,3 +672,70 @@ fn ensure_miri_sharedreadonly_not_violated() { let _foo = &headers.iter().next(); } + +#[test] +fn test_append() { + let mut header_map = HeaderMap::new(); + + header_map.insert( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 2); + for (k, v) in header_map.iter() { + println!("{}: {}", k.as_raw_str(), v.to_str().unwrap()); + } + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + + header_map.append( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("text/plain"), + ); + assert_eq!(header_map.len(), 3); + let mut header_value_iter = header_map.get_all("content-type").iter(); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "application/json"); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "text/plain"); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("image/jpeg"), + ); + assert_eq!(header_map.len(), 4); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + let mut header_value_iter = header_map.get_all("Content-Type").iter(); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "text/html"); + let header_value = header_value_iter.next().unwrap(); + assert_eq!(header_value.to_str().unwrap(), "image/jpeg"); + + let mut iter = header_map.iter(); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("content-type", "application/json") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("content-type", "text/plain") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("Content-Type", "text/html") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("Content-Type", "image/jpeg") + ) +} From f4a05851dc907f9585beb84da499995868596cc6 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 16 May 2025 19:55:03 +0800 Subject: [PATCH 10/20] feat: add fasthttp feature --- src/ext/fasthttp/consts.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index 2e1ecd89..6d67007f 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -12,7 +12,6 @@ pub(crate) struct StandardHeaders(Trie); impl StandardHeaders { fn new() -> StandardHeaders { - // FIXME: 是否需要将 "Content-type" 也加入到 trie 中? let mut builder = TrieBuilder::new(); // RFC标准header From d8d625036680e498316a96313dd4351a9186afd9 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 16 May 2025 20:24:32 +0800 Subject: [PATCH 11/20] feat: add fasthttp feature --- src/header/name.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/header/name.rs b/src/header/name.rs index 23f27096..a16b8cca 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -62,6 +62,18 @@ enum Repr { impl StructuralPartialEq for Repr {} +#[cfg(not(feature = "double-write"))] +impl PartialEq for Repr { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Standard(l, _), Self::Standard(r, _)) => l == r, + (Self::Custom(l, _), Self::Custom(r, _)) => l == r, + _ => false, + } + } +} + +#[cfg(feature = "double-write")] impl PartialEq for Repr { fn eq(&self, other: &Self) -> bool { match (self, other) { From 360992cec4cbafe3c7ce818738c20c7dcb2a8c6a Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 16 May 2025 20:25:52 +0800 Subject: [PATCH 12/20] feat: add fasthttp feature --- tests/header_map.rs | 67 --------------------------------------------- 1 file changed, 67 deletions(-) diff --git a/tests/header_map.rs b/tests/header_map.rs index 26c50ca8..9a9d7e12 100644 --- a/tests/header_map.rs +++ b/tests/header_map.rs @@ -672,70 +672,3 @@ fn ensure_miri_sharedreadonly_not_violated() { let _foo = &headers.iter().next(); } - -#[test] -fn test_append() { - let mut header_map = HeaderMap::new(); - - header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), - ); - header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("text/html"), - ); - assert_eq!(header_map.len(), 2); - for (k, v) in header_map.iter() { - println!("{}: {}", k.as_raw_str(), v.to_str().unwrap()); - } - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); - - header_map.append( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("text/plain"), - ); - assert_eq!(header_map.len(), 3); - let mut header_value_iter = header_map.get_all("content-type").iter(); - let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "application/json"); - let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "text/plain"); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); - - header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("image/jpeg"), - ); - assert_eq!(header_map.len(), 4); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - let mut header_value_iter = header_map.get_all("Content-Type").iter(); - let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "text/html"); - let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "image/jpeg"); - - let mut iter = header_map.iter(); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("content-type", "application/json") - ); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("content-type", "text/plain") - ); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("Content-Type", "text/html") - ); - let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("Content-Type", "image/jpeg") - ) -} From 783f6fd0baff9177bee3e5a096e5b419c6a1d1ae Mon Sep 17 00:00:00 2001 From: StellarisW Date: Fri, 16 May 2025 21:56:01 +0800 Subject: [PATCH 13/20] feat: add fasthttp feature --- src/ext/fasthttp/header_name.rs | 8 +++--- src/header/map.rs | 46 ++++++++++++++------------------- src/header/name.rs | 39 +++++++++++++++++++++++++++- tests/fasthttp.rs | 9 ++++--- 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs index 8a39f298..664cbe0b 100644 --- a/src/ext/fasthttp/header_name.rs +++ b/src/ext/fasthttp/header_name.rs @@ -8,7 +8,7 @@ lazy_static::lazy_static! { let mut a = [0u8; 256]; for i in 0..256 { let mut c = i as u8; - if (b'A'..=b'Z').contains(&c){ + if c.is_ascii_uppercase(){ c += TO_LOWER; } a[i] = c; @@ -20,7 +20,7 @@ lazy_static::lazy_static! { let mut a = [0u8; 256]; for i in 0..256 { let mut c = i as u8; - if (b'a'..=b'z').contains(&c){ + if c.is_ascii_lowercase(){ c -= TO_LOWER; } a[i] = c; @@ -82,7 +82,9 @@ where #[allow(dead_code)] #[cfg(test)] mod tests { - use crate::ext::fasthttp::header_name::{normalize_header_key_for_std_header, normalize_header_key}; + use crate::ext::fasthttp::header_name::{ + normalize_header_key, normalize_header_key_for_std_header, + }; use crate::HeaderName; use std::str::FromStr; diff --git a/src/header/map.rs b/src/header/map.rs index 5385a00d..a0216e96 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -463,16 +463,13 @@ impl HeaderMap { } impl HeaderMap { - /// - /// #[cfg(feature = "fasthttp")] fn append_to_mapped_keys(&mut self, original_key: HeaderName) { let normalized_key = normalize_header_key(&original_key).into(); match self.mapped_keys.get_mut(&normalized_key) { Some(original_keys) => original_keys.push(original_key), None => { - let mut original_keys = Vec::new(); - original_keys.push(original_key.clone()); + let original_keys = vec![original_key.clone()]; self.mapped_keys.insert(normalized_key, original_keys); } } @@ -1572,25 +1569,24 @@ impl HeaderMap { return None; } - self.find2(key).or({ - #[cfg(feature = "fasthttp")] - { - match self.mapped_keys.get(&normalize_header_key(key).into()) { - Some(original_keys) => { - for original_key in original_keys { - if original_key != key { - return self.find2::(original_key); - } + #[cfg(feature = "fasthttp")] + match self.find2(key) { + x @ Some(_) => x, + None => match self.mapped_keys.get(&normalize_header_key(key).into()) { + Some(original_keys) => { + for original_key in original_keys { + if original_key != key { + return self.find2::(original_key); } - None } - None => None, + None } - } + None => None, + }, + } - #[cfg(not(feature = "fasthttp"))] - None - }) + #[cfg(not(feature = "fasthttp"))] + self.find2(key) } #[inline] @@ -1715,7 +1711,7 @@ impl HeaderMap { return_original_key.clone() } else { let mut is_found = false; - for original_key in original_keys.into_iter() { + for original_key in original_keys.iter() { i -= 1; return_original_key = original_key; if original_key == header_name { @@ -1744,11 +1740,9 @@ impl HeaderMap { let entry = self.remove_found(probe, idx); - self.mapped_keys - .get_mut(&normalized_key) - .map(|original_keys| { - original_keys.remove(i); - }); + if let Some(original_keys) = self.mapped_keys.get_mut(&normalized_key) { + original_keys.remove(i); + } Some(entry.value) } None => None, @@ -1926,7 +1920,7 @@ impl HeaderMap { return Err(MaxSizeReached::new()); } - let normalized_key = normalize_header_key_for_std_header(&(key.clone()), false).into(); + let normalized_key = normalize_header_key_for_std_header(&(key.clone()), false); match normalized_key { HOST | CONTENT_TYPE | USER_AGENT | COOKIE | CONTENT_LENGTH | CONNECTION | TRANSFER_ENCODING => { diff --git a/src/header/name.rs b/src/header/name.rs index a16b8cca..fffa58b3 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -82,7 +82,11 @@ impl PartialEq for Repr { (None, None) => la == ra, _ => false, }, - (Self::Custom(l, _), Self::Custom(r, _)) => l == r, + (Self::Custom(la, lb), Self::Custom(ra, rb)) => match (lb, rb) { + (Some(lb), Some(rb)) => lb == rb, + (None, None) => la == ra, + _ => la == ra, + }, _ => false, } } @@ -1765,6 +1769,7 @@ impl<'a> From> for HeaderName { } #[doc(hidden)] +#[cfg(not(feature = "double-write"))] impl<'a> PartialEq> for HeaderName { #[inline] fn eq(&self, other: &HdrName<'a>) -> bool { @@ -1787,6 +1792,38 @@ impl<'a> PartialEq> for HeaderName { } } +#[doc(hidden)] +#[cfg(feature = "double-write")] +impl<'a> PartialEq> for HeaderName { + #[inline] + fn eq(&self, other: &HdrName<'a>) -> bool { + match &self.inner { + Repr::Standard(la, lb) => match (lb, other.original) { + (Some(lb), Some(rb)) => lb.as_bytes() == rb, + (None, None) => match &other.inner { + Repr::Standard(ra, _) => la == ra, + _ => false, + }, + _ => false, + }, + Repr::Custom(Custom(ref la), lb) => match (lb, other.original) { + (Some(lb), Some(rb)) => lb.as_bytes() == rb, + (None, None) => match other.inner { + Repr::Custom(ref ra, _) => { + if ra.lower { + la.as_bytes() == ra.buf + } else { + eq_ignore_ascii_case(la.as_bytes(), ra.buf) + } + } + _ => false, + }, + _ => false, + }, + } + } +} + // ===== Custom ===== impl Hash for Custom { diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs index ff7e0561..b647e36d 100644 --- a/tests/fasthttp.rs +++ b/tests/fasthttp.rs @@ -119,7 +119,7 @@ mod fasthttp_tests { header_map.remove(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); assert_eq!(header_map.len(), 0); assert_eq!(header_map.get("content-type"), None); - let mut header_map = HeaderMap::new(); + let mut header_map = HeaderMap::new(); header_map.insert( HeaderName::from_bytes("content-type".as_bytes()).unwrap(), HeaderValue::from_static("application/json"), @@ -161,7 +161,7 @@ mod fasthttp_tests { header_map.remove_all(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); assert_eq!(header_map.len(), 0); assert_eq!(header_map.get("content-type"), None); - let mut header_map = HeaderMap::new(); + let mut header_map = HeaderMap::new(); header_map.insert( HeaderName::from_bytes("content-type".as_bytes()).unwrap(), HeaderValue::from_static("application/json"), @@ -198,7 +198,10 @@ mod fasthttp_tests { .unwrap(); assert_eq!(request.headers().get("content-type").unwrap(), "text/plain"); - assert_eq!(request.headers().get("Content-Type").unwrap(), "application/json"); + assert_eq!( + request.headers().get("Content-Type").unwrap(), + "application/json" + ); let mut header_value_iter = request.headers().get_all("Content-Type").iter(); let header_value = header_value_iter.next().unwrap(); assert_eq!(header_value.to_str().unwrap(), "application/json"); From 987417558381b8dc87e8130b05102ed313108cee Mon Sep 17 00:00:00 2001 From: StellarisW Date: Tue, 20 May 2025 14:41:37 +0800 Subject: [PATCH 14/20] fix: append for special std header --- src/ext/fasthttp/consts.rs | 41 ++++++- src/ext/fasthttp/header_name.rs | 5 +- src/ext/fasthttp/mod.rs | 2 +- src/header/map.rs | 184 ++++++++++++++++++-------------- src/header/name.rs | 2 +- tests/fasthttp.rs | 183 +++++++++++++++++-------------- 6 files changed, 253 insertions(+), 164 deletions(-) diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index 6d67007f..43189542 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -50,14 +50,51 @@ impl StandardHeaders { StandardHeaders(trie) } - pub fn is_std_header(&self, header_name: &HeaderName) -> bool { + pub fn is_match(&self, header_name: &HeaderName) -> bool { + let lowercase_header = header_name.as_str().to_ascii_lowercase(); + self.0.exact_match(lowercase_header) + } +} + +pub(crate) struct NonAppendStandardHeaders(Trie); + +impl NonAppendStandardHeaders { + fn new() -> NonAppendStandardHeaders { + let mut builder = TrieBuilder::new(); + + // 不可有多个值的特殊 std header + // 调用 append 等同于 insert + builder.push(CONTENT_TYPE); + builder.push(SERVER); + builder.push(SET_COOKIE); + builder.push(CONTENT_LENGTH); + builder.push(CONNECTION); + builder.push(TRANSFER_ENCODING); + builder.push(DATE); + + let trie = builder.build(); + + NonAppendStandardHeaders(trie) + } + + pub fn is_match(&self, header_name: &HeaderName) -> bool { let lowercase_header = header_name.as_str().to_ascii_lowercase(); self.0.exact_match(lowercase_header) } } lazy_static! { - pub(crate) static ref STANDARD_HEADERS: StandardHeaders = StandardHeaders::new(); + pub(super) static ref STANDARD_HEADERS: StandardHeaders = StandardHeaders::new(); + pub(super) static ref NON_APPEND_STANDARD_HEADERS: NonAppendStandardHeaders = + NonAppendStandardHeaders::new(); +} + +pub fn is_standard_header(header_name: &HeaderName) -> bool { + STANDARD_HEADERS.is_match(header_name) +} + +pub fn is_non_append_standard_headers(header_name: &HeaderName) -> bool { + NON_APPEND_STANDARD_HEADERS.is_match(header_name) } #[test] diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs index 664cbe0b..28b15d1c 100644 --- a/src/ext/fasthttp/header_name.rs +++ b/src/ext/fasthttp/header_name.rs @@ -1,4 +1,4 @@ -use crate::ext::fasthttp::consts::STANDARD_HEADERS; +use crate::ext::fasthttp::consts::is_standard_header; use crate::HeaderName; const TO_LOWER: u8 = b'a' - b'A'; @@ -30,6 +30,7 @@ lazy_static::lazy_static! { } /// 只normalize标准header,其他自定义header保持原样透传 +#[allow(warnings)] pub(crate) fn normalize_header_key_for_std_header( header_name: &HeaderName, disable_normalizing: bool, @@ -38,7 +39,7 @@ pub(crate) fn normalize_header_key_for_std_header( return header_name.clone(); } - if !STANDARD_HEADERS.is_std_header(&(header_name.clone())) { + if !is_standard_header(&(header_name.clone())) { return header_name.clone(); } diff --git a/src/ext/fasthttp/mod.rs b/src/ext/fasthttp/mod.rs index 30cf9b3a..6196dae4 100644 --- a/src/ext/fasthttp/mod.rs +++ b/src/ext/fasthttp/mod.rs @@ -1,2 +1,2 @@ -mod consts; +pub mod consts; pub mod header_name; diff --git a/src/header/map.rs b/src/header/map.rs index a0216e96..8bb9f33e 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -3,9 +3,7 @@ pub use self::into_header_name::IntoHeaderName; use super::name::{HdrName, HeaderName, InvalidHeaderName}; use super::HeaderValue; #[cfg(feature = "fasthttp")] -use super::{ - CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, HOST, TRANSFER_ENCODING, USER_AGENT, -}; +use crate::ext::fasthttp::consts::is_non_append_standard_headers; #[cfg(feature = "fasthttp")] use crate::ext::fasthttp::header_name::{ normalize_header_key, normalize_header_key_for_std_header, @@ -1380,6 +1378,63 @@ impl HeaderMap { )) } + #[cfg(feature = "fasthttp")] + fn try_insert2_do(&mut self, key: HeaderName, value: T) -> Result, MaxSizeReached> { + self.try_reserve_one()?; + + #[cfg(feature = "fasthttp")] + { + Ok(insert_phase_one!( + self, + key, + probe, + pos, + hash, + danger, + // Vacant + { + let _ = danger; // Make lint happy + self.try_insert_entry(hash, key.into(), value)?; + let index = self.entries.len() - 1; + self.indices[probe] = Pos::new(index, hash); + None + }, + // Occupied + Some(self.insert_occupied(pos, value)), + // Robinhood + { + self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + None + } + )) + } + + #[cfg(not(feature = "fasthttp"))] + Ok(insert_phase_one!( + self, + key, + probe, + pos, + hash, + danger, + // Vacant + { + let _ = danger; // Make lint happy + let index = self.entries.len(); + self.try_insert_entry(hash, key.into(), value)?; + self.indices[probe] = Pos::new(index, hash); + None + }, + // Occupied + Some(self.insert_occupied(pos, value)), + // Robinhood + { + self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + None + } + )) + } + /// Set an occupied bucket to the given value #[inline] fn insert_occupied(&mut self, index: usize, value: T) -> T { @@ -1494,45 +1549,61 @@ impl HeaderMap { fn try_append2(&mut self, key: K, value: T) -> Result where K: Hash + Into, - HeaderName: PartialEq, { self.try_reserve_one()?; + let header_name = key.into(); + #[cfg(feature = "fasthttp")] { - Ok(insert_phase_one!( - self, - key, - probe, - pos, - hash, - danger, - // Vacant - { - let _ = danger; - let index = self.entries.len(); - self.try_insert_entry_for_append(hash, key.into(), value)?; - self.indices[probe] = Pos::new(index, hash); - false - }, - // Occupied - { - append_value(pos, &mut self.entries[pos], &mut self.extra_values, value); - true - }, - // Robinhood - { - self.try_insert_phase_two_for_append(key.into(), value, hash, probe, danger)?; - - false + let normalized_header_name = normalize_header_key_for_std_header(&header_name, true); + if is_non_append_standard_headers(&normalized_header_name) { + match self.try_insert2_do(header_name, value) { + Ok(None) => return Ok(false), + Ok(Some(_)) => return Ok(false), + Err(e) => return Err(e), } - )) + } else { + Ok(insert_phase_one!( + self, + header_name, + probe, + pos, + hash, + danger, + // Vacant + { + let _ = danger; + let index = self.entries.len(); + self.try_insert_entry_for_append(hash, header_name, value)?; + self.indices[probe] = Pos::new(index, hash); + false + }, + // Occupied + { + append_value(pos, &mut self.entries[pos], &mut self.extra_values, value); + true + }, + // Robinhood + { + self.try_insert_phase_two_for_append( + header_name, + value, + hash, + probe, + danger, + )?; + + false + } + )) + } } #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, - key, + header_name, probe, pos, hash, @@ -1541,7 +1612,7 @@ impl HeaderMap { { let _ = danger; let index = self.entries.len(); - self.try_insert_entry(hash, key.into(), value)?; + self.try_insert_entry(hash, header_name, value)?; self.indices[probe] = Pos::new(index, hash); false }, @@ -1552,7 +1623,7 @@ impl HeaderMap { }, // Robinhood { - self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + self.try_insert_phase_two(header_name, value, hash, probe, danger)?; false } @@ -1699,40 +1770,7 @@ impl HeaderMap { let normalized_key = normalize_header_key(&header_name).into(); - let mut i; - let original_key = match self.mapped_keys.get(&normalized_key) { - Some(original_keys) => match original_keys.iter().next_back() { - Some(return_original_key) => { - i = original_keys.len() - 1; - let mut return_original_key = return_original_key; - let _a = return_original_key.clone().as_raw_str(); - let _b = header_name.clone().as_raw_str(); - if return_original_key == header_name { - return_original_key.clone() - } else { - let mut is_found = false; - for original_key in original_keys.iter() { - i -= 1; - return_original_key = original_key; - if original_key == header_name { - is_found = true; - break; - } - } - if !is_found { - return None; - } - return_original_key.clone() - } - } - None => return None, - }, - None => { - return None; - } - }; - - match original_key.find(self) { + match header_name.find(self) { Some((probe, idx)) => { if let Some(links) = self.entries[idx].links { self.remove_all_extra_values(links.next); @@ -1741,7 +1779,7 @@ impl HeaderMap { let entry = self.remove_found(probe, idx); if let Some(original_keys) = self.mapped_keys.get_mut(&normalized_key) { - original_keys.remove(i); + original_keys.retain(|x| x != &header_name); } Some(entry.value) } @@ -1920,16 +1958,6 @@ impl HeaderMap { return Err(MaxSizeReached::new()); } - let normalized_key = normalize_header_key_for_std_header(&(key.clone()), false); - match normalized_key { - HOST | CONTENT_TYPE | USER_AGENT | COOKIE | CONTENT_LENGTH | CONNECTION - | TRANSFER_ENCODING => { - // 清理掉所有的 insensitive key - self.remove_key_insensitively(&key); - } - _ => {} - } - self.append_to_mapped_keys(key.clone()); self.entries.push(Bucket { diff --git a/src/header/name.rs b/src/header/name.rs index fffa58b3..082003ce 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -85,7 +85,7 @@ impl PartialEq for Repr { (Self::Custom(la, lb), Self::Custom(ra, rb)) => match (lb, rb) { (Some(lb), Some(rb)) => lb == rb, (None, None) => la == ra, - _ => la == ra, + _ => false, }, _ => false, } diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs index b647e36d..d537ca5c 100644 --- a/tests/fasthttp.rs +++ b/tests/fasthttp.rs @@ -43,147 +43,173 @@ mod fasthttp_tests { ); } + // tes append for non-std headers #[test] fn test_append() { let mut header_map = HeaderMap::new(); header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar1"), ); header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("text/html"), + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar2"), ); assert_eq!(header_map.len(), 2); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + assert_eq!(header_map.get("foo").unwrap(), "bar1"); + assert_eq!(header_map.get("Foo").unwrap(), "bar2"); header_map.append( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("text/plain"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar3"), ); assert_eq!(header_map.len(), 3); - let mut header_value_iter = header_map.get_all("content-type").iter(); + let mut header_value_iter = header_map.get_all("foo").iter(); let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "application/json"); + assert_eq!(header_value.to_str().unwrap(), "bar1"); let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "text/plain"); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + assert_eq!(header_value.to_str().unwrap(), "bar3"); + assert_eq!(header_map.get("foo").unwrap(), "bar1"); + assert_eq!(header_map.get("Foo").unwrap(), "bar2"); header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("image/jpeg"), + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar4"), ); assert_eq!(header_map.len(), 4); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - let mut header_value_iter = header_map.get_all("Content-Type").iter(); + assert_eq!(header_map.get("foo").unwrap(), "bar1"); + let mut header_value_iter = header_map.get_all("Foo").iter(); let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "text/html"); + assert_eq!(header_value.to_str().unwrap(), "bar2"); let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "image/jpeg"); + assert_eq!(header_value.to_str().unwrap(), "bar4"); let mut iter = header_map.iter(); let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("content-type", "application/json") - ); + assert_eq!((k.as_raw_str(), v.to_str().unwrap()), ("foo", "bar1")); let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("content-type", "text/plain") - ); + assert_eq!((k.as_raw_str(), v.to_str().unwrap()), ("foo", "bar3")); let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("Content-Type", "text/html") - ); + assert_eq!((k.as_raw_str(), v.to_str().unwrap()), ("Foo", "bar2")); let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("Content-Type", "image/jpeg") - ) + assert_eq!((k.as_raw_str(), v.to_str().unwrap()), ("Foo", "bar4")) + } + + #[test] + fn test_append_special_std_header() { + // test append for special std header, should equal to insert + let mut header_map = HeaderMap::new(); + + header_map.insert( + HeaderName::from_bytes("content-length".as_bytes()).unwrap(), + HeaderValue::from_static("1"), + ); + header_map.append( + HeaderName::from_bytes("Content-Length".as_bytes()).unwrap(), + HeaderValue::from_static("2"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-length").unwrap(), "2"); + assert_eq!(header_map.get("Content-Length").unwrap(), "2"); + + header_map.append( + HeaderName::from_bytes("content-length".as_bytes()).unwrap(), + HeaderValue::from_static("3"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-length").unwrap(), "3"); + + header_map.append( + HeaderName::from_bytes("Content-Length".as_bytes()).unwrap(), + HeaderValue::from_static("4"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-length").unwrap(), "4"); } #[test] fn test_remove() { let mut header_map = HeaderMap::new(); header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar1"), ); assert_eq!(header_map.len(), 1); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - header_map.remove(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + assert_eq!(header_map.get("foo").unwrap(), "bar1"); + + header_map.remove(HeaderName::from_bytes("foo".as_bytes()).unwrap()); assert_eq!(header_map.len(), 0); - assert_eq!(header_map.get("content-type"), None); + assert_eq!(header_map.get("foo"), None); let mut header_map = HeaderMap::new(); + header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar1"), ); header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("text/html"), + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar2"), ); assert_eq!(header_map.len(), 2); - header_map.remove(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + + header_map.remove(HeaderName::from_bytes("foo".as_bytes()).unwrap()); assert_eq!(header_map.len(), 1); - assert_eq!(header_map.get("content-type").unwrap(), "text/html"); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + assert_eq!(header_map.get("foo").unwrap(), "bar2"); + assert_eq!(header_map.get("Foo").unwrap(), "bar2"); + let mut header_map = HeaderMap::new(); header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar1"), ); header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("text/html"), + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar2"), ); assert_eq!(header_map.len(), 2); - header_map.remove(HeaderName::from_bytes("Content-Type".as_bytes()).unwrap()); + + header_map.remove(HeaderName::from_bytes("Foo".as_bytes()).unwrap()); assert_eq!(header_map.len(), 1); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - assert_eq!(header_map.get("Content-Type").unwrap(), "application/json"); + assert_eq!(header_map.get("foo").unwrap(), "bar1"); + assert_eq!(header_map.get("Foo").unwrap(), "bar1"); } #[test] fn test_remove_all() { let mut header_map = HeaderMap::new(); header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("foo1"), ); assert_eq!(header_map.len(), 1); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - header_map.remove_all(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + assert_eq!(header_map.get("foo").unwrap(), "foo1"); + header_map.remove_all(HeaderName::from_bytes("foo".as_bytes()).unwrap()); assert_eq!(header_map.len(), 0); - assert_eq!(header_map.get("content-type"), None); + assert_eq!(header_map.get("foo"), None); let mut header_map = HeaderMap::new(); header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("foo1"), ); header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("text/html"), + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("foo2"), ); assert_eq!(header_map.len(), 2); - header_map.remove_all(HeaderName::from_bytes("content-type".as_bytes()).unwrap()); + header_map.remove_all(HeaderName::from_bytes("foo".as_bytes()).unwrap()); assert_eq!(header_map.len(), 0); let mut header_map = HeaderMap::new(); header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("foo1"), ); header_map.append( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("text/html"), + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("foo2"), ); assert_eq!(header_map.len(), 2); - header_map.remove_all(HeaderName::from_bytes("Content-Type".as_bytes()).unwrap()); + header_map.remove_all(HeaderName::from_bytes("Foo".as_bytes()).unwrap()); assert_eq!(header_map.len(), 0); } @@ -191,21 +217,18 @@ mod fasthttp_tests { fn test_get_with_request() { let request = Request::builder() .uri("/") - .header("Content-Type", "application/json") - .header("Content-Type", "text/html") - .header("content-type", "text/plain") + .header("Foo", "bar1") + .header("Foo", "bar2") + .header("foo", "bar3") .body(()) .unwrap(); - assert_eq!(request.headers().get("content-type").unwrap(), "text/plain"); - assert_eq!( - request.headers().get("Content-Type").unwrap(), - "application/json" - ); - let mut header_value_iter = request.headers().get_all("Content-Type").iter(); + assert_eq!(request.headers().get("foo").unwrap(), "bar3"); + assert_eq!(request.headers().get("Foo").unwrap(), "bar1"); + let mut header_value_iter = request.headers().get_all("Foo").iter(); let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "application/json"); + assert_eq!(header_value.to_str().unwrap(), "bar1"); let header_value = header_value_iter.next().unwrap(); - assert_eq!(header_value.to_str().unwrap(), "text/html"); + assert_eq!(header_value.to_str().unwrap(), "bar2"); } } From e8680b0b78bfe3074cca61d7878d2a0f5393ef96 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Mon, 26 May 2025 00:15:29 +0800 Subject: [PATCH 15/20] test: fix --- Cargo.toml | 1 + src/ext/fasthttp/consts.rs | 47 +++++++++++++++++++----- src/ext/fasthttp/header_name.rs | 1 + src/ext/fasthttp/mod.rs | 1 + src/ext/mod.rs | 1 + src/header/map.rs | 63 ++++++++++++++++++++++----------- src/header/name.rs | 2 +- src/lib.rs | 3 +- tests/fasthttp.rs | 16 +++++++++ tests/header_map_fuzz.rs | 38 ++++++++++++++++++++ 10 files changed, 142 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a0fda57f..c5905eb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ fnv = "1.0.5" itoa = "1" trie-rs = "0.4.2" lazy_static = "1.5" +smallvec = { version = "1.15.0", features = ["specialization"]} [dev-dependencies] quickcheck = "1" diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index 43189542..2c537f51 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -1,3 +1,4 @@ +//! consts for fasthttp use crate::header::{ ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, AUTHORIZATION, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, COOKIE, DATE, HOST, IF_MODIFIED_SINCE, @@ -64,6 +65,29 @@ impl NonAppendStandardHeaders { // 不可有多个值的特殊 std header // 调用 append 等同于 insert + builder.push(CONTENT_LENGTH); + builder.push(CONNECTION); + builder.push(TRANSFER_ENCODING); + builder.push(DATE); + + let trie = builder.build(); + + NonAppendStandardHeaders(trie) + } + + pub fn is_match(&self, header_name: &HeaderName) -> bool { + let lowercase_header = header_name.as_str().to_ascii_lowercase(); + self.0.exact_match(lowercase_header) + } +} + +pub(crate) struct NonDuplicateHeaders(Trie); + +impl NonDuplicateHeaders { + fn new() -> NonDuplicateHeaders { + let mut builder = TrieBuilder::new(); + + // 不可有重名的特殊 std header(大小写不敏感下只能存在一个) builder.push(CONTENT_TYPE); builder.push(SERVER); builder.push(SET_COOKIE); @@ -74,7 +98,7 @@ impl NonAppendStandardHeaders { let trie = builder.build(); - NonAppendStandardHeaders(trie) + NonDuplicateHeaders(trie) } pub fn is_match(&self, header_name: &HeaderName) -> bool { @@ -87,23 +111,28 @@ lazy_static! { pub(super) static ref STANDARD_HEADERS: StandardHeaders = StandardHeaders::new(); pub(super) static ref NON_APPEND_STANDARD_HEADERS: NonAppendStandardHeaders = NonAppendStandardHeaders::new(); + pub(super) static ref NON_DUPLICATE_HEADERS: NonDuplicateHeaders = NonDuplicateHeaders::new(); } +/// check if is standard header pub fn is_standard_header(header_name: &HeaderName) -> bool { STANDARD_HEADERS.is_match(header_name) } -pub fn is_non_append_standard_headers(header_name: &HeaderName) -> bool { +/// check if is non-append standard header +pub fn is_non_append_standard_header(header_name: &HeaderName) -> bool { NON_APPEND_STANDARD_HEADERS.is_match(header_name) } +/// check if is non-duplicate header +pub fn is_non_duplicate_header(header_name: &HeaderName) -> bool { + NON_DUPLICATE_HEADERS.is_match(header_name) +} + #[test] fn test_is_std_header() { - assert!(STANDARD_HEADERS.is_std_header(&"Content-Type".parse().unwrap())); - assert!(STANDARD_HEADERS.is_std_header(&"content-type".parse().unwrap())); - assert!(STANDARD_HEADERS.is_std_header(&"CONTENT-TYPE".parse().unwrap())); - assert_eq!( - STANDARD_HEADERS.is_std_header(&"content_type".parse().unwrap()), - false - ); + assert!(is_standard_header(&"Content-Type".parse().unwrap())); + assert!(is_standard_header(&"content-type".parse().unwrap())); + assert!(is_standard_header(&"CONTENT-TYPE".parse().unwrap())); + assert_eq!(is_standard_header(&"content_type".parse().unwrap()), false); } diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs index 28b15d1c..b14e33d6 100644 --- a/src/ext/fasthttp/header_name.rs +++ b/src/ext/fasthttp/header_name.rs @@ -1,3 +1,4 @@ +//! normalization for fasthttp use crate::ext::fasthttp::consts::is_standard_header; use crate::HeaderName; diff --git a/src/ext/fasthttp/mod.rs b/src/ext/fasthttp/mod.rs index 6196dae4..7d547753 100644 --- a/src/ext/fasthttp/mod.rs +++ b/src/ext/fasthttp/mod.rs @@ -1,2 +1,3 @@ +//! feature that compatible with fasthttp pub mod consts; pub mod header_name; diff --git a/src/ext/mod.rs b/src/ext/mod.rs index ff9558b6..64828a25 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,2 +1,3 @@ +//! ext module #[cfg(feature = "fasthttp")] pub mod fasthttp; diff --git a/src/header/map.rs b/src/header/map.rs index 8bb9f33e..dd8553f2 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -3,14 +3,15 @@ pub use self::into_header_name::IntoHeaderName; use super::name::{HdrName, HeaderName, InvalidHeaderName}; use super::HeaderValue; #[cfg(feature = "fasthttp")] -use crate::ext::fasthttp::consts::is_non_append_standard_headers; +use crate::ext::fasthttp::consts::is_non_append_standard_header; +use crate::ext::fasthttp::consts::is_non_duplicate_header; #[cfg(feature = "fasthttp")] -use crate::ext::fasthttp::header_name::{ - normalize_header_key, normalize_header_key_for_std_header, -}; +use crate::ext::fasthttp::header_name::normalize_header_key; #[cfg(feature = "fasthttp")] use crate::header::map::as_header_name::Sealed; use crate::Error; +#[cfg(feature = "fasthttp")] +use smallvec::{smallvec, SmallVec}; use std::collections::hash_map::RandomState; use std::collections::HashMap; use std::convert::TryFrom; @@ -58,7 +59,7 @@ pub struct HeaderMap { danger: Danger, #[cfg(feature = "fasthttp")] - mapped_keys: HashMap>, // normalized_key -> original_keys + mapped_keys: HashMap>, // normalized_key -> original_keys } // # Implementation notes @@ -467,29 +468,36 @@ impl HeaderMap { match self.mapped_keys.get_mut(&normalized_key) { Some(original_keys) => original_keys.push(original_key), None => { - let original_keys = vec![original_key.clone()]; + let original_keys = smallvec![original_key.clone()]; self.mapped_keys.insert(normalized_key, original_keys); } } } #[cfg(feature = "fasthttp")] - fn remove_key_insensitively(&mut self, original_key: &HeaderName) { + fn remove_all_keys_insensitively(&mut self, original_key: &HeaderName) { let normalized_key = normalize_header_key(&original_key).into(); - let mut keys = Vec::new(); - - if let Some(original_keys) = self.mapped_keys.get(&normalized_key) { + if let Some(original_keys) = self.mapped_keys.remove(&normalized_key) { for key in original_keys { - keys.push(key.clone()); + self.remove(key); } } + } - for key in keys { - self.remove(key); - } + #[cfg(feature = "fasthttp")] + fn remove_other_keys_insensitively(&mut self, original_key: &HeaderName) { + let normalized_key = normalize_header_key(&original_key).into(); - self.mapped_keys.remove(&normalized_key); + if let Some(original_keys) = self.mapped_keys.remove(&normalized_key) { + for key in original_keys { + if key != original_key { + self.remove(key); + } + } + let keys = smallvec![original_key.clone()]; + self.mapped_keys.insert(normalized_key, keys); + } } /// Create an empty `HeaderMap` with the specified capacity. @@ -1343,7 +1351,7 @@ impl HeaderMap { None }, // Occupied - Some(self.insert_occupied(pos, value)), + Some(self.insert_occupied2(key.into(), pos, value)), // Robinhood { self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; @@ -1446,6 +1454,19 @@ impl HeaderMap { mem::replace(&mut entry.value, value) } + #[cfg(feature = "fasthttp")] + #[inline] + fn insert_occupied2(&mut self, key: HeaderName, index: usize, value: T) -> T { + if let Some(links) = self.entries[index].links { + self.remove_all_extra_values(links.next); + } + + self.remove_other_keys_insensitively(&key); + + let entry = &mut self.entries[index]; + mem::replace(&mut entry.value, value) + } + fn insert_occupied_mult(&mut self, index: usize, value: T) -> ValueDrain<'_, T> { let old; let links; @@ -1556,8 +1577,7 @@ impl HeaderMap { #[cfg(feature = "fasthttp")] { - let normalized_header_name = normalize_header_key_for_std_header(&header_name, true); - if is_non_append_standard_headers(&normalized_header_name) { + if is_non_append_standard_header(&header_name) { match self.try_insert2_do(header_name, value) { Ok(None) => return Ok(false), Ok(Some(_)) => return Ok(false), @@ -1819,7 +1839,7 @@ impl HeaderMap { let mut return_headers = vec![]; - let original_keys = match self.mapped_keys.get(&normalized_key) { + let original_keys = match self.mapped_keys.remove(&normalized_key) { Some(original_keys) => original_keys.clone(), None => { return None; @@ -1932,7 +1952,7 @@ impl HeaderMap { #[cfg(feature = "fasthttp")] { - self.remove_key_insensitively(&key); + self.remove_all_keys_insensitively(&key); self.append_to_mapped_keys(key.clone()); } @@ -1958,6 +1978,9 @@ impl HeaderMap { return Err(MaxSizeReached::new()); } + if is_non_duplicate_header(&key) { + self.remove_all_keys_insensitively(&key); + } self.append_to_mapped_keys(key.clone()); self.entries.push(Bucket { diff --git a/src/header/name.rs b/src/header/name.rs index 082003ce..3815651b 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -4,7 +4,7 @@ use bytes::{Bytes, BytesMut}; use std::borrow::Borrow; use std::convert::TryFrom; use std::error::Error; -use std::fmt; +use std::{fmt}; use std::hash::{Hash, Hasher}; use std::marker::StructuralPartialEq; use std::mem::MaybeUninit; diff --git a/src/lib.rs b/src/lib.rs index 0c2c11f1..2d4849ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,7 +180,8 @@ pub mod version; mod byte_str; mod error; -mod ext; +#[cfg(feature = "fasthttp")] +pub mod ext; mod extensions; pub use crate::error::{Error, Result}; diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs index d537ca5c..95bc1f44 100644 --- a/tests/fasthttp.rs +++ b/tests/fasthttp.rs @@ -231,4 +231,20 @@ mod fasthttp_tests { let header_value = header_value_iter.next().unwrap(); assert_eq!(header_value.to_str().unwrap(), "bar2"); } + + #[test] + fn append_multiple_values() { + let mut map = HeaderMap::new(); + + map.append("foo", "bar1".parse().unwrap()); + map.append("foo", "bar2".parse().unwrap()); + map.append("foo", "bar3".parse().unwrap()); + + let vals = map + .get_all("foo") + .iter() + .collect::>(); + + assert_eq!(&vals, &[&"bar1", &"bar2", &"bar3"]); + } } diff --git a/tests/header_map_fuzz.rs b/tests/header_map_fuzz.rs index 40db0494..60379555 100644 --- a/tests/header_map_fuzz.rs +++ b/tests/header_map_fuzz.rs @@ -6,6 +6,10 @@ use rand::rngs::StdRng; use rand::seq::SliceRandom; use rand::{Rng, SeedableRng}; +#[cfg(feature = "fasthttp")] +use crate::ext::fasthttp::consts::is_non_append_standard_header; +#[cfg(feature = "fasthttp")] +use http::ext::fasthttp::consts::is_non_duplicate_header; use std::collections::HashMap; #[cfg(not(miri))] @@ -191,9 +195,43 @@ impl AltMap { if let Some(name) = self.find_random_name(rng) { name } else { + #[cfg(feature = "fasthttp")] + { + let mut header_name = gen_header_name(rng); + + loop { + if is_non_append_standard_header(&header_name) + || is_non_duplicate_header(&header_name) + { + header_name = gen_header_name(rng); + } else { + break; + } + } + + header_name + } + #[cfg(not(feature = "fasthttp"))] gen_header_name(rng) } } else { + #[cfg(feature = "fasthttp")] + { + let mut header_name = gen_header_name(rng); + + loop { + if is_non_append_standard_header(&header_name) + || is_non_duplicate_header(&header_name) + { + header_name = gen_header_name(rng); + } else { + break; + } + } + + header_name + } + #[cfg(not(feature = "fasthttp"))] gen_header_name(rng) } } From 670864b16612ebb0a3b09a6493897891f8bf68e4 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Mon, 26 May 2025 00:21:13 +0800 Subject: [PATCH 16/20] test: fix --- src/header/name.rs | 2 +- tests/fasthttp.rs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/header/name.rs b/src/header/name.rs index 3815651b..082003ce 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -4,7 +4,7 @@ use bytes::{Bytes, BytesMut}; use std::borrow::Borrow; use std::convert::TryFrom; use std::error::Error; -use std::{fmt}; +use std::fmt; use std::hash::{Hash, Hasher}; use std::marker::StructuralPartialEq; use std::mem::MaybeUninit; diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs index 95bc1f44..debabcd0 100644 --- a/tests/fasthttp.rs +++ b/tests/fasthttp.rs @@ -231,7 +231,7 @@ mod fasthttp_tests { let header_value = header_value_iter.next().unwrap(); assert_eq!(header_value.to_str().unwrap(), "bar2"); } - + #[test] fn append_multiple_values() { let mut map = HeaderMap::new(); @@ -240,10 +240,7 @@ mod fasthttp_tests { map.append("foo", "bar2".parse().unwrap()); map.append("foo", "bar3".parse().unwrap()); - let vals = map - .get_all("foo") - .iter() - .collect::>(); + let vals = map.get_all("foo").iter().collect::>(); assert_eq!(&vals, &[&"bar1", &"bar2", &"bar3"]); } From 767a1353f6e48232a78797f16caa48fd7ceb6992 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Mon, 26 May 2025 00:21:27 +0800 Subject: [PATCH 17/20] test: fix --- tests/fasthttp.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs index debabcd0..d537ca5c 100644 --- a/tests/fasthttp.rs +++ b/tests/fasthttp.rs @@ -231,17 +231,4 @@ mod fasthttp_tests { let header_value = header_value_iter.next().unwrap(); assert_eq!(header_value.to_str().unwrap(), "bar2"); } - - #[test] - fn append_multiple_values() { - let mut map = HeaderMap::new(); - - map.append("foo", "bar1".parse().unwrap()); - map.append("foo", "bar2".parse().unwrap()); - map.append("foo", "bar3".parse().unwrap()); - - let vals = map.get_all("foo").iter().collect::>(); - - assert_eq!(&vals, &[&"bar1", &"bar2", &"bar3"]); - } } From 14cccb455abe1fdba9976b174b577aeb640116fa Mon Sep 17 00:00:00 2001 From: StellarisW Date: Mon, 26 May 2025 21:18:33 +0800 Subject: [PATCH 18/20] test: fix --- src/header/map.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/header/map.rs b/src/header/map.rs index dd8553f2..b8ff80ee 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -4,6 +4,7 @@ use super::name::{HdrName, HeaderName, InvalidHeaderName}; use super::HeaderValue; #[cfg(feature = "fasthttp")] use crate::ext::fasthttp::consts::is_non_append_standard_header; +#[cfg(feature = "fasthttp")] use crate::ext::fasthttp::consts::is_non_duplicate_header; #[cfg(feature = "fasthttp")] use crate::ext::fasthttp::header_name::normalize_header_key; From 3fea6e272cf660efd9713968e34dc36865e2f17d Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 28 May 2025 02:31:21 +0800 Subject: [PATCH 19/20] test: fix --- src/ext/fasthttp/consts.rs | 15 +++++++-------- src/ext/fasthttp/header_name.rs | 2 ++ src/header/map.rs | 24 ++++++++++-------------- tests/fasthttp.rs | 23 ++++++++++++++++++++++- tests/header_map_fuzz.rs | 8 +++----- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index 2c537f51..87e9fc3f 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -57,10 +57,10 @@ impl StandardHeaders { } } -pub(crate) struct NonAppendStandardHeaders(Trie); +pub(crate) struct NonAppendHeaders(Trie); -impl NonAppendStandardHeaders { - fn new() -> NonAppendStandardHeaders { +impl NonAppendHeaders { + fn new() -> NonAppendHeaders { let mut builder = TrieBuilder::new(); // 不可有多个值的特殊 std header @@ -72,7 +72,7 @@ impl NonAppendStandardHeaders { let trie = builder.build(); - NonAppendStandardHeaders(trie) + NonAppendHeaders(trie) } pub fn is_match(&self, header_name: &HeaderName) -> bool { @@ -109,8 +109,7 @@ impl NonDuplicateHeaders { lazy_static! { pub(super) static ref STANDARD_HEADERS: StandardHeaders = StandardHeaders::new(); - pub(super) static ref NON_APPEND_STANDARD_HEADERS: NonAppendStandardHeaders = - NonAppendStandardHeaders::new(); + pub(super) static ref NON_APPEND_HEADERS: NonAppendHeaders = NonAppendHeaders::new(); pub(super) static ref NON_DUPLICATE_HEADERS: NonDuplicateHeaders = NonDuplicateHeaders::new(); } @@ -120,8 +119,8 @@ pub fn is_standard_header(header_name: &HeaderName) -> bool { } /// check if is non-append standard header -pub fn is_non_append_standard_header(header_name: &HeaderName) -> bool { - NON_APPEND_STANDARD_HEADERS.is_match(header_name) +pub fn is_non_append_header(header_name: &HeaderName) -> bool { + NON_APPEND_HEADERS.is_match(header_name) } /// check if is non-duplicate header diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs index b14e33d6..a10a0605 100644 --- a/src/ext/fasthttp/header_name.rs +++ b/src/ext/fasthttp/header_name.rs @@ -7,6 +7,7 @@ const TO_LOWER: u8 = b'a' - b'A'; lazy_static::lazy_static! { static ref TO_LOWER_TABLE: [u8; 256] = { let mut a = [0u8; 256]; + #[allow(warnings)] for i in 0..256 { let mut c = i as u8; if c.is_ascii_uppercase(){ @@ -19,6 +20,7 @@ lazy_static::lazy_static! { static ref TO_UPPER_TABLE: [u8; 256] = { let mut a = [0u8; 256]; + #[allow(warnings)] for i in 0..256 { let mut c = i as u8; if c.is_ascii_lowercase(){ diff --git a/src/header/map.rs b/src/header/map.rs index b8ff80ee..da99c0dd 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -3,7 +3,7 @@ pub use self::into_header_name::IntoHeaderName; use super::name::{HdrName, HeaderName, InvalidHeaderName}; use super::HeaderValue; #[cfg(feature = "fasthttp")] -use crate::ext::fasthttp::consts::is_non_append_standard_header; +use crate::ext::fasthttp::consts::is_non_append_header; #[cfg(feature = "fasthttp")] use crate::ext::fasthttp::consts::is_non_duplicate_header; #[cfg(feature = "fasthttp")] @@ -1403,7 +1403,7 @@ impl HeaderMap { // Vacant { let _ = danger; // Make lint happy - self.try_insert_entry(hash, key.into(), value)?; + self.try_insert_entry(hash, key, value)?; let index = self.entries.len() - 1; self.indices[probe] = Pos::new(index, hash); None @@ -1412,7 +1412,7 @@ impl HeaderMap { Some(self.insert_occupied(pos, value)), // Robinhood { - self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + self.try_insert_phase_two(key, value, hash, probe, danger)?; None } )) @@ -1578,11 +1578,11 @@ impl HeaderMap { #[cfg(feature = "fasthttp")] { - if is_non_append_standard_header(&header_name) { + if is_non_append_header(&header_name) { match self.try_insert2_do(header_name, value) { - Ok(None) => return Ok(false), - Ok(Some(_)) => return Ok(false), - Err(e) => return Err(e), + Ok(None) => Ok(false), + Ok(Some(_)) => Ok(false), + Err(e) => Err(e), } } else { Ok(insert_phase_one!( @@ -1595,8 +1595,8 @@ impl HeaderMap { // Vacant { let _ = danger; - let index = self.entries.len(); self.try_insert_entry_for_append(hash, header_name, value)?; + let index = self.entries.len() - 1; self.indices[probe] = Pos::new(index, hash); false }, @@ -1718,10 +1718,8 @@ impl HeaderMap { probe: usize, danger: bool, ) -> Result { - // Push the value and get the index - let index = self.entries.len(); - self.try_insert_entry(hash, key, value)?; + let index = self.entries.len() - 1; let num_displaced = do_insert_phase_two(&mut self.indices, probe, Pos::new(index, hash)); @@ -1743,10 +1741,8 @@ impl HeaderMap { probe: usize, danger: bool, ) -> Result { - // Push the value and get the index - let index = self.entries.len(); - self.try_insert_entry_for_append(hash, key.clone(), value)?; + let index = self.entries.len() - 1; let num_displaced = do_insert_phase_two(&mut self.indices, probe, Pos::new(index, hash)); diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs index d537ca5c..b68f8cba 100644 --- a/tests/fasthttp.rs +++ b/tests/fasthttp.rs @@ -97,7 +97,7 @@ mod fasthttp_tests { } #[test] - fn test_append_special_std_header() { + fn test_append_for_non_append_header() { // test append for special std header, should equal to insert let mut header_map = HeaderMap::new(); @@ -128,6 +128,27 @@ mod fasthttp_tests { assert_eq!(header_map.get("content-length").unwrap(), "4"); } + #[test] + fn test_append_for_non_duplicate_header() { + let mut header_map = HeaderMap::new(); + + header_map.append( + HeaderName::from_bytes("content-type".as_bytes()).unwrap(), + HeaderValue::from_static("application/json"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-type").unwrap(), "application/json"); + assert_eq!(header_map.get("Content-Type").unwrap(), "application/json"); + + header_map.append( + HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), + HeaderValue::from_static("text/html"), + ); + assert_eq!(header_map.len(), 1); + assert_eq!(header_map.get("content-type").unwrap(), "text/html"); + assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + } + #[test] fn test_remove() { let mut header_map = HeaderMap::new(); diff --git a/tests/header_map_fuzz.rs b/tests/header_map_fuzz.rs index 60379555..d3359ca1 100644 --- a/tests/header_map_fuzz.rs +++ b/tests/header_map_fuzz.rs @@ -7,7 +7,7 @@ use rand::seq::SliceRandom; use rand::{Rng, SeedableRng}; #[cfg(feature = "fasthttp")] -use crate::ext::fasthttp::consts::is_non_append_standard_header; +use crate::ext::fasthttp::consts::is_non_append_header; #[cfg(feature = "fasthttp")] use http::ext::fasthttp::consts::is_non_duplicate_header; use std::collections::HashMap; @@ -200,7 +200,7 @@ impl AltMap { let mut header_name = gen_header_name(rng); loop { - if is_non_append_standard_header(&header_name) + if is_non_append_header(&header_name) || is_non_duplicate_header(&header_name) { header_name = gen_header_name(rng); @@ -220,9 +220,7 @@ impl AltMap { let mut header_name = gen_header_name(rng); loop { - if is_non_append_standard_header(&header_name) - || is_non_duplicate_header(&header_name) - { + if is_non_append_header(&header_name) || is_non_duplicate_header(&header_name) { header_name = gen_header_name(rng); } else { break; From 789a669265e43ee7f610b8fea4016388ec2871d9 Mon Sep 17 00:00:00 2001 From: StellarisW Date: Wed, 28 May 2025 04:26:09 +0800 Subject: [PATCH 20/20] =?UTF-8?q?feat:=20=E6=A0=87=E5=87=86=20header=20?= =?UTF-8?q?=E5=8F=AA=E5=AD=98=E5=9C=A8=20MIME=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ext/fasthttp/consts.rs | 32 ------- src/ext/fasthttp/header_name.rs | 49 +++++----- src/header/map.rs | 160 ++++++++++++++++++-------------- tests/fasthttp.rs | 47 +++++----- tests/header_map_fuzz.rs | 8 +- 5 files changed, 137 insertions(+), 159 deletions(-) diff --git a/src/ext/fasthttp/consts.rs b/src/ext/fasthttp/consts.rs index 87e9fc3f..a6c2887e 100644 --- a/src/ext/fasthttp/consts.rs +++ b/src/ext/fasthttp/consts.rs @@ -81,36 +81,9 @@ impl NonAppendHeaders { } } -pub(crate) struct NonDuplicateHeaders(Trie); - -impl NonDuplicateHeaders { - fn new() -> NonDuplicateHeaders { - let mut builder = TrieBuilder::new(); - - // 不可有重名的特殊 std header(大小写不敏感下只能存在一个) - builder.push(CONTENT_TYPE); - builder.push(SERVER); - builder.push(SET_COOKIE); - builder.push(CONTENT_LENGTH); - builder.push(CONNECTION); - builder.push(TRANSFER_ENCODING); - builder.push(DATE); - - let trie = builder.build(); - - NonDuplicateHeaders(trie) - } - - pub fn is_match(&self, header_name: &HeaderName) -> bool { - let lowercase_header = header_name.as_str().to_ascii_lowercase(); - self.0.exact_match(lowercase_header) - } -} - lazy_static! { pub(super) static ref STANDARD_HEADERS: StandardHeaders = StandardHeaders::new(); pub(super) static ref NON_APPEND_HEADERS: NonAppendHeaders = NonAppendHeaders::new(); - pub(super) static ref NON_DUPLICATE_HEADERS: NonDuplicateHeaders = NonDuplicateHeaders::new(); } /// check if is standard header @@ -123,11 +96,6 @@ pub fn is_non_append_header(header_name: &HeaderName) -> bool { NON_APPEND_HEADERS.is_match(header_name) } -/// check if is non-duplicate header -pub fn is_non_duplicate_header(header_name: &HeaderName) -> bool { - NON_DUPLICATE_HEADERS.is_match(header_name) -} - #[test] fn test_is_std_header() { assert!(is_standard_header(&"Content-Type".parse().unwrap())); diff --git a/src/ext/fasthttp/header_name.rs b/src/ext/fasthttp/header_name.rs index a10a0605..24157e75 100644 --- a/src/ext/fasthttp/header_name.rs +++ b/src/ext/fasthttp/header_name.rs @@ -34,26 +34,19 @@ lazy_static::lazy_static! { /// 只normalize标准header,其他自定义header保持原样透传 #[allow(warnings)] -pub(crate) fn normalize_header_key_for_std_header( - header_name: &HeaderName, - disable_normalizing: bool, -) -> HeaderName { - if disable_normalizing { - return header_name.clone(); - } - - if !is_standard_header(&(header_name.clone())) { - return header_name.clone(); +pub(crate) fn normalize_header_key_for_std_header(header_name: HeaderName) -> HeaderName { + if !is_standard_header(&header_name) { + return header_name; } - normalize_header_key(&header_name).into() + normalize_header_key(header_name).into() } -pub(crate) fn normalize_header_key(header_name: &K) -> impl Into +pub(crate) fn normalize_header_key(header_name: K) -> impl Into where - K: Into + Clone, + K: Into, { - let header_name: HeaderName = header_name.clone().into(); + let header_name: HeaderName = header_name.into(); let mut header_name_str = header_name.as_raw_str().to_string(); // let normalized_header_name = header_name.as_str(); let n = header_name_str.len(); @@ -93,45 +86,45 @@ mod tests { use std::str::FromStr; #[test] - fn test_normalize_header_key() { - let mut header_name = HeaderName::from_str("content-type").unwrap(); + fn test_normalize_header_key_for_std_header() { + let header_name = HeaderName::from_str("content-type").unwrap(); assert_eq!( - normalize_header_key_for_std_header(&mut header_name, false).as_raw_str(), + normalize_header_key_for_std_header(header_name).as_raw_str(), "Content-Type" ); - let mut header_name = HeaderName::from_str("Content-Type").unwrap(); + let header_name = HeaderName::from_str("Content-Type").unwrap(); assert_eq!( - normalize_header_key_for_std_header(&mut header_name, false).as_raw_str(), + normalize_header_key_for_std_header(header_name).as_raw_str(), "Content-Type" ); // 非标准header,不做处理 - let mut header_name = HeaderName::from_str("x-tt-agw").unwrap(); + let header_name = HeaderName::from_str("x-tt-agw").unwrap(); assert_eq!( - normalize_header_key_for_std_header(&mut header_name, false).as_raw_str(), + normalize_header_key_for_std_header(header_name).as_raw_str(), "x-tt-agw" ); } #[test] - fn test_normalize_header_key2() { - let mut header_name = HeaderName::from_str("content-type").unwrap(); + fn test_normalize_header_key() { + let header_name = HeaderName::from_str("content-type").unwrap(); assert_eq!( - normalize_header_key(&mut header_name).into().as_raw_str(), + normalize_header_key(header_name).into().as_raw_str(), "Content-Type" ); - let mut header_name = HeaderName::from_str("Content-Type").unwrap(); + let header_name = HeaderName::from_str("Content-Type").unwrap(); assert_eq!( - normalize_header_key(&mut header_name).into().as_raw_str(), + normalize_header_key(header_name).into().as_raw_str(), "Content-Type" ); // 非标准header,不做处理 - let mut header_name = HeaderName::from_str("x-tt-agw").unwrap(); + let header_name = HeaderName::from_str("x-tt-agw").unwrap(); assert_eq!( - normalize_header_key(&mut header_name).into().as_raw_str(), + normalize_header_key(header_name).into().as_raw_str(), "X-Tt-Agw" ); } diff --git a/src/header/map.rs b/src/header/map.rs index da99c0dd..cf4c22e8 100644 --- a/src/header/map.rs +++ b/src/header/map.rs @@ -5,10 +5,10 @@ use super::HeaderValue; #[cfg(feature = "fasthttp")] use crate::ext::fasthttp::consts::is_non_append_header; #[cfg(feature = "fasthttp")] -use crate::ext::fasthttp::consts::is_non_duplicate_header; -#[cfg(feature = "fasthttp")] use crate::ext::fasthttp::header_name::normalize_header_key; #[cfg(feature = "fasthttp")] +use crate::ext::fasthttp::header_name::normalize_header_key_for_std_header; +#[cfg(feature = "fasthttp")] use crate::header::map::as_header_name::Sealed; use crate::Error; #[cfg(feature = "fasthttp")] @@ -465,7 +465,7 @@ impl HeaderMap { impl HeaderMap { #[cfg(feature = "fasthttp")] fn append_to_mapped_keys(&mut self, original_key: HeaderName) { - let normalized_key = normalize_header_key(&original_key).into(); + let normalized_key = normalize_header_key(original_key.clone()).into(); match self.mapped_keys.get_mut(&normalized_key) { Some(original_keys) => original_keys.push(original_key), None => { @@ -477,8 +477,7 @@ impl HeaderMap { #[cfg(feature = "fasthttp")] fn remove_all_keys_insensitively(&mut self, original_key: &HeaderName) { - let normalized_key = normalize_header_key(&original_key).into(); - + let normalized_key = normalize_header_key(original_key.clone()).into(); if let Some(original_keys) = self.mapped_keys.remove(&normalized_key) { for key in original_keys { self.remove(key); @@ -488,8 +487,7 @@ impl HeaderMap { #[cfg(feature = "fasthttp")] fn remove_other_keys_insensitively(&mut self, original_key: &HeaderName) { - let normalized_key = normalize_header_key(&original_key).into(); - + let normalized_key = normalize_header_key(original_key.clone()).into(); if let Some(original_keys) = self.mapped_keys.remove(&normalized_key) { for key in original_keys { if key != original_key { @@ -1216,14 +1214,49 @@ impl HeaderMap { fn try_entry2(&mut self, key: K) -> Result, MaxSizeReached> where K: Hash + Into, - HeaderName: PartialEq, { // Ensure that there is space in the map self.try_reserve_one()?; + let header_name = key.into(); + + #[cfg(feature = "fasthttp")] + { + let normalized_header_key = normalize_header_key_for_std_header(header_name); + + Ok(insert_phase_one!( + self, + normalized_header_key, + probe, + pos, + hash, + danger, + Entry::Vacant(VacantEntry { + map: self, + hash, + key: normalized_header_key, + probe, + danger, + }), + Entry::Occupied(OccupiedEntry { + map: self, + index: pos, + probe, + }), + Entry::Vacant(VacantEntry { + map: self, + hash, + key: normalized_header_key, + probe, + danger, + }) + )) + } + + #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, - key, + header_name, probe, pos, hash, @@ -1231,7 +1264,7 @@ impl HeaderMap { Entry::Vacant(VacantEntry { map: self, hash, - key: key.into(), + key: header_name, probe, danger, }), @@ -1243,7 +1276,7 @@ impl HeaderMap { Entry::Vacant(VacantEntry { map: self, hash, - key: key.into(), + key: header_name, probe, danger, }) @@ -1330,15 +1363,17 @@ impl HeaderMap { fn try_insert2(&mut self, key: K, value: T) -> Result, MaxSizeReached> where K: Hash + Into, - HeaderName: PartialEq, { self.try_reserve_one()?; + let header_name = key.into(); + #[cfg(feature = "fasthttp")] { + let normalized_header_name = normalize_header_key_for_std_header(header_name); Ok(insert_phase_one!( self, - key, + normalized_header_name, probe, pos, hash, @@ -1346,16 +1381,16 @@ impl HeaderMap { // Vacant { let _ = danger; // Make lint happy - self.try_insert_entry(hash, key.into(), value)?; + self.try_insert_entry(hash, normalized_header_name, value)?; let index = self.entries.len() - 1; self.indices[probe] = Pos::new(index, hash); None }, // Occupied - Some(self.insert_occupied2(key.into(), pos, value)), + Some(self.insert_occupied2(normalized_header_name, pos, value)), // Robinhood { - self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + self.try_insert_phase_two(normalized_header_name, value, hash, probe, danger)?; None } )) @@ -1364,7 +1399,7 @@ impl HeaderMap { #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, - key, + header_name, probe, pos, hash, @@ -1373,7 +1408,7 @@ impl HeaderMap { { let _ = danger; // Make lint happy let index = self.entries.len(); - self.try_insert_entry(hash, key.into(), value)?; + self.try_insert_entry(hash, header_name, value)?; self.indices[probe] = Pos::new(index, hash); None }, @@ -1381,7 +1416,7 @@ impl HeaderMap { Some(self.insert_occupied(pos, value)), // Robinhood { - self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + self.try_insert_phase_two(header_name, value, hash, probe, danger)?; None } )) @@ -1391,37 +1426,11 @@ impl HeaderMap { fn try_insert2_do(&mut self, key: HeaderName, value: T) -> Result, MaxSizeReached> { self.try_reserve_one()?; - #[cfg(feature = "fasthttp")] - { - Ok(insert_phase_one!( - self, - key, - probe, - pos, - hash, - danger, - // Vacant - { - let _ = danger; // Make lint happy - self.try_insert_entry(hash, key, value)?; - let index = self.entries.len() - 1; - self.indices[probe] = Pos::new(index, hash); - None - }, - // Occupied - Some(self.insert_occupied(pos, value)), - // Robinhood - { - self.try_insert_phase_two(key, value, hash, probe, danger)?; - None - } - )) - } + let normalized_header_name = normalize_header_key_for_std_header(key); - #[cfg(not(feature = "fasthttp"))] Ok(insert_phase_one!( self, - key, + normalized_header_name, probe, pos, hash, @@ -1429,8 +1438,8 @@ impl HeaderMap { // Vacant { let _ = danger; // Make lint happy - let index = self.entries.len(); - self.try_insert_entry(hash, key.into(), value)?; + self.try_insert_entry(hash, normalized_header_name, value)?; + let index = self.entries.len() - 1; self.indices[probe] = Pos::new(index, hash); None }, @@ -1438,7 +1447,7 @@ impl HeaderMap { Some(self.insert_occupied(pos, value)), // Robinhood { - self.try_insert_phase_two(key.into(), value, hash, probe, danger)?; + self.try_insert_phase_two(normalized_header_name, value, hash, probe, danger)?; None } )) @@ -1585,9 +1594,11 @@ impl HeaderMap { Err(e) => Err(e), } } else { + let normalized_header_name = normalize_header_key_for_std_header(header_name); + Ok(insert_phase_one!( self, - header_name, + normalized_header_name, probe, pos, hash, @@ -1595,8 +1606,8 @@ impl HeaderMap { // Vacant { let _ = danger; - self.try_insert_entry_for_append(hash, header_name, value)?; - let index = self.entries.len() - 1; + let index = self.entries.len(); + self.try_insert_entry_for_append(hash, normalized_header_name, value)?; self.indices[probe] = Pos::new(index, hash); false }, @@ -1608,7 +1619,7 @@ impl HeaderMap { // Robinhood { self.try_insert_phase_two_for_append( - header_name, + normalized_header_name, value, hash, probe, @@ -1662,19 +1673,25 @@ impl HeaderMap { } #[cfg(feature = "fasthttp")] - match self.find2(key) { - x @ Some(_) => x, - None => match self.mapped_keys.get(&normalize_header_key(key).into()) { - Some(original_keys) => { - for original_key in original_keys { - if original_key != key { - return self.find2::(original_key); + { + let normalized_header_key = normalize_header_key_for_std_header(key.clone().into()); + match self.find2::(&normalized_header_key) { + x @ Some(_) => x, + None => match self + .mapped_keys + .get(&normalize_header_key(normalized_header_key.clone()).into()) + { + Some(original_keys) => { + for original_key in original_keys { + if original_key != key { + return self.find2::(original_key); + } } + None } - None - } - None => None, - }, + None => None, + }, + } } #[cfg(not(feature = "fasthttp"))] @@ -1741,8 +1758,8 @@ impl HeaderMap { probe: usize, danger: bool, ) -> Result { + let index = self.entries.len(); self.try_insert_entry_for_append(hash, key.clone(), value)?; - let index = self.entries.len() - 1; let num_displaced = do_insert_phase_two(&mut self.indices, probe, Pos::new(index, hash)); @@ -1785,9 +1802,11 @@ impl HeaderMap { Err(_) => return None, }; - let normalized_key = normalize_header_key(&header_name).into(); + let normalized_header_name = normalize_header_key_for_std_header(header_name); + + let normalized_key = normalize_header_key(&normalized_header_name).into(); - match header_name.find(self) { + match normalized_header_name.find(self) { Some((probe, idx)) => { if let Some(links) = self.entries[idx].links { self.remove_all_extra_values(links.next); @@ -1796,7 +1815,7 @@ impl HeaderMap { let entry = self.remove_found(probe, idx); if let Some(original_keys) = self.mapped_keys.get_mut(&normalized_key) { - original_keys.retain(|x| x != &header_name); + original_keys.retain(|x| x != &normalized_header_name); } Some(entry.value) } @@ -1975,9 +1994,6 @@ impl HeaderMap { return Err(MaxSizeReached::new()); } - if is_non_duplicate_header(&key) { - self.remove_all_keys_insensitively(&key); - } self.append_to_mapped_keys(key.clone()); self.entries.push(Bucket { diff --git a/tests/fasthttp.rs b/tests/fasthttp.rs index b68f8cba..d837f1b0 100644 --- a/tests/fasthttp.rs +++ b/tests/fasthttp.rs @@ -7,40 +7,34 @@ mod fasthttp_tests { let mut header_map = HeaderMap::new(); header_map.insert( - HeaderName::from_bytes("content-type".as_bytes()).unwrap(), - HeaderValue::from_static("application/json"), + HeaderName::from_bytes("foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar1"), ); assert_eq!(header_map.len(), 1); - assert_eq!(header_map.get("content-type").unwrap(), "application/json"); - assert_eq!(header_map.get("Content-Type").unwrap(), "application/json"); + assert_eq!(header_map.get("foo").unwrap(), "bar1"); + assert_eq!(header_map.get("Foo").unwrap(), "bar1"); header_map.insert( - HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), - HeaderValue::from_static("text/html"), + HeaderName::from_bytes("Foo".as_bytes()).unwrap(), + HeaderValue::from_static("bar2"), ); assert_eq!(header_map.len(), 1); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + assert_eq!(header_map.get("foo").unwrap(), "bar2"); + assert_eq!(header_map.get("Foo").unwrap(), "bar2"); header_map.insert( - HeaderName::from_bytes("content-length".as_bytes()).unwrap(), - HeaderValue::from_static("-1"), + HeaderName::from_bytes("foo1".as_bytes()).unwrap(), + HeaderValue::from_static("bar1"), ); assert_eq!(header_map.len(), 2); let mut iter = header_map.iter(); let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("Content-Type", "text/html") - ); + assert_eq!((k.as_raw_str(), v.to_str().unwrap()), ("Foo", "bar2")); let (k, v) = iter.next().unwrap(); - assert_eq!( - (k.as_raw_str(), v.to_str().unwrap()), - ("content-length", "-1") - ); + assert_eq!((k.as_raw_str(), v.to_str().unwrap()), ("foo1", "bar1")); } // tes append for non-std headers @@ -129,7 +123,7 @@ mod fasthttp_tests { } #[test] - fn test_append_for_non_duplicate_header() { + fn test_append_for_std_header() { let mut header_map = HeaderMap::new(); header_map.append( @@ -144,9 +138,18 @@ mod fasthttp_tests { HeaderName::from_bytes("Content-Type".as_bytes()).unwrap(), HeaderValue::from_static("text/html"), ); - assert_eq!(header_map.len(), 1); - assert_eq!(header_map.get("content-type").unwrap(), "text/html"); - assert_eq!(header_map.get("Content-Type").unwrap(), "text/html"); + assert_eq!(header_map.len(), 2); + let mut iter = header_map.iter(); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("Content-Type", "application/json") + ); + let (k, v) = iter.next().unwrap(); + assert_eq!( + (k.as_raw_str(), v.to_str().unwrap()), + ("Content-Type", "text/html") + ); } #[test] diff --git a/tests/header_map_fuzz.rs b/tests/header_map_fuzz.rs index d3359ca1..cc6a97b9 100644 --- a/tests/header_map_fuzz.rs +++ b/tests/header_map_fuzz.rs @@ -9,7 +9,7 @@ use rand::{Rng, SeedableRng}; #[cfg(feature = "fasthttp")] use crate::ext::fasthttp::consts::is_non_append_header; #[cfg(feature = "fasthttp")] -use http::ext::fasthttp::consts::is_non_duplicate_header; +use http::ext::fasthttp::consts::is_standard_header; use std::collections::HashMap; #[cfg(not(miri))] @@ -200,9 +200,7 @@ impl AltMap { let mut header_name = gen_header_name(rng); loop { - if is_non_append_header(&header_name) - || is_non_duplicate_header(&header_name) - { + if is_non_append_header(&header_name) || is_standard_header(&header_name) { header_name = gen_header_name(rng); } else { break; @@ -220,7 +218,7 @@ impl AltMap { let mut header_name = gen_header_name(rng); loop { - if is_non_append_header(&header_name) || is_non_duplicate_header(&header_name) { + if is_non_append_header(&header_name) || is_standard_header(&header_name) { header_name = gen_header_name(rng); } else { break;