diff --git a/vendor/xpath-tests/filters b/vendor/xpath-tests/filters index d2fdb94fe..7be18f0f9 100644 --- a/vendor/xpath-tests/filters +++ b/vendor/xpath-tests/filters @@ -54,11 +54,6 @@ fo-test-fn-json-to-xml-001 fo-test-fn-json-to-xml-002 fo-test-fn-json-to-xml-003 fo-test-fn-json-to-xml-004 -fo-test-fn-parse-ietf-date-001 -fo-test-fn-parse-ietf-date-002 -fo-test-fn-parse-ietf-date-003 -fo-test-fn-parse-ietf-date-004 -fo-test-fn-parse-ietf-date-005 fo-test-fn-parse-json-005 fo-test-fn-parse-json-007 fo-test-fn-starts-with-004 @@ -1138,52 +1133,6 @@ cbcl-fn-normalize-unicode-006 = fn-one-or-more = fn-outermost = fn-parse-ietf-date -parse-ietf-date-10 -parse-ietf-date-11 -parse-ietf-date-12 -parse-ietf-date-13 -parse-ietf-date-18 -parse-ietf-date-19 -parse-ietf-date-20 -parse-ietf-date-21 -parse-ietf-date-22 -parse-ietf-date-23 -parse-ietf-date-24 -parse-ietf-date-25 -parse-ietf-date-26 -parse-ietf-date-27 -parse-ietf-date-28 -parse-ietf-date-29 -parse-ietf-date-3 -parse-ietf-date-31 -parse-ietf-date-4 -parse-ietf-date-41 -parse-ietf-date-42 -parse-ietf-date-43 -parse-ietf-date-44 -parse-ietf-date-45 -parse-ietf-date-46 -parse-ietf-date-47 -parse-ietf-date-48 -parse-ietf-date-5 -parse-ietf-date-52 -parse-ietf-date-53 -parse-ietf-date-54 -parse-ietf-date-54a -parse-ietf-date-55 -parse-ietf-date-56 -parse-ietf-date-57 -parse-ietf-date-58 -parse-ietf-date-59 -parse-ietf-date-6 -parse-ietf-date-60 -parse-ietf-date-61 -parse-ietf-date-62 -parse-ietf-date-63 -parse-ietf-date-64 -parse-ietf-date-errs28 -parse-ietf-date-errs4 -parse-ietf-date-errs5 = fn-parse-json fn-parse-json-050 fn-parse-json-052 diff --git a/xee-interpreter/src/library/datetime.rs b/xee-interpreter/src/library/datetime.rs index 9e015f3b2..a9e608c39 100644 --- a/xee-interpreter/src/library/datetime.rs +++ b/xee-interpreter/src/library/datetime.rs @@ -328,14 +328,7 @@ fn duration_to_offset( #[xpath_fn("fn:parse-ietf-date($value as xs:string?) as xs:dateTime?")] fn parse_ietf_date(value: Option<&str>) -> error::Result> { - if let Some(value) = value { - match chrono::DateTime::parse_from_rfc2822(value.trim()) { - Ok(date_time) => Ok(Some(date_time.into())), - Err(_) => Err(error::Error::FORG0010), - } - } else { - Ok(None) - } + value.map(super::ietf_date::parse).transpose() } pub(crate) fn static_function_descriptions() -> Vec { diff --git a/xee-interpreter/src/library/ietf_date.rs b/xee-interpreter/src/library/ietf_date.rs new file mode 100644 index 000000000..187693f14 --- /dev/null +++ b/xee-interpreter/src/library/ietf_date.rs @@ -0,0 +1,842 @@ +// Parser for fn:parse-ietf-date +// https://www.w3.org/TR/xpath-functions-31/#func-parse-ietf-date +// +// Hand-written per the direction in +// https://github.com/Paligo/xee/issues/36: the interpreter's existing +// chumsky dependency is slated for removal, so this deliberately adds no +// parser-library usage. The grammar is the one in the spec: the three RFC +// 2616 (HTTP) date formats, extended with liberal whitespace, single-digit +// fields, and lowercase input. + +use crate::atomic::NaiveDateTimeWithOffset; +use crate::error; + +// full names before abbreviations so that "Tuesday" is not read as "Tue" + "sday" +const DAYNAMES: [&str; 14] = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", +]; + +const MONTHNAMES: [&str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +// timezone names with their offset in minutes; "UTC" before "UT" so the +// longest match wins +const TZNAMES: [(&str, i32); 11] = [ + ("UTC", 0), + ("UT", 0), + ("GMT", 0), + ("EST", -5 * 60), + ("EDT", -4 * 60), + ("CST", -6 * 60), + ("CDT", -5 * 60), + ("MST", -7 * 60), + ("MDT", -6 * 60), + ("PST", -8 * 60), + ("PDT", -7 * 60), +]; + +const MAX_OFFSET_MINUTES: u32 = 14 * 60; + +struct Time { + hours: u32, + minutes: u32, + seconds: u32, + nanoseconds: u32, + // whether any fractional digit was nonzero, even beyond the nanosecond + // precision that survives in nanoseconds + nonzero_fraction: bool, + offset_minutes: Option, +} + +pub(crate) fn parse(input: &str) -> error::Result { + Parser { + input: input.as_bytes(), + pos: 0, + } + .parse() +} + +struct Parser<'a> { + input: &'a [u8], + pos: usize, +} + +impl<'a> Parser<'a> { + // input ::= S? (dayname ","? S)? ((datespec S time) | asctime) S? + fn parse(mut self) -> error::Result { + self.skip_whitespace(); + if self.accept_dayname() { + self.accept(b','); + self.require_whitespace()?; + } + let (date, time) = if matches!(self.peek(), Some(b'0'..=b'9')) { + // datespec ::= daynum dsep monthname dsep year + let day = self.daynum()?; + self.date_separator()?; + let month = self.monthname()?; + self.date_separator()?; + let year = self.year()?; + self.require_whitespace()?; + let time = self.time()?; + ((year, month, day), time) + } else { + // asctime ::= monthname dsep daynum S time S year + let month = self.monthname()?; + self.date_separator()?; + let day = self.daynum()?; + self.require_whitespace()?; + let time = self.time()?; + self.require_whitespace()?; + let year = self.year()?; + ((year, month, day), time) + }; + self.skip_whitespace(); + if self.pos != self.input.len() { + return Err(error::Error::FORG0010); + } + build_date_time(date, time) + } + + fn peek(&self) -> Option { + self.input.get(self.pos).copied() + } + + fn accept(&mut self, byte: u8) -> bool { + if self.peek() == Some(byte) { + self.pos += 1; + true + } else { + false + } + } + + // a case-insensitive match of the whole keyword + fn accept_keyword(&mut self, keyword: &str) -> bool { + let bytes = keyword.as_bytes(); + match self.input.get(self.pos..self.pos + bytes.len()) { + Some(candidate) if candidate.eq_ignore_ascii_case(bytes) => { + self.pos += bytes.len(); + true + } + _ => false, + } + } + + // S ::= ( x09 | x0A | x0D | x20 )+ + fn skip_whitespace(&mut self) -> bool { + let start = self.pos; + while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) { + self.pos += 1; + } + self.pos > start + } + + fn require_whitespace(&mut self) -> error::Result<()> { + if self.skip_whitespace() { + Ok(()) + } else { + Err(error::Error::FORG0010) + } + } + + fn digits(&mut self) -> &'a [u8] { + let start = self.pos; + while matches!(self.peek(), Some(b'0'..=b'9')) { + self.pos += 1; + } + &self.input[start..self.pos] + } + + fn accept_dayname(&mut self) -> bool { + DAYNAMES.iter().any(|dayname| self.accept_keyword(dayname)) + } + + fn monthname(&mut self) -> error::Result { + for (index, monthname) in MONTHNAMES.iter().enumerate() { + if self.accept_keyword(monthname) { + return Ok(index as u32 + 1); + } + } + Err(error::Error::FORG0010) + } + + // dsep ::= S | (S? "-" S?) + fn date_separator(&mut self) -> error::Result<()> { + let seen_whitespace = self.skip_whitespace(); + if self.accept(b'-') { + self.skip_whitespace(); + Ok(()) + } else if seen_whitespace { + Ok(()) + } else { + Err(error::Error::FORG0010) + } + } + + // daynum ::= digit digit? + fn daynum(&mut self) -> error::Result { + match self.digits() { + digits @ ([_] | [_, _]) => Ok(to_number(digits)), + _ => Err(error::Error::FORG0010), + } + } + + // year ::= digit digit (digit digit)? + // A two-digit year must have 1900 added to it. + fn year(&mut self) -> error::Result { + match self.digits() { + digits @ [_, _] => Ok(to_number(digits) as i32 + 1900), + digits @ [_, _, _, _] => Ok(to_number(digits) as i32), + _ => Err(error::Error::FORG0010), + } + } + + // time ::= hours ":" minutes (":" seconds)? (S? timezone)? + fn time(&mut self) -> error::Result