diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/headers/src/util | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/headers/src/util')
-rw-r--r-- | third_party/rust/headers/src/util/csv.rs | 38 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/entity.rs | 357 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/flat_csv.rs | 199 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/fmt.rs | 11 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/http_date.rs | 152 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/iter.rs | 11 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/mod.rs | 87 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/seconds.rs | 67 | ||||
-rw-r--r-- | third_party/rust/headers/src/util/value_string.rs | 93 |
9 files changed, 1015 insertions, 0 deletions
diff --git a/third_party/rust/headers/src/util/csv.rs b/third_party/rust/headers/src/util/csv.rs new file mode 100644 index 0000000000..2d0a8ed68d --- /dev/null +++ b/third_party/rust/headers/src/util/csv.rs @@ -0,0 +1,38 @@ +use std::fmt; + +/// Reads a comma-delimited raw header into a Vec. +pub(crate) fn from_comma_delimited<'i, I, T, E>(values: &mut I) -> Result<E, ::Error> +where + I: Iterator<Item = &'i ::HeaderValue>, + T: ::std::str::FromStr, + E: ::std::iter::FromIterator<T>, +{ + values + .flat_map(|value| { + value.to_str().into_iter().flat_map(|string| { + string + .split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y), + }) + .map(|x| x.parse().map_err(|_| ::Error::invalid())) + }) + }) + .collect() +} + +/// Format an array into a comma-delimited string. +pub(crate) fn fmt_comma_delimited<T: fmt::Display>( + f: &mut fmt::Formatter, + mut iter: impl Iterator<Item = T>, +) -> fmt::Result { + if let Some(part) = iter.next() { + fmt::Display::fmt(&part, f)?; + } + for part in iter { + f.write_str(", ")?; + fmt::Display::fmt(&part, f)?; + } + Ok(()) +} diff --git a/third_party/rust/headers/src/util/entity.rs b/third_party/rust/headers/src/util/entity.rs new file mode 100644 index 0000000000..c966b6fe96 --- /dev/null +++ b/third_party/rust/headers/src/util/entity.rs @@ -0,0 +1,357 @@ +use std::fmt; + +use super::{FlatCsv, IterExt}; +use HeaderValue; + +/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) +/// +/// An entity tag consists of a string enclosed by two literal double quotes. +/// Preceding the first double quote is an optional weakness indicator, +/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and `W/"xyzzy"`. +/// +/// # ABNF +/// +/// ```text +/// entity-tag = [ weak ] opaque-tag +/// weak = %x57.2F ; "W/", case-sensitive +/// opaque-tag = DQUOTE *etagc DQUOTE +/// etagc = %x21 / %x23-7E / obs-text +/// ; VCHAR except double quotes, plus obs-text +/// ``` +/// +/// # Comparison +/// To check if two entity tags are equivalent in an application always use the `strong_eq` or +/// `weak_eq` methods based on the context of the Tag. Only use `==` to check if two tags are +/// identical. +/// +/// The example below shows the results for a set of entity-tag pairs and +/// both the weak and strong comparison function results: +/// +/// | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | +/// |---------|---------|-------------------|-----------------| +/// | `W/"1"` | `W/"1"` | no match | match | +/// | `W/"1"` | `W/"2"` | no match | no match | +/// | `W/"1"` | `"1"` | no match | match | +/// | `"1"` | `"1"` | match | match | +#[derive(Clone, Eq, PartialEq)] +pub(crate) struct EntityTag<T = HeaderValue>(T); + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum EntityTagRange { + Any, + Tags(FlatCsv), +} + +// ===== impl EntityTag ===== + +impl<T: AsRef<[u8]>> EntityTag<T> { + /// Get the tag. + pub(crate) fn tag(&self) -> &[u8] { + let bytes = self.0.as_ref(); + let end = bytes.len() - 1; + if bytes[0] == b'W' { + // W/"<tag>" + &bytes[3..end] + } else { + // "<tag>" + &bytes[1..end] + } + } + + /// Return if this is a "weak" tag. + pub(crate) fn is_weak(&self) -> bool { + self.0.as_ref()[0] == b'W' + } + + /// For strong comparison two entity-tags are equivalent if both are not weak and their + /// opaque-tags match character-by-character. + pub(crate) fn strong_eq<R>(&self, other: &EntityTag<R>) -> bool + where + R: AsRef<[u8]>, + { + !self.is_weak() && !other.is_weak() && self.tag() == other.tag() + } + + /// For weak comparison two entity-tags are equivalent if their + /// opaque-tags match character-by-character, regardless of either or + /// both being tagged as "weak". + pub(crate) fn weak_eq<R>(&self, other: &EntityTag<R>) -> bool + where + R: AsRef<[u8]>, + { + self.tag() == other.tag() + } + + /// The inverse of `EntityTag.strong_eq()`. + #[cfg(test)] + pub(crate) fn strong_ne(&self, other: &EntityTag) -> bool { + !self.strong_eq(other) + } + + /// The inverse of `EntityTag.weak_eq()`. + #[cfg(test)] + pub(crate) fn weak_ne(&self, other: &EntityTag) -> bool { + !self.weak_eq(other) + } + + pub(crate) fn parse(src: T) -> Option<Self> { + let slice = src.as_ref(); + let length = slice.len(); + + // Early exits if it doesn't terminate in a DQUOTE. + if length < 2 || slice[length - 1] != b'"' { + return None; + } + + let start = match slice[0] { + // "<tag>" + b'"' => 1, + // W/"<tag>" + b'W' => { + if length >= 4 && slice[1] == b'/' && slice[2] == b'"' { + 3 + } else { + return None; + } + } + _ => return None, + }; + + if check_slice_validity(&slice[start..length - 1]) { + Some(EntityTag(src)) + } else { + None + } + } +} + +impl EntityTag { + /* + /// Constructs a new EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn new(weak: bool, tag: String) -> EntityTag { + assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); + EntityTag { weak: weak, tag: tag } + } + + /// Constructs a new weak EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn weak(tag: String) -> EntityTag { + EntityTag::new(true, tag) + } + + /// Constructs a new strong EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn strong(tag: String) -> EntityTag { + EntityTag::new(false, tag) + } + */ + + #[cfg(test)] + pub fn from_static(bytes: &'static str) -> EntityTag { + let val = HeaderValue::from_static(bytes); + match EntityTag::from_val(&val) { + Some(tag) => tag, + None => { + panic!("invalid static string for EntityTag: {:?}", bytes); + } + } + } + + pub(crate) fn from_owned(val: HeaderValue) -> Option<EntityTag> { + EntityTag::parse(val.as_bytes())?; + Some(EntityTag(val)) + } + + pub(crate) fn from_val(val: &HeaderValue) -> Option<EntityTag> { + EntityTag::parse(val.as_bytes()).map(|_entity| { + EntityTag(val.clone()) + }) + } +} + +impl<T: fmt::Debug> fmt::Debug for EntityTag<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl super::TryFromValues for EntityTag { + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i HeaderValue>, + { + values + .just_one() + .and_then(EntityTag::from_val) + .ok_or_else(::Error::invalid) + } +} + +impl From<EntityTag> for HeaderValue { + fn from(tag: EntityTag) -> HeaderValue { + tag.0 + } +} + +impl<'a> From<&'a EntityTag> for HeaderValue { + fn from(tag: &'a EntityTag) -> HeaderValue { + tag.0.clone() + } +} + +/// check that each char in the slice is either: +/// 1. `%x21`, or +/// 2. in the range `%x23` to `%x7E`, or +/// 3. above `%x80` +fn check_slice_validity(slice: &[u8]) -> bool { + slice.iter().all(|&c| { + // HeaderValue already validates that this doesnt contain control + // characters, so we only need to look for DQUOTE (`"`). + // + // The debug_assert is just in case we use check_slice_validity in + // some new context that didnt come from a HeaderValue. + debug_assert!( + (c >= b'\x21' && c <= b'\x7e') | (c >= b'\x80'), + "EntityTag expects HeaderValue to have check for control characters" + ); + c != b'"' + }) +} + +// ===== impl EntityTagRange ===== + +impl EntityTagRange { + pub(crate) fn matches_strong(&self, entity: &EntityTag) -> bool { + self.matches_if(entity, |a, b| a.strong_eq(b)) + } + + pub(crate) fn matches_weak(&self, entity: &EntityTag) -> bool { + self.matches_if(entity, |a, b| a.weak_eq(b)) + } + + fn matches_if<F>(&self, entity: &EntityTag, func: F) -> bool + where + F: Fn(&EntityTag<&str>, &EntityTag) -> bool, + { + match *self { + EntityTagRange::Any => true, + EntityTagRange::Tags(ref tags) => { + tags.iter() + .flat_map(EntityTag::<&str>::parse) + .any(|tag| func(&tag, entity)) + }, + } + } +} + +impl super::TryFromValues for EntityTagRange { + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i HeaderValue>, + { + let flat = FlatCsv::try_from_values(values)?; + if flat.value == "*" { + Ok(EntityTagRange::Any) + } else { + Ok(EntityTagRange::Tags(flat)) + } + } +} + +impl<'a> From<&'a EntityTagRange> for HeaderValue { + fn from(tag: &'a EntityTagRange) -> HeaderValue { + match *tag { + EntityTagRange::Any => HeaderValue::from_static("*"), + EntityTagRange::Tags(ref tags) => tags.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(slice: &[u8]) -> Option<EntityTag> { + let val = HeaderValue::from_bytes(slice).ok()?; + EntityTag::from_val(&val) + } + + #[test] + fn test_etag_parse_success() { + // Expected success + let tag = parse(b"\"foobar\"").unwrap(); + assert!(!tag.is_weak()); + assert_eq!(tag.tag(), b"foobar"); + + let weak = parse(b"W/\"weaktag\"").unwrap(); + assert!(weak.is_weak()); + assert_eq!(weak.tag(), b"weaktag"); + } + + #[test] + fn test_etag_parse_failures() { + // Expected failures + macro_rules! fails { + ($slice:expr) => { + assert_eq!(parse($slice), None); + }; + } + + fails!(b"no-dquote"); + fails!(b"w/\"the-first-w-is-case sensitive\""); + fails!(b"W/\""); + fails!(b""); + fails!(b"\"unmatched-dquotes1"); + fails!(b"unmatched-dquotes2\""); + fails!(b"\"inner\"quotes\""); + } + + /* + #[test] + fn test_etag_fmt() { + assert_eq!(format!("{}", EntityTag::strong("foobar".to_owned())), "\"foobar\""); + assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); + assert_eq!(format!("{}", EntityTag::weak("weak-etag".to_owned())), "W/\"weak-etag\""); + assert_eq!(format!("{}", EntityTag::weak("\u{0065}".to_owned())), "W/\"\x65\""); + assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); + } + */ + + #[test] + fn test_cmp() { + // | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | + // |---------|---------|-------------------|-----------------| + // | `W/"1"` | `W/"1"` | no match | match | + // | `W/"1"` | `W/"2"` | no match | no match | + // | `W/"1"` | `"1"` | no match | match | + // | `"1"` | `"1"` | match | match | + let mut etag1 = EntityTag::from_static("W/\"1\""); + let mut etag2 = etag1.clone(); + assert!(!etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + + etag2 = EntityTag::from_static("W/\"2\""); + assert!(!etag1.strong_eq(&etag2)); + assert!(!etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(etag1.weak_ne(&etag2)); + + etag2 = EntityTag::from_static("\"1\""); + assert!(!etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + + etag1 = EntityTag::from_static("\"1\""); + assert!(etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(!etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + } +} diff --git a/third_party/rust/headers/src/util/flat_csv.rs b/third_party/rust/headers/src/util/flat_csv.rs new file mode 100644 index 0000000000..099b034212 --- /dev/null +++ b/third_party/rust/headers/src/util/flat_csv.rs @@ -0,0 +1,199 @@ +use std::fmt; +use std::iter::FromIterator; +use std::marker::PhantomData; + +use bytes::BytesMut; +use util::TryFromValues; +use HeaderValue; + +// A single `HeaderValue` that can flatten multiple values with commas. +#[derive(Clone, PartialEq, Eq, Hash)] +pub(crate) struct FlatCsv<Sep = Comma> { + pub(crate) value: HeaderValue, + _marker: PhantomData<Sep>, +} + +pub(crate) trait Separator { + const BYTE: u8; + const CHAR: char; +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Comma {} + +impl Separator for Comma { + const BYTE: u8 = b','; + const CHAR: char = ','; +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum SemiColon {} + +impl Separator for SemiColon { + const BYTE: u8 = b';'; + const CHAR: char = ';'; +} + +impl<Sep: Separator> FlatCsv<Sep> { + pub(crate) fn iter(&self) -> impl Iterator<Item = &str> { + self.value.to_str().ok().into_iter().flat_map(|value_str| { + let mut in_quotes = false; + value_str + .split(move |c| { + if in_quotes { + if c == '"' { + in_quotes = false; + } + false // dont split + } else { + if c == Sep::CHAR { + true // split + } else { + if c == '"' { + in_quotes = true; + } + false // dont split + } + } + }) + .map(|item| item.trim()) + }) + } +} + +impl<Sep: Separator> TryFromValues for FlatCsv<Sep> { + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i HeaderValue>, + { + let flat = values.collect(); + Ok(flat) + } +} + +impl<Sep> From<HeaderValue> for FlatCsv<Sep> { + fn from(value: HeaderValue) -> Self { + FlatCsv { + value, + _marker: PhantomData, + } + } +} + +impl<'a, Sep> From<&'a FlatCsv<Sep>> for HeaderValue { + fn from(flat: &'a FlatCsv<Sep>) -> HeaderValue { + flat.value.clone() + } +} + +impl<Sep> fmt::Debug for FlatCsv<Sep> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.value, f) + } +} + +impl<'a, Sep: Separator> FromIterator<&'a HeaderValue> for FlatCsv<Sep> { + fn from_iter<I>(iter: I) -> Self + where + I: IntoIterator<Item = &'a HeaderValue>, + { + let mut values = iter.into_iter(); + + // Common case is there is only 1 value, optimize for that + if let (1, Some(1)) = values.size_hint() { + return values + .next() + .expect("size_hint claimed 1 item") + .clone() + .into(); + } + + // Otherwise, there are multiple, so this should merge them into 1. + let mut buf = values + .next() + .cloned() + .map(|val| BytesMut::from(val.as_bytes())) + .unwrap_or_else(|| BytesMut::new()); + + for val in values { + buf.extend_from_slice(&[Sep::BYTE, b' ']); + buf.extend_from_slice(val.as_bytes()); + } + + let val = + HeaderValue::from_maybe_shared(buf.freeze()).expect("comma separated HeaderValues are valid"); + + val.into() + } +} + +// TODO: would be great if there was a way to de-dupe these with above +impl<Sep: Separator> FromIterator<HeaderValue> for FlatCsv<Sep> { + fn from_iter<I>(iter: I) -> Self + where + I: IntoIterator<Item = HeaderValue>, + { + let mut values = iter.into_iter(); + + // Common case is there is only 1 value, optimize for that + if let (1, Some(1)) = values.size_hint() { + return values.next().expect("size_hint claimed 1 item").into(); + } + + // Otherwise, there are multiple, so this should merge them into 1. + let mut buf = values + .next() + .map(|val| BytesMut::from(val.as_bytes())) + .unwrap_or_else(|| BytesMut::new()); + + for val in values { + buf.extend_from_slice(&[Sep::BYTE, b' ']); + buf.extend_from_slice(val.as_bytes()); + } + + let val = + HeaderValue::from_maybe_shared(buf.freeze()).expect("comma separated HeaderValues are valid"); + + val.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn comma() { + let val = HeaderValue::from_static("aaa, b; bb, ccc"); + let csv = FlatCsv::<Comma>::from(val); + + let mut values = csv.iter(); + assert_eq!(values.next(), Some("aaa")); + assert_eq!(values.next(), Some("b; bb")); + assert_eq!(values.next(), Some("ccc")); + assert_eq!(values.next(), None); + } + + #[test] + fn semicolon() { + let val = HeaderValue::from_static("aaa; b, bb; ccc"); + let csv = FlatCsv::<SemiColon>::from(val); + + let mut values = csv.iter(); + assert_eq!(values.next(), Some("aaa")); + assert_eq!(values.next(), Some("b, bb")); + assert_eq!(values.next(), Some("ccc")); + assert_eq!(values.next(), None); + } + + #[test] + fn quoted_text() { + let val = HeaderValue::from_static("foo=\"bar,baz\", sherlock=holmes"); + let csv = FlatCsv::<Comma>::from(val); + + let mut values = csv.iter(); + assert_eq!(values.next(), Some("foo=\"bar,baz\"")); + assert_eq!(values.next(), Some("sherlock=holmes")); + assert_eq!(values.next(), None); + } +} diff --git a/third_party/rust/headers/src/util/fmt.rs b/third_party/rust/headers/src/util/fmt.rs new file mode 100644 index 0000000000..c535c72925 --- /dev/null +++ b/third_party/rust/headers/src/util/fmt.rs @@ -0,0 +1,11 @@ +use std::fmt::Display; + +use HeaderValue; + +pub(crate) fn fmt<T: Display>(fmt: T) -> HeaderValue { + let s = fmt.to_string(); + match HeaderValue::from_maybe_shared(s) { + Ok(val) => val, + Err(err) => panic!("illegal HeaderValue; error = {:?}, fmt = \"{}\"", err, fmt), + } +} diff --git a/third_party/rust/headers/src/util/http_date.rs b/third_party/rust/headers/src/util/http_date.rs new file mode 100644 index 0000000000..da3f83969d --- /dev/null +++ b/third_party/rust/headers/src/util/http_date.rs @@ -0,0 +1,152 @@ +use std::fmt; +use std::str::FromStr; +use std::time::SystemTime; + +use bytes::Bytes; +use http::header::HeaderValue; +use httpdate; + +use super::IterExt; + +/// A timestamp with HTTP formatting and parsing +// Prior to 1995, there were three different formats commonly used by +// servers to communicate timestamps. For compatibility with old +// implementations, all three are defined here. The preferred format is +// a fixed-length and single-zone subset of the date and time +// specification used by the Internet Message Format [RFC5322]. +// +// HTTP-date = IMF-fixdate / obs-date +// +// An example of the preferred format is +// +// Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate +// +// Examples of the two obsolete formats are +// +// Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format +// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format +// +// A recipient that parses a timestamp value in an HTTP header field +// MUST accept all three HTTP-date formats. When a sender generates a +// header field that contains one or more timestamps defined as +// HTTP-date, the sender MUST generate those timestamps in the +// IMF-fixdate format. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct HttpDate(httpdate::HttpDate); + +impl HttpDate { + pub(crate) fn from_val(val: &HeaderValue) -> Option<Self> { + val.to_str().ok()?.parse().ok() + } +} + +// TODO: remove this and FromStr? +#[derive(Debug)] +pub struct Error(()); + +impl super::TryFromValues for HttpDate { + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i HeaderValue>, + { + values + .just_one() + .and_then(HttpDate::from_val) + .ok_or_else(::Error::invalid) + } +} + +impl From<HttpDate> for HeaderValue { + fn from(date: HttpDate) -> HeaderValue { + (&date).into() + } +} + +impl<'a> From<&'a HttpDate> for HeaderValue { + fn from(date: &'a HttpDate) -> HeaderValue { + // TODO: could be just BytesMut instead of String + let s = date.to_string(); + let bytes = Bytes::from(s); + HeaderValue::from_maybe_shared(bytes).expect("HttpDate always is a valid value") + } +} + +impl FromStr for HttpDate { + type Err = Error; + fn from_str(s: &str) -> Result<HttpDate, Error> { + Ok(HttpDate(s.parse().map_err(|_| Error(()))?)) + } +} + +impl fmt::Debug for HttpDate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl fmt::Display for HttpDate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl From<SystemTime> for HttpDate { + fn from(sys: SystemTime) -> HttpDate { + HttpDate(sys.into()) + } +} + +impl From<HttpDate> for SystemTime { + fn from(date: HttpDate) -> SystemTime { + SystemTime::from(date.0) + } +} + +#[cfg(test)] +mod tests { + use super::HttpDate; + + use std::time::{Duration, UNIX_EPOCH}; + + // The old tests had Sunday, but 1994-11-07 is a Monday. + // See https://github.com/pyfisch/httpdate/pull/6#issuecomment-846881001 + fn nov_07() -> HttpDate { + HttpDate((UNIX_EPOCH + Duration::new(784198117, 0)).into()) + } + + #[test] + fn test_display_is_imf_fixdate() { + assert_eq!("Mon, 07 Nov 1994 08:48:37 GMT", &nov_07().to_string()); + } + + #[test] + fn test_imf_fixdate() { + assert_eq!( + "Mon, 07 Nov 1994 08:48:37 GMT".parse::<HttpDate>().unwrap(), + nov_07() + ); + } + + #[test] + fn test_rfc_850() { + assert_eq!( + "Monday, 07-Nov-94 08:48:37 GMT" + .parse::<HttpDate>() + .unwrap(), + nov_07() + ); + } + + #[test] + fn test_asctime() { + assert_eq!( + "Mon Nov 7 08:48:37 1994".parse::<HttpDate>().unwrap(), + nov_07() + ); + } + + #[test] + fn test_no_date() { + assert!("this-is-no-date".parse::<HttpDate>().is_err()); + } +} diff --git a/third_party/rust/headers/src/util/iter.rs b/third_party/rust/headers/src/util/iter.rs new file mode 100644 index 0000000000..03ed691ed6 --- /dev/null +++ b/third_party/rust/headers/src/util/iter.rs @@ -0,0 +1,11 @@ +pub trait IterExt: Iterator { + fn just_one(&mut self) -> Option<Self::Item> { + let one = self.next()?; + match self.next() { + Some(_) => None, + None => Some(one), + } + } +} + +impl<T: Iterator> IterExt for T {} diff --git a/third_party/rust/headers/src/util/mod.rs b/third_party/rust/headers/src/util/mod.rs new file mode 100644 index 0000000000..07fddbfb6c --- /dev/null +++ b/third_party/rust/headers/src/util/mod.rs @@ -0,0 +1,87 @@ +use HeaderValue; + +//pub use self::charset::Charset; +//pub use self::encoding::Encoding; +pub(crate) use self::entity::{EntityTag, EntityTagRange}; +pub(crate) use self::flat_csv::{FlatCsv, SemiColon}; +pub(crate) use self::fmt::fmt; +pub(crate) use self::http_date::HttpDate; +pub(crate) use self::iter::IterExt; +//pub use language_tags::LanguageTag; +//pub use self::quality_value::{Quality, QualityValue}; +pub(crate) use self::seconds::Seconds; +pub(crate) use self::value_string::HeaderValueString; + +//mod charset; +pub(crate) mod csv; +//mod encoding; +mod entity; +mod flat_csv; +mod fmt; +mod http_date; +mod iter; +//mod quality_value; +mod seconds; +mod value_string; + +macro_rules! error_type { + ($name:ident) => { + #[doc(hidden)] + pub struct $name { + _inner: (), + } + + impl ::std::fmt::Debug for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct(stringify!($name)).finish() + } + } + + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.write_str(stringify!($name)) + } + } + + impl ::std::error::Error for $name {} + }; +} + +macro_rules! derive_header { + ($type:ident(_), name: $name:ident) => { + impl crate::Header for $type { + fn name() -> &'static ::http::header::HeaderName { + &::http::header::$name + } + + fn decode<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i ::http::header::HeaderValue>, + { + ::util::TryFromValues::try_from_values(values).map($type) + } + + fn encode<E: Extend<::HeaderValue>>(&self, values: &mut E) { + values.extend(::std::iter::once((&self.0).into())); + } + } + }; +} + +/// A helper trait for use when deriving `Header`. +pub(crate) trait TryFromValues: Sized { + /// Try to convert from the values into an instance of `Self`. + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + Self: Sized, + I: Iterator<Item = &'i HeaderValue>; +} + +impl TryFromValues for HeaderValue { + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i HeaderValue>, + { + values.next().cloned().ok_or_else(|| ::Error::invalid()) + } +} diff --git a/third_party/rust/headers/src/util/seconds.rs b/third_party/rust/headers/src/util/seconds.rs new file mode 100644 index 0000000000..a1a9194b0c --- /dev/null +++ b/third_party/rust/headers/src/util/seconds.rs @@ -0,0 +1,67 @@ +use std::fmt; +use std::time::Duration; + +use util::IterExt; +use HeaderValue; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Seconds(Duration); + +impl Seconds { + pub(crate) fn from_val(val: &HeaderValue) -> Option<Self> { + let secs = val.to_str().ok()?.parse().ok()?; + + Some(Self::from_secs(secs)) + } + + pub(crate) fn from_secs(secs: u64) -> Self { + Self::from(Duration::from_secs(secs)) + } + + pub(crate) fn as_u64(&self) -> u64 { + self.0.as_secs() + } +} + +impl super::TryFromValues for Seconds { + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i HeaderValue>, + { + values + .just_one() + .and_then(Seconds::from_val) + .ok_or_else(::Error::invalid) + } +} + +impl<'a> From<&'a Seconds> for HeaderValue { + fn from(secs: &'a Seconds) -> HeaderValue { + secs.0.as_secs().into() + } +} + +impl From<Duration> for Seconds { + fn from(dur: Duration) -> Seconds { + debug_assert!(dur.subsec_nanos() == 0); + Seconds(dur) + } +} + +impl From<Seconds> for Duration { + fn from(secs: Seconds) -> Duration { + secs.0 + } +} + +impl fmt::Debug for Seconds { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}s", self.0.as_secs()) + } +} + +impl fmt::Display for Seconds { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0.as_secs(), f) + } +} diff --git a/third_party/rust/headers/src/util/value_string.rs b/third_party/rust/headers/src/util/value_string.rs new file mode 100644 index 0000000000..865a3558b9 --- /dev/null +++ b/third_party/rust/headers/src/util/value_string.rs @@ -0,0 +1,93 @@ +use std::{ + fmt, + str::{self, FromStr}, +}; + +use bytes::Bytes; +use http::header::HeaderValue; + +use super::IterExt; + +/// A value that is both a valid `HeaderValue` and `String`. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct HeaderValueString { + /// Care must be taken to only set this value when it is also + /// a valid `String`, since `as_str` will convert to a `&str` + /// in an unchecked manner. + value: HeaderValue, +} + +impl HeaderValueString { + pub(crate) fn from_val(val: &HeaderValue) -> Result<Self, ::Error> { + if val.to_str().is_ok() { + Ok(HeaderValueString { value: val.clone() }) + } else { + Err(::Error::invalid()) + } + } + + pub(crate) fn from_string(src: String) -> Option<Self> { + // A valid `str` (the argument)... + let bytes = Bytes::from(src); + HeaderValue::from_maybe_shared(bytes) + .ok() + .map(|value| HeaderValueString { value }) + } + + pub(crate) fn from_static(src: &'static str) -> HeaderValueString { + // A valid `str` (the argument)... + HeaderValueString { + value: HeaderValue::from_static(src), + } + } + + pub(crate) fn as_str(&self) -> &str { + // HeaderValueString is only created from HeaderValues + // that have validated they are also UTF-8 strings. + unsafe { str::from_utf8_unchecked(self.value.as_bytes()) } + } +} + +impl fmt::Debug for HeaderValueString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} + +impl fmt::Display for HeaderValueString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl super::TryFromValues for HeaderValueString { + fn try_from_values<'i, I>(values: &mut I) -> Result<Self, ::Error> + where + I: Iterator<Item = &'i HeaderValue>, + { + values + .just_one() + .map(HeaderValueString::from_val) + .unwrap_or_else(|| Err(::Error::invalid())) + } +} + +impl<'a> From<&'a HeaderValueString> for HeaderValue { + fn from(src: &'a HeaderValueString) -> HeaderValue { + src.value.clone() + } +} + +#[derive(Debug)] +pub(crate) struct FromStrError(()); + +impl FromStr for HeaderValueString { + type Err = FromStrError; + + fn from_str(src: &str) -> Result<Self, Self::Err> { + // A valid `str` (the argument)... + src.parse() + .map(|value| HeaderValueString { value }) + .map_err(|_| FromStrError(())) + } +} |