diff --git a/Cargo.toml b/Cargo.toml index 1761da5e..95ae3e9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "headers-accept-encoding" -version = "1.0.1" # don't forget to update html_root_url +version = "1.1.0" # don't forget to update html_root_url description = "Hypper typed HTTP headers with Accept-Encoding + zstd support" license = "MIT" readme = "README.md" @@ -9,18 +9,13 @@ repository = "https://github.com/static-web-server/headers-accept-encoding" authors = ["Sean McArthur ", "Jose Quintana "] keywords = ["http", "headers", "hyper", "hyperium"] categories = ["web-programming"] -rust-version = "1.56" -[workspace] -members = [ - "./", - "headers-core", -] +rust-version = "1.56" [dependencies] -http = "0.2.0" -headers-core = { version = "0.2" } -base64 = "0.21.3" +http = "1.0.0" +headers-core = { version = "0.3" } +base64 = "0.21.5" bytes = "1" mime = "0.3.14" sha1 = "0.10" diff --git a/LICENSE b/LICENSE index aa33b8e7..985a757d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Sean McArthur +Copyright (c) 2014-2023 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/headers-core/Cargo.toml b/headers-core/Cargo.toml index 22c011f1..60ecec29 100644 --- a/headers-core/Cargo.toml +++ b/headers-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "headers-core" -version = "0.2.0" # don't forget to update html_root_url +version = "0.3.0" # don't forget to update html_root_url description = "typed HTTP headers core trait" license = "MIT" readme = "README.md" @@ -10,4 +10,4 @@ authors = ["Sean McArthur "] keywords = ["http", "headers", "hyper", "hyperium"] [dependencies] -http = "0.2.0" +http = "1.0.0" diff --git a/headers-core/src/lib.rs b/headers-core/src/lib.rs index 92e3d15e..5692b65a 100644 --- a/headers-core/src/lib.rs +++ b/headers-core/src/lib.rs @@ -1,7 +1,7 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] -#![doc(html_root_url = "https://docs.rs/headers-core/0.2.0")] +#![doc(html_root_url = "https://docs.rs/headers-core/0.3.0")] //! # headers-core //! diff --git a/src/common/authorization.rs b/src/common/authorization.rs index e62f2fd9..bb2c35ec 100644 --- a/src/common/authorization.rs +++ b/src/common/authorization.rs @@ -82,9 +82,9 @@ impl ::Header for Authorization { .next() .and_then(|val| { let slice = val.as_bytes(); - if slice.starts_with(C::SCHEME.as_bytes()) - && slice.len() > C::SCHEME.len() + if slice.len() > C::SCHEME.len() && slice[C::SCHEME.len()] == b' ' + && slice[..C::SCHEME.len()].eq_ignore_ascii_case(C::SCHEME.as_bytes()) { C::decode(val).map(Authorization) } else { @@ -151,7 +151,7 @@ impl Credentials for Basic { fn decode(value: &HeaderValue) -> Option { debug_assert!( - value.as_bytes().starts_with(b"Basic "), + value.as_bytes()[..Self::SCHEME.len()].eq_ignore_ascii_case(Self::SCHEME.as_bytes()), "HeaderValue to decode should start with \"Basic ..\", received = {:?}", value, ); @@ -186,7 +186,7 @@ pub struct Bearer(HeaderValueString); impl Bearer { /// View the token part as a `&str`. pub fn token(&self) -> &str { - &self.0.as_str()["Bearer ".len()..] + self.0.as_str()["Bearer ".len()..].trim_start() } } @@ -195,7 +195,7 @@ impl Credentials for Bearer { fn decode(value: &HeaderValue) -> Option { debug_assert!( - value.as_bytes().starts_with(b"Bearer "), + value.as_bytes()[..Self::SCHEME.len()].eq_ignore_ascii_case(Self::SCHEME.as_bytes()), "HeaderValue to decode should start with \"Bearer ..\", received = {:?}", value, ); @@ -252,6 +252,22 @@ mod tests { assert_eq!(auth.0.password(), "open sesame"); } + #[test] + fn basic_decode_case_insensitive() { + let auth: Authorization = + test_decode(&["basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="]).unwrap(); + assert_eq!(auth.0.username(), "Aladdin"); + assert_eq!(auth.0.password(), "open sesame"); + } + + #[test] + fn basic_decode_extra_whitespaces() { + let auth: Authorization = + test_decode(&["Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="]).unwrap(); + assert_eq!(auth.0.username(), "Aladdin"); + assert_eq!(auth.0.password(), "open sesame"); + } + #[test] fn basic_decode_no_password() { let auth: Authorization = test_decode(&["Basic QWxhZGRpbjo="]).unwrap(); @@ -273,6 +289,18 @@ mod tests { let auth: Authorization = test_decode(&["Bearer fpKL54jvWmEGVoRdCNjG"]).unwrap(); assert_eq!(auth.0.token().as_bytes(), b"fpKL54jvWmEGVoRdCNjG"); } + + #[test] + fn bearer_decode_case_insensitive() { + let auth: Authorization = test_decode(&["bearer fpKL54jvWmEGVoRdCNjG"]).unwrap(); + assert_eq!(auth.0.token().as_bytes(), b"fpKL54jvWmEGVoRdCNjG"); + } + + #[test] + fn bearer_decode_extra_whitespaces() { + let auth: Authorization = test_decode(&["Bearer fpKL54jvWmEGVoRdCNjG"]).unwrap(); + assert_eq!(auth.0.token().as_bytes(), b"fpKL54jvWmEGVoRdCNjG"); + } } //bench_header!(raw, Authorization, { vec![b"foo bar baz".to_vec()] }); diff --git a/src/common/cache_control.rs b/src/common/cache_control.rs index afb69249..7d5db055 100644 --- a/src/common/cache_control.rs +++ b/src/common/cache_control.rs @@ -59,6 +59,7 @@ impl Flags { const PRIVATE: Self = Self { bits: 0b001000000 }; const PROXY_REVALIDATE: Self = Self { bits: 0b010000000 }; const IMMUTABLE: Self = Self { bits: 0b100000000 }; + const MUST_UNDERSTAND: Self = Self { bits: 0b1000000000 }; fn empty() -> Self { Self { bits: 0 } @@ -121,6 +122,10 @@ impl CacheControl { pub fn immutable(&self) -> bool { self.flags.contains(Flags::IMMUTABLE) } + /// Check if the `must_understand` directive is set. + pub fn must_understand(&self) -> bool { + self.flags.contains(Flags::MUST_UNDERSTAND) + } /// Get the value of the `max-age` directive if set. pub fn max_age(&self) -> Option { @@ -186,6 +191,11 @@ impl CacheControl { self } + /// Set the `must_understand` directive. + pub fn with_must_understand(mut self) -> Self { + self.flags.insert(Flags::MUST_UNDERSTAND); + self + } /// Set the `max-age` directive. pub fn with_max_age(mut self, duration: Duration) -> Self { self.max_age = Some(duration.into()); @@ -258,6 +268,9 @@ impl FromIterator for FromIter { Directive::MustRevalidate => { cc.flags.insert(Flags::MUST_REVALIDATE); } + Directive::MustUnderstand => { + cc.flags.insert(Flags::MUST_UNDERSTAND); + } Directive::Public => { cc.flags.insert(Flags::PUBLIC); } @@ -310,6 +323,7 @@ impl<'a> fmt::Display for Fmt<'a> { if_flag(Flags::PUBLIC, Directive::Public), if_flag(Flags::PRIVATE, Directive::Private), if_flag(Flags::IMMUTABLE, Directive::Immutable), + if_flag(Flags::MUST_UNDERSTAND, Directive::MustUnderstand), if_flag(Flags::PROXY_REVALIDATE, Directive::ProxyRevalidate), self.0 .max_age @@ -355,6 +369,7 @@ enum Directive { // response directives MustRevalidate, + MustUnderstand, Public, Private, Immutable, @@ -376,6 +391,7 @@ impl fmt::Display for Directive { Directive::MinFresh(secs) => return write!(f, "min-fresh={}", secs), Directive::MustRevalidate => "must-revalidate", + Directive::MustUnderstand => "must-understand", Directive::Public => "public", Directive::Private => "private", Directive::Immutable => "immutable", @@ -399,6 +415,7 @@ impl FromStr for KnownDirective { "public" => Directive::Public, "private" => Directive::Private, "immutable" => Directive::Immutable, + "must-understand" => Directive::MustUnderstand, "proxy-revalidate" => Directive::ProxyRevalidate, "" => return Err(()), _ => match s.find('=') { @@ -472,6 +489,18 @@ mod tests { assert!(cc.immutable()); } + #[test] + fn test_must_understand() { + let cc = CacheControl::new().with_must_understand(); + let headers = test_encode(cc.clone()); + assert_eq!(headers["cache-control"], "must-understand"); + assert_eq!( + test_decode::(&["must-understand"]).unwrap(), + cc + ); + assert!(cc.must_understand()); + } + #[test] fn test_parse_bad_syntax() { assert_eq!(test_decode::(&["max-age=lolz"]), None); diff --git a/src/common/range.rs b/src/common/range.rs index 29cc79d3..6d35936d 100644 --- a/src/common/range.rs +++ b/src/common/range.rs @@ -48,29 +48,52 @@ impl Range { /// Creates a `Range` header from bounds. pub fn bytes(bounds: impl RangeBounds) -> Result { let v = match (bounds.start_bound(), bounds.end_bound()) { - (Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end), - (Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1), (Bound::Included(start), Bound::Included(end)) => format!("bytes={}-{}", start, end), (Bound::Included(start), Bound::Excluded(&end)) => { format!("bytes={}-{}", start, end - 1) } (Bound::Included(start), Bound::Unbounded) => format!("bytes={}-", start), + // These do not directly translate. + //(Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end), + //(Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1), _ => return Err(InvalidRange { _inner: () }), }; Ok(Range(::HeaderValue::from_str(&v).unwrap())) } - /// Iterate the range sets as a tuple of bounds. - pub fn iter<'a>(&'a self) -> impl Iterator, Bound)> + 'a { + /// Iterate the range sets as a tuple of bounds, if valid with length. + /// + /// The length of the content is passed as an argument, and all ranges + /// that can be satisfied will be iterated. + pub fn satisfiable_ranges<'a>( + &'a self, + len: u64, + ) -> impl Iterator, Bound)> + 'a { let s = self .0 .to_str() .expect("valid string checked in Header::decode()"); - s["bytes=".len()..].split(',').filter_map(|spec| { + s["bytes=".len()..].split(',').filter_map(move |spec| { let mut iter = spec.trim().splitn(2, '-'); - Some((parse_bound(iter.next()?)?, parse_bound(iter.next()?)?)) + let start = parse_bound(iter.next()?)?; + let end = parse_bound(iter.next()?)?; + + // Unbounded ranges in HTTP are actually a suffix + // For example, `-100` means the last 100 bytes. + if let Bound::Unbounded = start { + if let Bound::Included(end) = end { + if len < end { + // Last N bytes is larger than available! + return None; + } + return Some((Bound::Included(len - end), Bound::Unbounded)); + } + // else fall through + } + + Some((start, end)) }) } } @@ -416,3 +439,17 @@ fn test_byte_range_spec_to_satisfiable_range() { bench_header!(bytes_multi, Range, { vec![b"bytes=1-1001,2001-3001,10001-".to_vec()]}); bench_header!(custom_unit, Range, { vec![b"other=0-100000".to_vec()]}); */ + +#[test] +fn test_to_satisfiable_range_suffix() { + let range = super::test_decode::(&["bytes=-100"]).unwrap(); + let bounds = range.satisfiable_ranges(350).next().unwrap(); + assert_eq!(bounds, (Bound::Included(250), Bound::Unbounded)); +} + +#[test] +fn test_to_unsatisfiable_range_suffix() { + let range = super::test_decode::(&["bytes=-350"]).unwrap(); + let bounds = range.satisfiable_ranges(100).next(); + assert_eq!(bounds, None); +} diff --git a/src/lib.rs b/src/lib.rs index 09b8e203..dc809be9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] #![cfg_attr(all(test, feature = "nightly"), feature(test))] -#![doc(html_root_url = "https://docs.rs/headers/0.3.9")] +#![doc(html_root_url = "https://docs.rs/headers/0.4.0")] //! # Typed HTTP Headers //!