diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd96087..ec4df7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -229,23 +229,6 @@ jobs: run: | cargo bench --manifest-path bench/Cargo.toml -- --test - # Test that we can build the fuzzer targets. - # - # It's not necessary to check the fuzzers on all platforms, as this is pretty - # strictly a testing utility. - test-fuzz: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - - name: Build fuzzer targets - working-directory: ./fuzz - run: cargo build --verbose - # Runs miri on a subset of Jiff's test suite. This doesn't quite cover # everything. In particular, `miri` and `insta` cannot play nice together, # and `insta` is used a lot among Jiff's tests. However, the primary reason diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json index 24a7443..fc1050d 100644 --- a/.vim/coc-settings.json +++ b/.vim/coc-settings.json @@ -2,7 +2,6 @@ "rust-analyzer.linkedProjects": [ "bench/Cargo.toml", "crates/jiff-icu/Cargo.toml", - "fuzz/Cargo.toml", "Cargo.toml" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 914a7a5..705254a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,5 @@ # CHANGELOG -0.2.17 (TBD) -============ -TODO - -Enhancements: - -* [#412](https://github.com/BurntSushi/jiff/issues/412): -Add `Display`, `FromStr`, `Serialize` and `Deserialize` trait implementations -for `jiff::civil::ISOWeekDate`. These all use the ISO 8601 week date format. -* [#418](https://github.com/BurntSushi/jiff/issues/418): -Add some basic predicates to `jiff::Error` for basic error introspection. - - 0.2.16 (2025-11-07) =================== This release contains a number of enhancements and bug fixes that have accrued diff --git a/crates/jiff-cli/cmd/generate/shared.rs b/crates/jiff-cli/cmd/generate/shared.rs index 580d89a..be1783e 100644 --- a/crates/jiff-cli/cmd/generate/shared.rs +++ b/crates/jiff-cli/cmd/generate/shared.rs @@ -79,7 +79,7 @@ fn copy(srcdir: &Path, dstdir: &Path, dir: &Path) -> anyhow::Result<()> { fn copy_rust_source_file(src: &Path, dst: &Path) -> anyhow::Result<()> { let code = fs::read_to_string(src) .with_context(|| format!("failed to read {}", src.display()))?; - let code = remove_only_jiffs(&remove_cfg_alloc_or_std(&code)); + let code = remove_only_jiffs(&remove_cfg_alloc(&code)); let mut out = String::new(); writeln!(out, "// auto-generated by: jiff-cli generate shared")?; @@ -105,13 +105,13 @@ fn remove_only_jiffs(code: &str) -> String { RE.replace_all(code, "").into_owned() } -/// Removes all `#[cfg(feature = "alloc|std")]` gates. +/// Removes all `#[cfg(feature = "alloc")]` gates. /// /// This is because the proc-macro always runs in a context where `alloc` /// (and `std`) are enabled. -fn remove_cfg_alloc_or_std(code: &str) -> String { +fn remove_cfg_alloc(code: &str) -> String { static RE: LazyLock = LazyLock::new(|| { - Regex::new(r###"#\[cfg\(feature = "(alloc|std)"\)\]\n"###).unwrap() + Regex::new(r###"#\[cfg\(feature = "alloc"\)\]\n"###).unwrap() }); RE.replace_all(code, "").into_owned() } diff --git a/crates/jiff-static/src/shared/error/itime.rs b/crates/jiff-static/src/shared/error/itime.rs deleted file mode 100644 index 05f56a6..0000000 --- a/crates/jiff-static/src/shared/error/itime.rs +++ /dev/null @@ -1,100 +0,0 @@ -// auto-generated by: jiff-cli generate shared - -use crate::shared::{ - error, - util::itime::{days_in_month, days_in_year, IEpochDay}, -}; - -// N.B. Every variant in this error type is a range error. -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum Error { - DateInvalidDayOfYear { year: i16 }, - DateInvalidDayOfYearNoLeap, - DateInvalidDays { year: i16, month: i8 }, - DateTimeSeconds, - // TODO: I believe this can never happen. - DayOfYear, - EpochDayDays, - EpochDayI32, - NthWeekdayOfMonth, - Tomorrow, - YearNext, - YearPrevious, - Yesterday, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::Time(err).into() - } -} - -// impl error::IntoError for Error { -// fn into_error(self) -> error::Error { -// self.into() -// } -// } - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - DateInvalidDayOfYear { year } => write!( - f, - "number of days for `{year:04}` is invalid, \ - must be in range `1..={max_day}`", - max_day = days_in_year(year), - ), - DateInvalidDayOfYearNoLeap => f.write_str( - "number of days is invalid, must be in range `1..=365`", - ), - DateInvalidDays { year, month } => write!( - f, - "number of days for `{year:04}-{month:02}` is invalid, \ - must be in range `1..={max_day}`", - max_day = days_in_month(year, month), - ), - DateTimeSeconds => { - f.write_str("adding seconds to datetime overflowed") - } - DayOfYear => f.write_str("day of year is invalid"), - EpochDayDays => write!( - f, - "adding to epoch day resulted in a value outside \ - the allowed range of `{min}..={max}`", - min = IEpochDay::MIN.epoch_day, - max = IEpochDay::MAX.epoch_day, - ), - EpochDayI32 => f.write_str( - "adding to epoch day overflowed 32-bit signed integer", - ), - NthWeekdayOfMonth => f.write_str( - "invalid nth weekday of month, \ - must be non-zero and in range `-5..=5`", - ), - Tomorrow => f.write_str( - "returning tomorrow for `9999-12-31` is not \ - possible because it is greater than Jiff's supported - maximum date", - ), - YearNext => f.write_str( - "creating a date for a year following `9999` is \ - not possible because it is greater than Jiff's supported \ - maximum date", - ), - YearPrevious => f.write_str( - "creating a date for a year preceding `-9999` is \ - not possible because it is less than Jiff's supported \ - minimum date", - ), - Yesterday => f.write_str( - "returning yesterday for `-9999-01-01` is not \ - possible because it is less than Jiff's supported - minimum date", - ), - } - } -} diff --git a/crates/jiff-static/src/shared/error/mod.rs b/crates/jiff-static/src/shared/error/mod.rs deleted file mode 100644 index 921da75..0000000 --- a/crates/jiff-static/src/shared/error/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -// auto-generated by: jiff-cli generate shared - -pub(crate) mod itime; - -/// An error scoped to Jiff's `shared` module. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Error { - kind: ErrorKind, -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - self.kind.fmt(f) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum ErrorKind { - Time(self::itime::Error), -} - -impl From for Error { - fn from(kind: ErrorKind) -> Error { - Error { kind } - } -} - -impl core::fmt::Display for ErrorKind { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - match *self { - ErrorKind::Time(ref err) => err.fmt(f), - } - } -} - -/* -/// A slim error that occurs when an input value is out of bounds. -#[derive(Clone, Debug)] -struct SlimRangeError { - what: &'static str, -} - -impl SlimRangeError { - fn new(what: &'static str) -> SlimRangeError { - SlimRangeError { what } - } -} - -impl std::error::Error for SlimRangeError {} - -impl core::fmt::Display for SlimRangeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - let SlimRangeError { what } = *self; - write!(f, "parameter '{what}' is not in the required range") - } -} -*/ diff --git a/crates/jiff-static/src/shared/posix.rs b/crates/jiff-static/src/shared/posix.rs index 6297171..fef2428 100644 --- a/crates/jiff-static/src/shared/posix.rs +++ b/crates/jiff-static/src/shared/posix.rs @@ -5,6 +5,8 @@ use core::fmt::Debug; use super::{ util::{ array_str::Abbreviation, + error::{err, Error}, + escape::{Byte, Bytes}, itime::{ IAmbiguousOffset, IDate, IDateTime, IOffset, ITime, ITimeSecond, ITimestamp, IWeekday, @@ -17,9 +19,7 @@ use super::{ impl PosixTimeZone { /// Parse a POSIX `TZ` environment variable, assuming it's a rule and not /// an implementation defined value, from the given bytes. - pub fn parse( - bytes: &[u8], - ) -> Result, PosixTimeZoneError> { + pub fn parse(bytes: &[u8]) -> Result, Error> { // We enable the IANA v3+ extensions here. (Namely, that the time // specification hour value has the range `-167..=167` instead of // `0..=24`.) Requiring strict POSIX rules doesn't seem necessary @@ -333,11 +333,12 @@ impl + Debug> PosixTimeZone { impl> core::fmt::Display for PosixTimeZone { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt( - &AbbreviationDisplay(self.std_abbrev.as_ref()), + write!( f, + "{}{}", + AbbreviationDisplay(self.std_abbrev.as_ref()), + self.std_offset )?; - core::fmt::Display::fmt(&self.std_offset, f)?; if let Some(ref dst) = self.dst { dst.display(&self.std_offset, f)?; } @@ -351,29 +352,22 @@ impl> PosixDst { std_offset: &PosixOffset, f: &mut core::fmt::Formatter, ) -> core::fmt::Result { - core::fmt::Display::fmt( - &AbbreviationDisplay(self.abbrev.as_ref()), - f, - )?; + write!(f, "{}", AbbreviationDisplay(self.abbrev.as_ref()))?; // The overwhelming common case is that DST is exactly one hour ahead // of standard time. So common that this is the default. So don't write // the offset if we don't need to. let default = PosixOffset { second: std_offset.second + 3600 }; if self.offset != default { - core::fmt::Display::fmt(&self.offset, f)?; + write!(f, "{}", self.offset)?; } - f.write_str(",")?; - core::fmt::Display::fmt(&self.rule, f)?; + write!(f, ",{}", self.rule)?; Ok(()) } } impl core::fmt::Display for PosixRule { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt(&self.start, f)?; - f.write_str(",")?; - core::fmt::Display::fmt(&self.end, f)?; - Ok(()) + write!(f, "{},{}", self.start, self.end) } } @@ -424,12 +418,11 @@ impl PosixDayTime { impl core::fmt::Display for PosixDayTime { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt(&self.date, f)?; + write!(f, "{}", self.date)?; // This is the default time, so don't write it if we // don't need to. if self.time != PosixTime::DEFAULT { - f.write_str("/")?; - core::fmt::Display::fmt(&self.time, f)?; + write!(f, "/{}", self.time)?; } Ok(()) } @@ -497,19 +490,10 @@ impl PosixDay { impl core::fmt::Display for PosixDay { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { - PosixDay::JulianOne(n) => { - f.write_str("J")?; - core::fmt::Display::fmt(&n, f) - } - PosixDay::JulianZero(n) => core::fmt::Display::fmt(&n, f), + PosixDay::JulianOne(n) => write!(f, "J{n}"), + PosixDay::JulianZero(n) => write!(f, "{n}"), PosixDay::WeekdayOfMonth { month, week, weekday } => { - f.write_str("M")?; - core::fmt::Display::fmt(&month, f)?; - f.write_str(".")?; - core::fmt::Display::fmt(&week, f)?; - f.write_str(".")?; - core::fmt::Display::fmt(&weekday, f)?; - Ok(()) + write!(f, "M{month}.{week}.{weekday}") } } } @@ -522,7 +506,7 @@ impl PosixTime { impl core::fmt::Display for PosixTime { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { if self.second.is_negative() { - f.write_str("-")?; + write!(f, "-")?; // The default is positive, so when // positive, we write nothing. } @@ -530,7 +514,7 @@ impl core::fmt::Display for PosixTime { let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - core::fmt::Display::fmt(&h, f)?; + write!(f, "{h}")?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -553,13 +537,13 @@ impl core::fmt::Display for PosixOffset { // N.B. `+` is the default, so we don't // need to write that out. if self.second > 0 { - f.write_str("-")?; + write!(f, "-")?; } let second = self.second.unsigned_abs(); let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - core::fmt::Display::fmt(&h, f)?; + write!(f, "{h}")?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -581,11 +565,9 @@ impl> core::fmt::Display for AbbreviationDisplay { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { let s = self.0.as_ref(); if s.chars().any(|ch| ch == '+' || ch == '-') { - f.write_str("<")?; - core::fmt::Display::fmt(&s, f)?; - f.write_str(">") + write!(f, "<{s}>") } else { - core::fmt::Display::fmt(&s, f) + write!(f, "{s}") } } } @@ -679,12 +661,15 @@ impl<'s> Parser<'s> { /// Parses a POSIX time zone from the current position of the parser and /// ensures that the entire TZ string corresponds to a single valid POSIX /// time zone. - fn parse( - &self, - ) -> Result, PosixTimeZoneError> { + fn parse(&self) -> Result, Error> { let (time_zone, remaining) = self.parse_prefix()?; if !remaining.is_empty() { - return Err(ErrorKind::FoundRemaining.into()); + return Err(err!( + "expected entire TZ string to be a valid POSIX \ + time zone, but found `{}` after what would otherwise \ + be a valid POSIX TZ string", + Bytes(remaining), + )); } Ok(time_zone) } @@ -693,8 +678,7 @@ impl<'s> Parser<'s> { /// returns the remaining input. fn parse_prefix( &self, - ) -> Result<(PosixTimeZone, &'s [u8]), PosixTimeZoneError> - { + ) -> Result<(PosixTimeZone, &'s [u8]), Error> { let time_zone = self.parse_posix_time_zone()?; Ok((time_zone, self.remaining())) } @@ -705,14 +689,18 @@ impl<'s> Parser<'s> { /// TZ string. fn parse_posix_time_zone( &self, - ) -> Result, PosixTimeZoneError> { + ) -> Result, Error> { if self.is_done() { - return Err(ErrorKind::Empty.into()); + return Err(err!( + "an empty string is not a valid POSIX time zone" + )); } - let std_abbrev = - self.parse_abbreviation().map_err(ErrorKind::AbbreviationStd)?; - let std_offset = - self.parse_posix_offset().map_err(ErrorKind::OffsetStd)?; + let std_abbrev = self + .parse_abbreviation() + .map_err(|e| err!("failed to parse standard abbreviation: {e}"))?; + let std_offset = self + .parse_posix_offset() + .map_err(|e| err!("failed to parse standard offset: {e}"))?; let mut dst = None; if !self.is_done() && (self.byte().is_ascii_alphabetic() || self.byte() == b'<') @@ -733,30 +721,49 @@ impl<'s> Parser<'s> { fn parse_posix_dst( &self, std_offset: &PosixOffset, - ) -> Result, PosixTimeZoneError> { - let abbrev = - self.parse_abbreviation().map_err(ErrorKind::AbbreviationDst)?; + ) -> Result, Error> { + let abbrev = self + .parse_abbreviation() + .map_err(|e| err!("failed to parse DST abbreviation: {e}"))?; if self.is_done() { - return Err(ErrorKind::FoundDstNoRule.into()); + return Err(err!( + "found DST abbreviation `{abbrev}`, but no transition \ + rule (this is technically allowed by POSIX, but has \ + unspecified behavior)", + )); } // This is the default: one hour ahead of standard time. We may // override this if the DST portion specifies an offset. (But it // usually doesn't.) let mut offset = PosixOffset { second: std_offset.second + 3600 }; if self.byte() != b',' { - offset = - self.parse_posix_offset().map_err(ErrorKind::OffsetDst)?; + offset = self + .parse_posix_offset() + .map_err(|e| err!("failed to parse DST offset: {e}"))?; if self.is_done() { - return Err(ErrorKind::FoundDstNoRuleWithOffset.into()); + return Err(err!( + "found DST abbreviation `{abbrev}` and offset \ + `{offset}s`, but no transition rule (this is \ + technically allowed by POSIX, but has \ + unspecified behavior)", + offset = offset.second, + )); } } if self.byte() != b',' { - return Err(ErrorKind::ExpectedCommaAfterDst.into()); + return Err(err!( + "after parsing DST offset in POSIX time zone string, \ + found `{}` but expected a ','", + Byte(self.byte()), + )); } if !self.bump() { - return Err(ErrorKind::FoundEndAfterComma.into()); + return Err(err!( + "after parsing DST offset in POSIX time zone string, \ + found end of string after a trailing ','", + )); } - let rule = self.parse_rule().map_err(ErrorKind::Rule)?; + let rule = self.parse_rule()?; Ok(PosixDst { abbrev, offset, rule }) } @@ -772,17 +779,18 @@ impl<'s> Parser<'s> { /// The string returned is guaranteed to be no more than 30 bytes. /// (This restriction is somewhat arbitrary, but it's so we can put /// the abbreviation in a fixed capacity array.) - fn parse_abbreviation(&self) -> Result { + fn parse_abbreviation(&self) -> Result { if self.byte() == b'<' { if !self.bump() { - return Err(AbbreviationError::Quoted( - QuotedAbbreviationError::UnexpectedEndAfterOpening, + return Err(err!( + "found opening '<' quote for abbreviation in \ + POSIX time zone string, and expected a name \ + following it, but found the end of string instead" )); } - self.parse_quoted_abbreviation().map_err(AbbreviationError::Quoted) + self.parse_quoted_abbreviation() } else { self.parse_unquoted_abbreviation() - .map_err(AbbreviationError::Unquoted) } } @@ -797,16 +805,19 @@ impl<'s> Parser<'s> { /// The string returned is guaranteed to be no more than 30 bytes. /// (This restriction is somewhat arbitrary, but it's so we can put /// the abbreviation in a fixed capacity array.) - fn parse_unquoted_abbreviation( - &self, - ) -> Result { + fn parse_unquoted_abbreviation(&self) -> Result { let start = self.pos(); for i in 0.. { if !self.byte().is_ascii_alphabetic() { break; } if i >= Abbreviation::capacity() { - return Err(UnquotedAbbreviationError::TooLong); + return Err(err!( + "expected abbreviation with at most {} bytes, \ + but found a longer abbreviation beginning with `{}`", + Abbreviation::capacity(), + Bytes(&self.tz[start..][..i]), + )); } if !self.bump() { break; @@ -821,10 +832,18 @@ impl<'s> Parser<'s> { // `end` is ASCII and thus should be UTF-8. But it doesn't // cost us anything to report an error here in case the // code above evolves somehow. - UnquotedAbbreviationError::InvalidUtf8 + err!( + "found abbreviation `{}`, but it is not valid UTF-8", + Bytes(&self.tz[start..end]), + ) })?; if abbrev.len() < 3 { - return Err(UnquotedAbbreviationError::TooShort); + return Err(err!( + "expected abbreviation with 3 or more bytes, but found \ + abbreviation {:?} with {} bytes", + abbrev, + abbrev.len(), + )); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -842,9 +861,7 @@ impl<'s> Parser<'s> { /// The string returned is guaranteed to be no more than 30 bytes. /// (This restriction is somewhat arbitrary, but it's so we can put /// the abbreviation in a fixed capacity array.) - fn parse_quoted_abbreviation( - &self, - ) -> Result { + fn parse_quoted_abbreviation(&self) -> Result { let start = self.pos(); for i in 0.. { if !self.byte().is_ascii_alphanumeric() @@ -854,7 +871,12 @@ impl<'s> Parser<'s> { break; } if i >= Abbreviation::capacity() { - return Err(QuotedAbbreviationError::TooLong); + return Err(err!( + "expected abbreviation with at most {} bytes, \ + but found a longer abbreviation beginning with `{}`", + Abbreviation::capacity(), + Bytes(&self.tz[start..][..i]), + )); } if !self.bump() { break; @@ -869,17 +891,33 @@ impl<'s> Parser<'s> { // `end` is ASCII and thus should be UTF-8. But it doesn't // cost us anything to report an error here in case the // code above evolves somehow. - QuotedAbbreviationError::InvalidUtf8 + err!( + "found abbreviation `{}`, but it is not valid UTF-8", + Bytes(&self.tz[start..end]), + ) })?; if self.is_done() { - return Err(QuotedAbbreviationError::UnexpectedEnd); + return Err(err!( + "found non-empty quoted abbreviation {abbrev:?}, but \ + did not find expected end-of-quoted abbreviation \ + '>' character", + )); } if self.byte() != b'>' { - return Err(QuotedAbbreviationError::UnexpectedLastByte); + return Err(err!( + "found non-empty quoted abbreviation {abbrev:?}, but \ + found `{}` instead of end-of-quoted abbreviation '>' \ + character", + Byte(self.byte()), + )); } self.bump(); if abbrev.len() < 3 { - return Err(QuotedAbbreviationError::TooShort); + return Err(err!( + "expected abbreviation with 3 or more bytes, but found \ + abbreviation {abbrev:?} with {} bytes", + abbrev.len(), + )); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -894,18 +932,30 @@ impl<'s> Parser<'s> { /// /// Upon success, the parser will be positioned immediately after the /// end of the offset. - fn parse_posix_offset(&self) -> Result { - let sign = self.parse_optional_sign()?.unwrap_or(1); + fn parse_posix_offset(&self) -> Result { + let sign = self + .parse_optional_sign() + .map_err(|e| { + err!( + "failed to parse sign for time offset \ + in POSIX time zone string: {e}", + ) + })? + .unwrap_or(1); let hour = self.parse_hour_posix()?; let (mut minute, mut second) = (0, 0); if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixOffsetError::IncompleteMinutes); + return Err(err!( + "incomplete time in POSIX timezone (missing minutes)", + )); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixOffsetError::IncompleteSeconds); + return Err(err!( + "incomplete time in POSIX timezone (missing seconds)", + )); } second = self.parse_second()?; } @@ -936,16 +986,19 @@ impl<'s> Parser<'s> { /// Upon success, the parser will be positioned immediately after the /// DST transition rule. In typical cases, this corresponds to the end /// of the TZ string. - fn parse_rule(&self) -> Result { - let start = self - .parse_posix_datetime() - .map_err(PosixRuleError::DateTimeStart)?; + fn parse_rule(&self) -> Result { + let start = self.parse_posix_datetime().map_err(|e| { + err!("failed to parse start of DST transition rule: {e}") + })?; if self.maybe_byte() != Some(b',') || !self.bump() { - return Err(PosixRuleError::ExpectedEnd); + return Err(err!( + "expected end of DST rule after parsing the start \ + of the DST rule" + )); } - let end = self - .parse_posix_datetime() - .map_err(PosixRuleError::DateTimeEnd)?; + let end = self.parse_posix_datetime().map_err(|e| { + err!("failed to parse end of DST transition rule: {e}") + })?; Ok(PosixRule { start, end }) } @@ -957,9 +1010,7 @@ impl<'s> Parser<'s> { /// Upon success, the parser will be positioned after the datetime /// specification. This will either be immediately after the date, or /// if it's present, the time part of the specification. - fn parse_posix_datetime( - &self, - ) -> Result { + fn parse_posix_datetime(&self) -> Result { let mut daytime = PosixDayTime { date: self.parse_posix_date()?, time: PosixTime::DEFAULT, @@ -968,7 +1019,10 @@ impl<'s> Parser<'s> { return Ok(daytime); } if !self.bump() { - return Err(PosixDateTimeError::ExpectedTime); + return Err(err!( + "expected time specification after '/' following a date + specification in a POSIX time zone DST transition rule", + )); } daytime.time = self.parse_posix_time()?; Ok(daytime) @@ -987,11 +1041,16 @@ impl<'s> Parser<'s> { /// /// Upon success, the parser will be positioned immediately after the /// date specification. - fn parse_posix_date(&self) -> Result { + fn parse_posix_date(&self) -> Result { match self.byte() { b'J' => { if !self.bump() { - return Err(PosixDateError::ExpectedJulianNoLeap); + return Err(err!( + "expected one-based Julian day after 'J' in date \ + specification of a POSIX time zone DST \ + transition rule, but got the end of the string \ + instead" + )); } Ok(PosixDay::JulianOne(self.parse_posix_julian_day_no_leap()?)) } @@ -1000,12 +1059,22 @@ impl<'s> Parser<'s> { )), b'M' => { if !self.bump() { - return Err(PosixDateError::ExpectedMonthWeekWeekday); + return Err(err!( + "expected month-week-weekday after 'M' in date \ + specification of a POSIX time zone DST \ + transition rule, but got the end of the string \ + instead" + )); } let (month, week, weekday) = self.parse_weekday_of_month()?; Ok(PosixDay::WeekdayOfMonth { month, week, weekday }) } - _ => Err(PosixDateError::UnexpectedByte), + _ => Err(err!( + "expected 'J', a digit or 'M' at the beginning of a date \ + specification of a POSIX time zone DST transition rule, \ + but got `{}` instead", + Byte(self.byte()), + )), } } @@ -1015,16 +1084,22 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned just after the `J` and at the /// first digit of the Julian day. Upon success, the parser will be /// positioned immediately following the day number. - fn parse_posix_julian_day_no_leap( - &self, - ) -> Result { + fn parse_posix_julian_day_no_leap(&self) -> Result { let number = self .parse_number_with_upto_n_digits(3) - .map_err(PosixJulianNoLeapError::Parse)?; - let number = i16::try_from(number) - .map_err(|_| PosixJulianNoLeapError::Range)?; + .map_err(|e| err!("invalid one based Julian day: {e}"))?; + let number = i16::try_from(number).map_err(|_| { + err!( + "one based Julian day `{number}` in POSIX time zone \ + does not fit into 16-bit integer" + ) + })?; if !(1 <= number && number <= 365) { - return Err(PosixJulianNoLeapError::Range); + return Err(err!( + "parsed one based Julian day `{number}`, \ + but one based Julian day in POSIX time zone \ + must be in range 1..=365", + )); } Ok(number) } @@ -1035,16 +1110,22 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the first digit of the /// Julian day. Upon success, the parser will be positioned immediately /// following the day number. - fn parse_posix_julian_day_with_leap( - &self, - ) -> Result { + fn parse_posix_julian_day_with_leap(&self) -> Result { let number = self .parse_number_with_upto_n_digits(3) - .map_err(PosixJulianLeapError::Parse)?; - let number = - i16::try_from(number).map_err(|_| PosixJulianLeapError::Range)?; + .map_err(|e| err!("invalid zero based Julian day: {e}"))?; + let number = i16::try_from(number).map_err(|_| { + err!( + "zero based Julian day `{number}` in POSIX time zone \ + does not fit into 16-bit integer" + ) + })?; if !(0 <= number && number <= 365) { - return Err(PosixJulianLeapError::Range); + return Err(err!( + "parsed zero based Julian day `{number}`, \ + but zero based Julian day in POSIX time zone \ + must be in range 0..=365", + )); } Ok(number) } @@ -1058,22 +1139,31 @@ impl<'s> Parser<'s> { /// /// The tuple returned is month (1..=12), week (1..=5) and weekday /// (0..=6 with 0=Sunday). - fn parse_weekday_of_month( - &self, - ) -> Result<(i8, i8, i8), WeekdayOfMonthError> { + fn parse_weekday_of_month(&self) -> Result<(i8, i8, i8), Error> { let month = self.parse_month()?; if self.maybe_byte() != Some(b'.') { - return Err(WeekdayOfMonthError::ExpectedDotAfterMonth); + return Err(err!( + "expected '.' after month `{month}` in \ + POSIX time zone rule" + )); } if !self.bump() { - return Err(WeekdayOfMonthError::ExpectedWeekAfterMonth); + return Err(err!( + "expected week after month `{month}` in \ + POSIX time zone rule" + )); } let week = self.parse_week()?; if self.maybe_byte() != Some(b'.') { - return Err(WeekdayOfMonthError::ExpectedDotAfterWeek); + return Err(err!( + "expected '.' after week `{week}` in POSIX time zone rule" + )); } if !self.bump() { - return Err(WeekdayOfMonthError::ExpectedDayOfWeekAfterWeek); + return Err(err!( + "expected day-of-week after week `{week}` in \ + POSIX time zone rule" + )); } let weekday = self.parse_weekday()?; Ok((month, week, weekday)) @@ -1085,9 +1175,17 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the first `h` (or the /// sign, if present). Upon success, the parser will be positioned /// immediately following the end of the time specification. - fn parse_posix_time(&self) -> Result { + fn parse_posix_time(&self) -> Result { let (sign, hour) = if self.ianav3plus { - let sign = self.parse_optional_sign()?.unwrap_or(1); + let sign = self + .parse_optional_sign() + .map_err(|e| { + err!( + "failed to parse sign for transition time \ + in POSIX time zone string: {e}", + ) + })? + .unwrap_or(1); let hour = self.parse_hour_ianav3plus()?; (sign, hour) } else { @@ -1096,12 +1194,18 @@ impl<'s> Parser<'s> { let (mut minute, mut second) = (0, 0); if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixTimeError::IncompleteMinutes); + return Err(err!( + "incomplete transition time in \ + POSIX time zone string (missing minutes)", + )); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixTimeError::IncompleteSeconds); + return Err(err!( + "incomplete transition time in \ + POSIX time zone string (missing seconds)", + )); } second = self.parse_second()?; } @@ -1126,13 +1230,19 @@ impl<'s> Parser<'s> { /// This is expected to be positioned at the first digit. Upon success, /// the parser will be positioned after the month (which may contain /// two digits). - fn parse_month(&self) -> Result { - let number = self - .parse_number_with_upto_n_digits(2) - .map_err(MonthError::Parse)?; - let number = i8::try_from(number).map_err(|_| MonthError::Range)?; + fn parse_month(&self) -> Result { + let number = self.parse_number_with_upto_n_digits(2)?; + let number = i8::try_from(number).map_err(|_| { + err!( + "month `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(1 <= number && number <= 12) { - return Err(MonthError::Range); + return Err(err!( + "parsed month `{number}`, but month in \ + POSIX time zone must be in range 1..=12", + )); } Ok(number) } @@ -1141,14 +1251,19 @@ impl<'s> Parser<'s> { /// /// This is expected to be positioned at the first digit. Upon success, /// the parser will be positioned after the week digit. - fn parse_week(&self) -> Result { - let number = self - .parse_number_with_exactly_n_digits(1) - .map_err(WeekOfMonthError::Parse)?; - let number = - i8::try_from(number).map_err(|_| WeekOfMonthError::Range)?; + fn parse_week(&self) -> Result { + let number = self.parse_number_with_exactly_n_digits(1)?; + let number = i8::try_from(number).map_err(|_| { + err!( + "week `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(1 <= number && number <= 5) { - return Err(WeekOfMonthError::Range); + return Err(err!( + "parsed week `{number}`, but week in \ + POSIX time zone must be in range 1..=5" + )); } Ok(number) } @@ -1160,13 +1275,20 @@ impl<'s> Parser<'s> { /// /// The weekday returned is guaranteed to be in the range `0..=6`, with /// `0` corresponding to Sunday. - fn parse_weekday(&self) -> Result { - let number = self - .parse_number_with_exactly_n_digits(1) - .map_err(WeekdayError::Parse)?; - let number = i8::try_from(number).map_err(|_| WeekdayError::Range)?; + fn parse_weekday(&self) -> Result { + let number = self.parse_number_with_exactly_n_digits(1)?; + let number = i8::try_from(number).map_err(|_| { + err!( + "weekday `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 6) { - return Err(WeekdayError::Range); + return Err(err!( + "parsed weekday `{number}`, but weekday in \ + POSIX time zone must be in range `0..=6` \ + (with `0` corresponding to Sunday)", + )); } Ok(number) } @@ -1182,20 +1304,27 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first hour digit should occur. Upon success, the parser will be /// positioned immediately after the last hour digit. - fn parse_hour_ianav3plus(&self) -> Result { + fn parse_hour_ianav3plus(&self) -> Result { // Callers should only be using this method when IANA v3+ parsing // is enabled. assert!(self.ianav3plus); let number = self .parse_number_with_upto_n_digits(3) - .map_err(HourIanaError::Parse)?; - let number = - i16::try_from(number).map_err(|_| HourIanaError::Range)?; + .map_err(|e| err!("invalid hour digits: {e}"))?; + let number = i16::try_from(number).map_err(|_| { + err!( + "hour `{number}` in POSIX time zone \ + does not fit into 16-bit integer" + ) + })?; if !(0 <= number && number <= 167) { // The error message says -167 but the check above uses 0. // This is because the caller is responsible for parsing // the sign. - return Err(HourIanaError::Range); + return Err(err!( + "parsed hour `{number}`, but hour in IANA v3+ \ + POSIX time zone must be in range `-167..=167`", + )); } Ok(number) } @@ -1209,14 +1338,21 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first hour digit should occur. Upon success, the parser will be /// positioned immediately after the last hour digit. - fn parse_hour_posix(&self) -> Result { + fn parse_hour_posix(&self) -> Result { let number = self .parse_number_with_upto_n_digits(2) - .map_err(HourPosixError::Parse)?; - let number = - i8::try_from(number).map_err(|_| HourPosixError::Range)?; + .map_err(|e| err!("invalid hour digits: {e}"))?; + let number = i8::try_from(number).map_err(|_| { + err!( + "hour `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 24) { - return Err(HourPosixError::Range); + return Err(err!( + "parsed hour `{number}`, but hour in \ + POSIX time zone must be in range `0..=24`", + )); } Ok(number) } @@ -1228,13 +1364,21 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first minute digit should occur. Upon success, the parser will be /// positioned immediately after the second minute digit. - fn parse_minute(&self) -> Result { + fn parse_minute(&self) -> Result { let number = self .parse_number_with_exactly_n_digits(2) - .map_err(MinuteError::Parse)?; - let number = i8::try_from(number).map_err(|_| MinuteError::Range)?; + .map_err(|e| err!("invalid minute digits: {e}"))?; + let number = i8::try_from(number).map_err(|_| { + err!( + "minute `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 59) { - return Err(MinuteError::Range); + return Err(err!( + "parsed minute `{number}`, but minute in \ + POSIX time zone must be in range `0..=59`", + )); } Ok(number) } @@ -1246,13 +1390,21 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first second digit should occur. Upon success, the parser will be /// positioned immediately after the second second digit. - fn parse_second(&self) -> Result { + fn parse_second(&self) -> Result { let number = self .parse_number_with_exactly_n_digits(2) - .map_err(SecondError::Parse)?; - let number = i8::try_from(number).map_err(|_| SecondError::Range)?; + .map_err(|e| err!("invalid second digits: {e}"))?; + let number = i8::try_from(number).map_err(|_| { + err!( + "second `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 59) { - return Err(SecondError::Range); + return Err(err!( + "parsed second `{number}`, but second in \ + POSIX time zone must be in range `0..=59`", + )); } Ok(number) } @@ -1268,20 +1420,27 @@ impl<'s> Parser<'s> { fn parse_number_with_exactly_n_digits( &self, n: usize, - ) -> Result { + ) -> Result { assert!(n >= 1, "numbers must have at least 1 digit"); + let start = self.pos(); let mut number: i32 = 0; - for _ in 0..n { + for i in 0..n { if self.is_done() { - return Err(NumberError::ExpectedLength); + return Err(err!("expected {n} digits, but found {i}")); } let byte = self.byte(); let digit = match byte.checked_sub(b'0') { None => { - return Err(NumberError::InvalidDigit); + return Err(err!( + "invalid digit, expected 0-9 but got {}", + Byte(byte), + )); } Some(digit) if digit > 9 => { - return Err(NumberError::InvalidDigit); + return Err(err!( + "invalid digit, expected 0-9 but got {}", + Byte(byte), + )) } Some(digit) => { debug_assert!((0..=9).contains(&digit)); @@ -1291,7 +1450,12 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or(NumberError::TooBig)?; + .ok_or_else(|| { + err!( + "number `{}` too big to parse into 64-bit integer", + Bytes(&self.tz[start..][..i]), + ) + })?; self.bump(); } Ok(number) @@ -1303,16 +1467,14 @@ impl<'s> Parser<'s> { /// This assumes that `n >= 1` and that the parser is positioned at the /// first digit. Upon success, the parser is position immediately after /// the last digit (which can be at most `n`). - fn parse_number_with_upto_n_digits( - &self, - n: usize, - ) -> Result { + fn parse_number_with_upto_n_digits(&self, n: usize) -> Result { assert!(n >= 1, "numbers must have at least 1 digit"); + let start = self.pos(); let mut number: i32 = 0; for i in 0..n { if self.is_done() || !self.byte().is_ascii_digit() { if i == 0 { - return Err(NumberError::Empty); + return Err(err!("invalid number, no digits found")); } break; } @@ -1320,7 +1482,12 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or(NumberError::TooBig)?; + .ok_or_else(|| { + err!( + "number `{}` too big to parse into 64-bit integer", + Bytes(&self.tz[start..][..i]), + ) + })?; self.bump(); } Ok(number) @@ -1333,20 +1500,26 @@ impl<'s> Parser<'s> { /// is consumed and returned. Moreover, if one exists, then this /// guarantees that it is not the last byte in the input. That is, upon /// success, it is valid to call `self.byte()`. - fn parse_optional_sign(&self) -> Result, OptionalSignError> { + fn parse_optional_sign(&self) -> Result, Error> { if self.is_done() { return Ok(None); } Ok(match self.byte() { b'-' => { if !self.bump() { - return Err(OptionalSignError::ExpectedDigitAfterMinus); + return Err(err!( + "expected digit after '-' sign, \ + but got end of input", + )); } Some(-1) } b'+' => { if !self.bump() { - return Err(OptionalSignError::ExpectedDigitAfterPlus); + return Err(err!( + "expected digit after '+' sign, \ + but got end of input", + )); } Some(1) } @@ -1404,717 +1577,11 @@ impl<'s> Parser<'s> { } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PosixTimeZoneError { - kind: ErrorKind, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum ErrorKind { - AbbreviationDst(AbbreviationError), - AbbreviationStd(AbbreviationError), - Empty, - ExpectedCommaAfterDst, - FoundDstNoRule, - FoundDstNoRuleWithOffset, - FoundEndAfterComma, - FoundRemaining, - OffsetDst(PosixOffsetError), - OffsetStd(PosixOffsetError), - Rule(PosixRuleError), -} - -impl core::fmt::Display for PosixTimeZoneError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::ErrorKind::*; - match self.kind { - AbbreviationDst(ref err) => { - f.write_str("failed to parse DST time zone abbreviation: ")?; - core::fmt::Display::fmt(err, f) - } - AbbreviationStd(ref err) => { - f.write_str( - "failed to parse standard time zone abbreviation: ", - )?; - core::fmt::Display::fmt(err, f) - } - Empty => f.write_str( - "an empty string is not a valid POSIX time zone \ - transition rule", - ), - ExpectedCommaAfterDst => f.write_str( - "expected `,` after parsing DST offset \ - in POSIX time zone string", - ), - FoundDstNoRule => f.write_str( - "found DST abbreviation in POSIX time zone string, \ - but no transition rule \ - (this is technically allowed by POSIX, but has \ - unspecified behavior)", - ), - FoundDstNoRuleWithOffset => f.write_str( - "found DST abbreviation and offset in POSIX time zone string, \ - but no transition rule \ - (this is technically allowed by POSIX, but has \ - unspecified behavior)", - ), - FoundEndAfterComma => f.write_str( - "after parsing DST offset in POSIX time zone string, \ - found end of string after a trailing `,`", - ), - FoundRemaining => f.write_str( - "expected entire POSIX TZ string to be a valid \ - time zone transition rule, but found data after \ - parsing a valid time zone transition rule", - ), - OffsetDst(ref err) => { - f.write_str("failed to parse DST offset: ")?; - core::fmt::Display::fmt(err, f) - } - OffsetStd(ref err) => { - f.write_str("failed to parse standard offset: ")?; - core::fmt::Display::fmt(err, f) - } - Rule(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for PosixTimeZoneError { - fn from(kind: ErrorKind) -> PosixTimeZoneError { - PosixTimeZoneError { kind } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixOffsetError { - HourPosix(HourPosixError), - IncompleteMinutes, - IncompleteSeconds, - Minute(MinuteError), - OptionalSign(OptionalSignError), - Second(SecondError), -} - -impl core::fmt::Display for PosixOffsetError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixOffsetError::*; - match *self { - HourPosix(ref err) => core::fmt::Display::fmt(err, f), - IncompleteMinutes => f.write_str( - "incomplete time in \ - POSIX time zone string (missing minutes)", - ), - IncompleteSeconds => f.write_str( - "incomplete time in \ - POSIX time zone string (missing seconds)", - ), - Minute(ref err) => core::fmt::Display::fmt(err, f), - Second(ref err) => core::fmt::Display::fmt(err, f), - OptionalSign(ref err) => { - f.write_str( - "failed to parse sign for time offset \ - POSIX time zone string", - )?; - core::fmt::Display::fmt(err, f) - } - } - } -} - -impl From for PosixOffsetError { - fn from(err: HourPosixError) -> PosixOffsetError { - PosixOffsetError::HourPosix(err) - } -} - -impl From for PosixOffsetError { - fn from(err: MinuteError) -> PosixOffsetError { - PosixOffsetError::Minute(err) - } -} - -impl From for PosixOffsetError { - fn from(err: OptionalSignError) -> PosixOffsetError { - PosixOffsetError::OptionalSign(err) - } -} - -impl From for PosixOffsetError { - fn from(err: SecondError) -> PosixOffsetError { - PosixOffsetError::Second(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixRuleError { - DateTimeEnd(PosixDateTimeError), - DateTimeStart(PosixDateTimeError), - ExpectedEnd, -} - -impl core::fmt::Display for PosixRuleError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixRuleError::*; - match *self { - DateTimeEnd(ref err) => { - f.write_str("failed to parse end of DST transition rule: ")?; - core::fmt::Display::fmt(err, f) - } - DateTimeStart(ref err) => { - f.write_str("failed to parse start of DST transition rule: ")?; - core::fmt::Display::fmt(err, f) - } - ExpectedEnd => f.write_str( - "expected end of DST rule after parsing the start \ - of the DST rule", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixDateTimeError { - Date(PosixDateError), - ExpectedTime, - Time(PosixTimeError), -} - -impl core::fmt::Display for PosixDateTimeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixDateTimeError::*; - match *self { - Date(ref err) => core::fmt::Display::fmt(err, f), - ExpectedTime => f.write_str( - "expected time specification after `/` following a date - specification in a POSIX time zone DST transition rule", - ), - Time(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for PosixDateTimeError { - fn from(err: PosixDateError) -> PosixDateTimeError { - PosixDateTimeError::Date(err) - } -} - -impl From for PosixDateTimeError { - fn from(err: PosixTimeError) -> PosixDateTimeError { - PosixDateTimeError::Time(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixDateError { - ExpectedJulianNoLeap, - ExpectedMonthWeekWeekday, - JulianLeap(PosixJulianLeapError), - JulianNoLeap(PosixJulianNoLeapError), - UnexpectedByte, - WeekdayOfMonth(WeekdayOfMonthError), -} - -impl core::fmt::Display for PosixDateError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixDateError::*; - match *self { - ExpectedJulianNoLeap => f.write_str( - "expected one-based Julian day after `J` in date \ - specification of a POSIX time zone DST \ - transition rule, but found the end of input", - ), - ExpectedMonthWeekWeekday => f.write_str( - "expected month-week-weekday after `M` in date \ - specification of a POSIX time zone DST \ - transition rule, but found the end of input", - ), - JulianLeap(ref err) => core::fmt::Display::fmt(err, f), - JulianNoLeap(ref err) => core::fmt::Display::fmt(err, f), - UnexpectedByte => f.write_str( - "expected `J`, a digit or `M` at the beginning of a date \ - specification of a POSIX time zone DST transition rule", - ), - WeekdayOfMonth(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for PosixDateError { - fn from(err: PosixJulianLeapError) -> PosixDateError { - PosixDateError::JulianLeap(err) - } -} - -impl From for PosixDateError { - fn from(err: PosixJulianNoLeapError) -> PosixDateError { - PosixDateError::JulianNoLeap(err) - } -} - -impl From for PosixDateError { - fn from(err: WeekdayOfMonthError) -> PosixDateError { - PosixDateError::WeekdayOfMonth(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixJulianNoLeapError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for PosixJulianNoLeapError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixJulianNoLeapError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid one-based Julian day digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed one-based Julian day, but it's not in supported \ - range of `1..=365`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixJulianLeapError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for PosixJulianLeapError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixJulianLeapError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid zero-based Julian day digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed zero-based Julian day, but it's not in supported \ - range of `0..=365`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum AbbreviationError { - Quoted(QuotedAbbreviationError), - Unquoted(UnquotedAbbreviationError), -} - -impl core::fmt::Display for AbbreviationError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::AbbreviationError::*; - match *self { - Quoted(ref err) => core::fmt::Display::fmt(err, f), - Unquoted(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum UnquotedAbbreviationError { - InvalidUtf8, - TooLong, - TooShort, -} - -impl core::fmt::Display for UnquotedAbbreviationError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::UnquotedAbbreviationError::*; - match *self { - InvalidUtf8 => f.write_str( - "unquoted time zone abbreviation must be valid UTF-8", - ), - TooLong => write!( - f, - "expected unquoted time zone abbreviation with at most \ - {} bytes, but found an abbreviation that is longer", - Abbreviation::capacity(), - ), - TooShort => f.write_str( - "expected unquoted time zone abbreviation to have length of \ - 3 or more bytes, but an abbreviation that is shorter", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum QuotedAbbreviationError { - InvalidUtf8, - TooLong, - TooShort, - UnexpectedEnd, - UnexpectedEndAfterOpening, - UnexpectedLastByte, -} - -impl core::fmt::Display for QuotedAbbreviationError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::QuotedAbbreviationError::*; - match *self { - InvalidUtf8 => f.write_str( - "quoted time zone abbreviation must be valid UTF-8", - ), - TooLong => write!( - f, - "expected quoted time zone abbreviation with at most \ - {} bytes, but found an abbreviation that is longer", - Abbreviation::capacity(), - ), - TooShort => f.write_str( - "expected quoted time zone abbreviation to have length of \ - 3 or more bytes, but an abbreviation that is shorter", - ), - UnexpectedEnd => f.write_str( - "found non-empty quoted time zone abbreviation, but \ - found end of input before an end-of-quoted abbreviation \ - `>` character", - ), - UnexpectedEndAfterOpening => f.write_str( - "found opening `<` quote for time zone abbreviation in \ - POSIX time zone transition rule, and expected a name \ - following it, but found the end of input instead", - ), - UnexpectedLastByte => f.write_str( - "found non-empty quoted time zone abbreviation, but \ - found did not find end-of-quoted abbreviation `>` \ - character", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum WeekdayOfMonthError { - ExpectedDayOfWeekAfterWeek, - ExpectedDotAfterMonth, - ExpectedDotAfterWeek, - ExpectedWeekAfterMonth, - Month(MonthError), - WeekOfMonth(WeekOfMonthError), - Weekday(WeekdayError), -} - -impl core::fmt::Display for WeekdayOfMonthError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::WeekdayOfMonthError::*; - match *self { - ExpectedDayOfWeekAfterWeek => f.write_str( - "expected day-of-week after week in POSIX time zone rule", - ), - ExpectedDotAfterMonth => { - f.write_str("expected `.` after month in POSIX time zone rule") - } - ExpectedWeekAfterMonth => f.write_str( - "expected week after month in POSIX time zone rule", - ), - ExpectedDotAfterWeek => { - f.write_str("expected `.` after week in POSIX time zone rule") - } - Month(ref err) => core::fmt::Display::fmt(err, f), - WeekOfMonth(ref err) => core::fmt::Display::fmt(err, f), - Weekday(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for WeekdayOfMonthError { - fn from(err: MonthError) -> WeekdayOfMonthError { - WeekdayOfMonthError::Month(err) - } -} - -impl From for WeekdayOfMonthError { - fn from(err: WeekOfMonthError) -> WeekdayOfMonthError { - WeekdayOfMonthError::WeekOfMonth(err) - } -} - -impl From for WeekdayOfMonthError { - fn from(err: WeekdayError) -> WeekdayOfMonthError { - WeekdayOfMonthError::Weekday(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixTimeError { - HourIana(HourIanaError), - HourPosix(HourPosixError), - IncompleteMinutes, - IncompleteSeconds, - Minute(MinuteError), - OptionalSign(OptionalSignError), - Second(SecondError), -} - -impl core::fmt::Display for PosixTimeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixTimeError::*; - match *self { - HourIana(ref err) => core::fmt::Display::fmt(err, f), - HourPosix(ref err) => core::fmt::Display::fmt(err, f), - IncompleteMinutes => f.write_str( - "incomplete time zone transition time in \ - POSIX time zone string (missing minutes)", - ), - IncompleteSeconds => f.write_str( - "incomplete time zone transition time in \ - POSIX time zone string (missing seconds)", - ), - Minute(ref err) => core::fmt::Display::fmt(err, f), - Second(ref err) => core::fmt::Display::fmt(err, f), - OptionalSign(ref err) => { - f.write_str( - "failed to parse sign for time zone transition time", - )?; - core::fmt::Display::fmt(err, f) - } - } - } -} - -impl From for PosixTimeError { - fn from(err: HourIanaError) -> PosixTimeError { - PosixTimeError::HourIana(err) - } -} - -impl From for PosixTimeError { - fn from(err: HourPosixError) -> PosixTimeError { - PosixTimeError::HourPosix(err) - } -} - -impl From for PosixTimeError { - fn from(err: MinuteError) -> PosixTimeError { - PosixTimeError::Minute(err) - } -} - -impl From for PosixTimeError { - fn from(err: OptionalSignError) -> PosixTimeError { - PosixTimeError::OptionalSign(err) - } -} - -impl From for PosixTimeError { - fn from(err: SecondError) -> PosixTimeError { - PosixTimeError::Second(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum MonthError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for MonthError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::MonthError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid month digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed month, but it's not in supported \ - range of `1..=12`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum WeekOfMonthError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for WeekOfMonthError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::WeekOfMonthError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid week-of-month digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed week-of-month, but it's not in supported \ - range of `1..=5`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum WeekdayError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for WeekdayError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::WeekdayError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid weekday digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed weekday, but it's not in supported \ - range of `0..=6` (with `0` corresponding to Sunday)", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum HourIanaError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for HourIanaError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::HourIanaError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid hour digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed hours, but it's not in supported \ - range of `-167..=167`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum HourPosixError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for HourPosixError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::HourPosixError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid hour digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed hours, but it's not in supported \ - range of `0..=24`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum MinuteError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for MinuteError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::MinuteError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid minute digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed minutes, but it's not in supported \ - range of `0..=59`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum SecondError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for SecondError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::SecondError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid second digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed seconds, but it's not in supported \ - range of `0..=59`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum NumberError { - Empty, - ExpectedLength, - InvalidDigit, - TooBig, -} - -impl core::fmt::Display for NumberError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::NumberError::*; - match *self { - Empty => f.write_str("invalid number, no digits found"), - ExpectedLength => f.write_str( - "expected a fixed number of digits, \ - but found incorrect number", - ), - InvalidDigit => f.write_str("expected digit in range `0..=9`"), - TooBig => f.write_str( - "parsed number too big to fit into a 32-bit signed integer", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum OptionalSignError { - ExpectedDigitAfterMinus, - ExpectedDigitAfterPlus, -} - -impl core::fmt::Display for OptionalSignError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::OptionalSignError::*; - match *self { - ExpectedDigitAfterMinus => f.write_str( - "expected digit after `-` sign, \ - but got end of input", - ), - ExpectedDigitAfterPlus => f.write_str( - "expected digit after `+` sign, \ - but got end of input", - ), - } - } -} - +// Tests require parsing, and parsing requires alloc. #[cfg(test)] mod tests { + use alloc::string::ToString; + use super::*; fn posix_time_zone( @@ -2122,25 +1589,21 @@ mod tests { ) -> PosixTimeZone { let input = input.as_ref(); let tz = PosixTimeZone::parse(input).unwrap(); - { - use alloc::string::ToString; - - // While we're here, assert that converting the TZ back - // to a string matches what we got. In the original version - // of the POSIX TZ parser, we were very meticulous about - // capturing the exact AST of the time zone. But I've - // since simplified the data structure considerably such - // that it is lossy in terms of what was actually parsed - // (but of course, not lossy in terms of the semantic - // meaning of the time zone). - // - // So to account for this, we serialize to a string and - // then parse it back. We should get what we started with. - let reparsed = - PosixTimeZone::parse(tz.to_string().as_bytes()).unwrap(); - assert_eq!(tz, reparsed); - assert_eq!(tz.to_string(), reparsed.to_string()); - } + // While we're here, assert that converting the TZ back + // to a string matches what we got. In the original version + // of the POSIX TZ parser, we were very meticulous about + // capturing the exact AST of the time zone. But I've + // since simplified the data structure considerably such + // that it is lossy in terms of what was actually parsed + // (but of course, not lossy in terms of the semantic + // meaning of the time zone). + // + // So to account for this, we serialize to a string and + // then parse it back. We should get what we started with. + let reparsed = + PosixTimeZone::parse(tz.to_string().as_bytes()).unwrap(); + assert_eq!(tz, reparsed); + assert_eq!(tz.to_string(), reparsed.to_string()); tz } diff --git a/crates/jiff-static/src/shared/tzif.rs b/crates/jiff-static/src/shared/tzif.rs index 1b60a78..36d69dd 100644 --- a/crates/jiff-static/src/shared/tzif.rs +++ b/crates/jiff-static/src/shared/tzif.rs @@ -5,6 +5,8 @@ use alloc::{string::String, vec}; use super::{ util::{ array_str::Abbreviation, + error::{err, Error}, + escape::{Byte, Bytes}, itime::{IOffset, ITimestamp}, }, PosixTimeZone, TzifDateTime, TzifFixed, TzifIndicator, TzifLocalTimeType, @@ -57,7 +59,7 @@ const FATTEN_UP_TO_YEAR: i16 = 2038; // // For "normal" cases, there should be at most two transitions per // year. So this limit permits 300/2=150 years of transition data. -// (Although we won't go above `FATTEN_UP_TO_YEAR`. See above.) +// (Although we won't go above 2036. See above.) const FATTEN_MAX_TRANSITIONS: usize = 300; impl TzifOwned { @@ -78,11 +80,11 @@ impl TzifOwned { pub(crate) fn parse( name: Option, bytes: &[u8], - ) -> Result { + ) -> Result { let original = bytes; let name = name.into(); - let (header32, rest) = - Header::parse(4, bytes).map_err(TzifErrorKind::Header32)?; + let (header32, rest) = Header::parse(4, bytes) + .map_err(|e| err!("failed to parse 32-bit header: {e}"))?; let (mut tzif, rest) = if header32.version == 0 { TzifOwned::parse32(name, header32, rest)? } else { @@ -115,7 +117,7 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> Result<(TzifOwned, &'b [u8]), TzifError> { + ) -> Result<(TzifOwned, &'b [u8]), Error> { let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -146,11 +148,14 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> Result<(TzifOwned, &'b [u8]), TzifError> { - let (_, rest) = - try_split_at(SplitAtError::V1, bytes, header32.data_block_len()?)?; - let (header64, rest) = - Header::parse(8, rest).map_err(TzifErrorKind::Header64)?; + ) -> Result<(TzifOwned, &'b [u8]), Error> { + let (_, rest) = try_split_at( + "V1 TZif data block", + bytes, + header32.data_block_len()?, + )?; + let (header64, rest) = Header::parse(8, rest) + .map_err(|e| err!("failed to parse 64-bit header: {e}"))?; let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -185,9 +190,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TzifError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::TransitionTimes, + "transition times data block", bytes, header.transition_times_len()?, )?; @@ -237,17 +242,21 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TransitionTypeError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::TransitionTypes, + "transition types data block", bytes, - header.transition_types_len(), + header.transition_types_len()?, )?; // We skip the first transition because it is our minimum dummy // transition. for (transition_index, &type_index) in (1..).zip(bytes) { if usize::from(type_index) >= header.tzh_typecnt { - return Err(TransitionTypeError::ExceedsLocalTimeTypes); + return Err(err!( + "found transition type index {type_index}, + but there are only {} local time types", + header.tzh_typecnt, + )); } self.transitions.infos[transition_index].type_index = type_index; } @@ -258,9 +267,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TzifError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::LocalTimeTypes, + "local time types data block", bytes, header.local_time_types_len()?, )?; @@ -268,8 +277,8 @@ impl TzifOwned { while let Some(chunk) = it.next() { let offset = from_be_bytes_i32(&chunk[..4]); if !(OFFSET_MIN <= offset && offset <= OFFSET_MAX) { - return Err(TzifError::from( - LocalTimeTypeError::InvalidOffset { offset }, + return Err(err!( + "found local time type with out-of-bounds offset: {offset}" )); } let is_dst = chunk[4] == 1; @@ -289,30 +298,49 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TimeZoneDesignatorError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::TimeZoneDesignations, + "time zone designations data block", bytes, - header.time_zone_designations_len(), + header.time_zone_designations_len()?, )?; - self.fixed.designations = String::from_utf8(bytes.to_vec()) - .map_err(|_| TimeZoneDesignatorError::InvalidUtf8)?; + self.fixed.designations = + String::from_utf8(bytes.to_vec()).map_err(|_| { + err!( + "time zone designations are not valid UTF-8: {:?}", + Bytes(bytes), + ) + })?; // Holy hell, this is brutal. The boundary conditions are crazy. - for typ in self.types.iter_mut() { + for (i, typ) in self.types.iter_mut().enumerate() { let start = usize::from(typ.designation.0); - let suffix = self - .fixed - .designations - .get(start..) - .ok_or(TimeZoneDesignatorError::InvalidStart)?; - let len = suffix - .find('\x00') - .ok_or(TimeZoneDesignatorError::MissingNul)?; - let end = start - .checked_add(len) - .ok_or(TimeZoneDesignatorError::InvalidLength)?; - typ.designation.1 = u8::try_from(end) - .map_err(|_| TimeZoneDesignatorError::InvalidEnd)?; + let Some(suffix) = self.fixed.designations.get(start..) else { + return Err(err!( + "local time type {i} has designation index of {start}, \ + but cannot be more than {}", + self.fixed.designations.len(), + )); + }; + let Some(len) = suffix.find('\x00') else { + return Err(err!( + "local time type {i} has designation index of {start}, \ + but could not find NUL terminator after it in \ + designations: {:?}", + self.fixed.designations, + )); + }; + let Some(end) = start.checked_add(len) else { + return Err(err!( + "local time type {i} has designation index of {start}, \ + but its length {len} is too big", + )); + }; + typ.designation.1 = u8::try_from(end).map_err(|_| { + err!( + "local time type {i} has designation range of \ + {start}..{end}, but end is too big", + ) + })?; } Ok(rest) } @@ -325,9 +353,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TzifError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::LeapSeconds, + "leap seconds data block", bytes, header.leap_second_len()?, )?; @@ -353,16 +381,16 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], IndicatorError> { + ) -> Result<&'b [u8], Error> { let (std_wall_bytes, rest) = try_split_at( - SplitAtError::StandardWallIndicators, + "standard/wall indicators data block", bytes, - header.standard_wall_len(), + header.standard_wall_len()?, )?; let (ut_local_bytes, rest) = try_split_at( - SplitAtError::UTLocalIndicators, + "UT/local indicators data block", rest, - header.ut_local_len(), + header.ut_local_len()?, )?; if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() { // This is a weird case, but technically possible only if all @@ -370,8 +398,14 @@ impl TzifOwned { // because it would require the corresponding std/wall indicator // to be 1 too. Which it can't be, because there aren't any. So // we just check that they're all zeros. - if ut_local_bytes.iter().any(|&byte| byte != 0) { - return Err(IndicatorError::UtLocalNonZero); + for (i, &byte) in ut_local_bytes.iter().enumerate() { + if byte != 0 { + return Err(err!( + "found UT/local indicator '{byte}' for local time \ + type {i}, but it must be 0 since all std/wall \ + indicators are 0", + )); + } } } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() { for (i, &byte) in std_wall_bytes.iter().enumerate() { @@ -382,7 +416,10 @@ impl TzifOwned { } else if byte == 1 { TzifIndicator::LocalStandard } else { - return Err(IndicatorError::InvalidStdWallIndicator); + return Err(err!( + "found invalid std/wall indicator '{byte}' for \ + local time type {i}, it must be 0 or 1", + )); }; } } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() { @@ -396,9 +433,18 @@ impl TzifOwned { (1, 0) => TzifIndicator::LocalStandard, (1, 1) => TzifIndicator::UTStandard, (0, 1) => { - return Err(IndicatorError::InvalidUtWallCombination); + return Err(err!( + "found illegal ut-wall combination for \ + local time type {i}, only local-wall, \ + local-standard and ut-standard are allowed", + )) + } + _ => { + return Err(err!( + "found illegal std/wall or ut/local value for \ + local time type {i}, each must be 0 or 1", + )) } - _ => return Err(IndicatorError::InvalidCombination), }; } } else { @@ -415,31 +461,42 @@ impl TzifOwned { &mut self, _header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], FooterError> { + ) -> Result<&'b [u8], Error> { if bytes.is_empty() { - return Err(FooterError::UnexpectedEnd); + return Err(err!( + "invalid V2+ TZif footer, expected \\n, \ + but found unexpected end of data", + )); } if bytes[0] != b'\n' { - return Err(FooterError::MismatchEnd); + return Err(err!( + "invalid V2+ TZif footer, expected {:?}, but found {:?}", + Byte(b'\n'), + Byte(bytes[0]), + )); } let bytes = &bytes[1..]; // Only scan up to 1KB for a NUL terminator in case we somehow got // passed a huge block of bytes. let toscan = &bytes[..bytes.len().min(1024)]; - let nlat = toscan - .iter() - .position(|&b| b == b'\n') - .ok_or(FooterError::TerminatorNotFound)?; + let Some(nlat) = toscan.iter().position(|&b| b == b'\n') else { + return Err(err!( + "invalid V2 TZif footer, could not find {:?} \ + terminator in: {:?}", + Byte(b'\n'), + Bytes(toscan), + )); + }; let (bytes, rest) = bytes.split_at(nlat); if !bytes.is_empty() { // We could in theory limit TZ strings to their strict POSIX // definition here for TZif V2, but I don't think there is any // harm in allowing the extensions in V2 formatted TZif data. Note - // that GNU tooling allows it via the `TZ` environment variable + // that the GNU tooling allow it via the `TZ` environment variable // even though POSIX doesn't specify it. This all seems okay to me // because the V3+ extension is a strict superset of functionality. - let posix_tz = PosixTimeZone::parse(bytes) - .map_err(FooterError::InvalidPosixTz)?; + let posix_tz = + PosixTimeZone::parse(bytes).map_err(|e| err!("{e}"))?; self.fixed.posix_tz = Some(posix_tz); } Ok(&rest[1..]) @@ -452,9 +509,7 @@ impl TzifOwned { /// RFC 8536 says, "If the string is nonempty and one or more /// transitions appear in the version 2+ data, the string MUST be /// consistent with the last version 2+ transition." - fn verify_posix_time_zone_consistency( - &self, - ) -> Result<(), InconsistentPosixTimeZoneError> { + fn verify_posix_time_zone_consistency(&self) -> Result<(), Error> { // We need to be a little careful, since we always have at least one // transition (accounting for the dummy `Timestamp::MIN` transition). // So if we only have 1 transition and a POSIX TZ string, then we @@ -481,13 +536,35 @@ impl TzifOwned { let (ioff, abbrev, is_dst) = tz.to_offset_info(ITimestamp::from_second(*last)); if ioff.second != typ.offset { - return Err(InconsistentPosixTimeZoneError::Offset); + return Err(err!( + "expected last transition to have DST offset \ + of {expected_offset}, but got {got_offset} \ + according to POSIX TZ string {tz}", + expected_offset = typ.offset, + got_offset = ioff.second, + tz = tz, + )); } if is_dst != typ.is_dst { - return Err(InconsistentPosixTimeZoneError::Dst); + return Err(err!( + "expected last transition to have is_dst={expected_dst}, \ + but got is_dst={got_dst} according to POSIX TZ \ + string {tz}", + expected_dst = typ.is_dst, + got_dst = is_dst, + tz = tz, + )); } if abbrev != self.designation(&typ) { - return Err(InconsistentPosixTimeZoneError::Designation); + return Err(err!( + "expected last transition to have \ + designation={expected_abbrev}, \ + but got designation={got_abbrev} according to POSIX TZ \ + string {tz}", + expected_abbrev = self.designation(&typ), + got_abbrev = abbrev, + tz = tz, + )); } Ok(()) } @@ -503,6 +580,7 @@ impl TzifOwned { /// and the timestamps in TZif data are, of course, all in UTC.) fn add_civil_datetimes_to_transitions(&mut self) { fn to_datetime(timestamp: i64, offset: i32) -> TzifDateTime { + use crate::shared::util::itime::{IOffset, ITimestamp}; let its = ITimestamp { second: timestamp, nanosecond: 0 }; let ioff = IOffset { second: offset }; let dt = its.to_datetime(ioff); @@ -768,14 +846,14 @@ impl Header { fn parse( time_size: usize, bytes: &[u8], - ) -> Result<(Header, &[u8]), HeaderError> { + ) -> Result<(Header, &[u8]), Error> { assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8"); if bytes.len() < 44 { - return Err(HeaderError::TooShort); + return Err(err!("invalid header: too short")); } let (magic, rest) = bytes.split_at(4); if magic != b"TZif" { - return Err(HeaderError::MismatchMagic); + return Err(err!("invalid header: magic bytes mismatch")); } let (version, rest) = rest.split_at(1); let (_reserved, rest) = rest.split_at(15); @@ -787,42 +865,40 @@ impl Header { let (tzh_typecnt_bytes, rest) = rest.split_at(4); let (tzh_charcnt_bytes, rest) = rest.split_at(4); - let tzh_ttisutcnt = - from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Ut, convert: e } - })?; - let tzh_ttisstdcnt = - from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Std, convert: e } - })?; - let tzh_leapcnt = - from_be_bytes_u32_to_usize(tzh_leapcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Leap, convert: e } - })?; - let tzh_timecnt = - from_be_bytes_u32_to_usize(tzh_timecnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Time, convert: e } - })?; - let tzh_typecnt = - from_be_bytes_u32_to_usize(tzh_typecnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Type, convert: e } - })?; - let tzh_charcnt = - from_be_bytes_u32_to_usize(tzh_charcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Char, convert: e } - })?; + let tzh_ttisutcnt = from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes) + .map_err(|e| err!("failed to parse tzh_ttisutcnt: {e}"))?; + let tzh_ttisstdcnt = from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes) + .map_err(|e| err!("failed to parse tzh_ttisstdcnt: {e}"))?; + let tzh_leapcnt = from_be_bytes_u32_to_usize(tzh_leapcnt_bytes) + .map_err(|e| err!("failed to parse tzh_leapcnt: {e}"))?; + let tzh_timecnt = from_be_bytes_u32_to_usize(tzh_timecnt_bytes) + .map_err(|e| err!("failed to parse tzh_timecnt: {e}"))?; + let tzh_typecnt = from_be_bytes_u32_to_usize(tzh_typecnt_bytes) + .map_err(|e| err!("failed to parse tzh_typecnt: {e}"))?; + let tzh_charcnt = from_be_bytes_u32_to_usize(tzh_charcnt_bytes) + .map_err(|e| err!("failed to parse tzh_charcnt: {e}"))?; if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt { - return Err(HeaderError::MismatchUtType); + return Err(err!( + "expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \ + or equal to tzh_typecnt={tzh_typecnt}", + )); } if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt { - return Err(HeaderError::MismatchStdType); + return Err(err!( + "expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \ + or equal to tzh_typecnt={tzh_typecnt}", + )); } if tzh_typecnt < 1 { - return Err(HeaderError::ZeroType); + return Err(err!( + "expected tzh_typecnt={tzh_typecnt} to be at least 1", + )); } if tzh_charcnt < 1 { - return Err(HeaderError::ZeroChar); + return Err(err!( + "expected tzh_charcnt={tzh_charcnt} to be at least 1", + )); } let header = Header { @@ -853,513 +929,64 @@ impl Header { /// /// This is useful for, e.g., skipping over the 32-bit V1 data block in /// V2+ TZif formatted files. - fn data_block_len(&self) -> Result { + fn data_block_len(&self) -> Result { let a = self.transition_times_len()?; - let b = self.transition_types_len(); + let b = self.transition_types_len()?; let c = self.local_time_types_len()?; - let d = self.time_zone_designations_len(); + let d = self.time_zone_designations_len()?; let e = self.leap_second_len()?; - let f = self.standard_wall_len(); - let g = self.ut_local_len(); + let f = self.standard_wall_len()?; + let g = self.ut_local_len()?; a.checked_add(b) .and_then(|z| z.checked_add(c)) .and_then(|z| z.checked_add(d)) .and_then(|z| z.checked_add(e)) .and_then(|z| z.checked_add(f)) .and_then(|z| z.checked_add(g)) - .ok_or(HeaderError::InvalidDataBlock { version: self.version }) + .ok_or_else(|| { + err!( + "length of data block in V{} tzfile is too big", + self.version + ) + }) } - fn transition_times_len(&self) -> Result { - self.tzh_timecnt - .checked_mul(self.time_size) - .ok_or(HeaderError::InvalidTimeCount) + fn transition_times_len(&self) -> Result { + self.tzh_timecnt.checked_mul(self.time_size).ok_or_else(|| { + err!("tzh_timecnt value {} is too big", self.tzh_timecnt) + }) } - fn transition_types_len(&self) -> usize { - self.tzh_timecnt + fn transition_types_len(&self) -> Result { + Ok(self.tzh_timecnt) } - fn local_time_types_len(&self) -> Result { - self.tzh_typecnt.checked_mul(6).ok_or(HeaderError::InvalidTypeCount) + fn local_time_types_len(&self) -> Result { + self.tzh_typecnt.checked_mul(6).ok_or_else(|| { + err!("tzh_typecnt value {} is too big", self.tzh_typecnt) + }) } - fn time_zone_designations_len(&self) -> usize { - self.tzh_charcnt + fn time_zone_designations_len(&self) -> Result { + Ok(self.tzh_charcnt) } - fn leap_second_len(&self) -> Result { + fn leap_second_len(&self) -> Result { let record_len = self .time_size .checked_add(4) .expect("4-or-8 plus 4 always fits in usize"); - self.tzh_leapcnt - .checked_mul(record_len) - .ok_or(HeaderError::InvalidLeapSecondCount) + self.tzh_leapcnt.checked_mul(record_len).ok_or_else(|| { + err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt) + }) } - fn standard_wall_len(&self) -> usize { - self.tzh_ttisstdcnt + fn standard_wall_len(&self) -> Result { + Ok(self.tzh_ttisstdcnt) } - fn ut_local_len(&self) -> usize { - self.tzh_ttisutcnt - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct TzifError { - kind: TzifErrorKind, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum TzifErrorKind { - Footer(FooterError), - Header(HeaderError), - Header32(HeaderError), - Header64(HeaderError), - InconsistentPosixTimeZone(InconsistentPosixTimeZoneError), - Indicator(IndicatorError), - LocalTimeType(LocalTimeTypeError), - SplitAt(SplitAtError), - TimeZoneDesignator(TimeZoneDesignatorError), - TransitionType(TransitionTypeError), -} - -impl std::error::Error for TzifError {} - -impl core::fmt::Display for TzifError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::TzifErrorKind::*; - match self.kind { - Footer(ref err) => { - f.write_str("invalid TZif footer: ")?; - err.fmt(f) - } - Header(ref err) => { - f.write_str("invalid TZif header: ")?; - err.fmt(f) - } - Header32(ref err) => { - f.write_str("invalid 32-bit TZif header: ")?; - err.fmt(f) - } - Header64(ref err) => { - f.write_str("invalid 64-bit TZif header: ")?; - err.fmt(f) - } - InconsistentPosixTimeZone(ref err) => { - f.write_str( - "found inconsistency with \ - POSIX time zone transition rule \ - in TZif file footer: ", - )?; - err.fmt(f) - } - Indicator(ref err) => { - f.write_str("failed to parse indicators: ")?; - err.fmt(f) - } - LocalTimeType(ref err) => { - f.write_str("failed to parse local time types: ")?; - err.fmt(f) - } - SplitAt(ref err) => err.fmt(f), - TimeZoneDesignator(ref err) => { - f.write_str("failed to parse time zone designators: ")?; - err.fmt(f) - } - TransitionType(ref err) => { - f.write_str("failed to parse time zone transition types: ")?; - err.fmt(f) - } - } - } -} - -impl From for TzifError { - fn from(kind: TzifErrorKind) -> TzifError { - TzifError { kind } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum TransitionTypeError { - ExceedsLocalTimeTypes, - Split(SplitAtError), -} - -impl From for TzifError { - fn from(err: TransitionTypeError) -> TzifError { - TzifErrorKind::TransitionType(err).into() - } -} - -impl From for TransitionTypeError { - fn from(err: SplitAtError) -> TransitionTypeError { - TransitionTypeError::Split(err) - } -} - -impl core::fmt::Display for TransitionTypeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::TransitionTypeError::*; - - match *self { - ExceedsLocalTimeTypes => f.write_str( - "found time zone transition type index \ - that exceeds the number of local time types", - ), - Split(ref err) => err.fmt(f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum LocalTimeTypeError { - InvalidOffset { offset: i32 }, - Split(SplitAtError), -} - -impl From for TzifError { - fn from(err: LocalTimeTypeError) -> TzifError { - TzifErrorKind::LocalTimeType(err).into() - } -} - -impl From for LocalTimeTypeError { - fn from(err: SplitAtError) -> LocalTimeTypeError { - LocalTimeTypeError::Split(err) - } -} - -impl core::fmt::Display for LocalTimeTypeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::LocalTimeTypeError::*; - - match *self { - InvalidOffset { offset } => write!( - f, - "found local time type with \ - out-of-bounds time zone offset: {offset}, \ - Jiff's allowed range is `{OFFSET_MIN}..={OFFSET_MAX}`" - ), - Split(ref err) => err.fmt(f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum TimeZoneDesignatorError { - InvalidEnd, - InvalidLength, - InvalidStart, - InvalidUtf8, - MissingNul, - Split(SplitAtError), -} - -impl From for TzifError { - fn from(err: TimeZoneDesignatorError) -> TzifError { - TzifErrorKind::TimeZoneDesignator(err).into() - } -} - -impl From for TimeZoneDesignatorError { - fn from(err: SplitAtError) -> TimeZoneDesignatorError { - TimeZoneDesignatorError::Split(err) - } -} - -impl core::fmt::Display for TimeZoneDesignatorError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::TimeZoneDesignatorError::*; - - match *self { - InvalidEnd => f.write_str( - "found invalid end of time zone designator \ - for local time type", - ), - InvalidLength => f.write_str( - "found invalid length of time zone designator \ - for local time type", - ), - InvalidStart => f.write_str( - "found invalid start of time zone designator \ - for local time type", - ), - InvalidUtf8 => { - f.write_str("found invalid UTF-8 in time zone designators") - } - MissingNul => f.write_str( - "could not find NUL terminator for time zone designator", - ), - Split(ref err) => err.fmt(f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum IndicatorError { - InvalidCombination, - InvalidStdWallIndicator, - InvalidUtWallCombination, - Split(SplitAtError), - UtLocalNonZero, -} - -impl From for TzifError { - fn from(err: IndicatorError) -> TzifError { - TzifErrorKind::Indicator(err).into() - } -} - -impl From for IndicatorError { - fn from(err: SplitAtError) -> IndicatorError { - IndicatorError::Split(err) - } -} - -impl core::fmt::Display for IndicatorError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::IndicatorError::*; - - match *self { - InvalidCombination => f.write_str( - "found invalid std/wall or UT/local value for \ - local time type, each must be 0 or 1", - ), - InvalidStdWallIndicator => f.write_str( - "found invalid std/wall indicator, \ - expected it to be 0 or 1", - ), - InvalidUtWallCombination => f.write_str( - "found invalid UT-wall combination for \ - local time type, only local-wall, \ - local-standard and UT-standard are allowed", - ), - Split(ref err) => err.fmt(f), - UtLocalNonZero => f.write_str( - "found non-zero UT/local indicator, \ - but all such indicators should be zero", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum InconsistentPosixTimeZoneError { - Designation, - Dst, - Offset, -} - -impl From for TzifError { - fn from(err: InconsistentPosixTimeZoneError) -> TzifError { - TzifErrorKind::InconsistentPosixTimeZone(err).into() - } -} - -impl core::fmt::Display for InconsistentPosixTimeZoneError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::InconsistentPosixTimeZoneError::*; - - match *self { - Designation => f.write_str( - "expected last transition in TZif file to have \ - a time zone abbreviation matching the abbreviation \ - derived from the POSIX time zone transition rule", - ), - Dst => f.write_str( - "expected last transition in TZif file to have \ - a DST status matching the status derived from the \ - POSIX time zone transition rule", - ), - Offset => f.write_str( - "expected last transition in TZif file to have \ - DST offset matching the offset derived from the \ - POSIX time zone transition rule", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum FooterError { - InvalidPosixTz(crate::shared::posix::PosixTimeZoneError), - MismatchEnd, - TerminatorNotFound, - UnexpectedEnd, -} - -impl From for TzifError { - fn from(err: FooterError) -> TzifError { - TzifErrorKind::Footer(err).into() - } -} - -impl core::fmt::Display for FooterError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::FooterError::*; - - match *self { - InvalidPosixTz(ref err) => { - f.write_str("invalid POSIX time zone transition rule")?; - core::fmt::Display::fmt(err, f) - } - MismatchEnd => f.write_str( - "expected to find `\\n` at the beginning of \ - the TZif file footer, \ - but found something else instead", - ), - TerminatorNotFound => f.write_str( - "expected to find `\\n` terminating \ - the TZif file footer, \ - but no line terminator could be found", - ), - UnexpectedEnd => f.write_str( - "expected to find `\\n` at the beginning of \ - the TZif file footer, \ - but found unexpected end of data", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum HeaderError { - InvalidDataBlock { version: u8 }, - InvalidLeapSecondCount, - InvalidTimeCount, - InvalidTypeCount, - MismatchMagic, - MismatchStdType, - MismatchUtType, - ParseCount { kind: CountKind, convert: U32UsizeError }, - TooShort, - ZeroChar, - ZeroType, -} - -impl From for TzifError { - fn from(err: HeaderError) -> TzifError { - TzifErrorKind::Header(err).into() - } -} - -impl core::fmt::Display for HeaderError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::HeaderError::*; - - match *self { - InvalidDataBlock { version } => write!( - f, - "length of data block in V{version} TZif file is too big", - ), - InvalidLeapSecondCount => { - f.write_str("number of leap seconds is too big") - } - InvalidTimeCount => { - f.write_str("number of transition times is too big") - } - InvalidTypeCount => { - f.write_str("number of local time types is too big") - } - MismatchMagic => f.write_str("magic bytes mismatch"), - MismatchStdType => f.write_str( - "expected number of standard/wall indicators to be zero \ - or equal to the number of local time types", - ), - MismatchUtType => f.write_str( - "expected number of UT/local indicators to be zero \ - or equal to the number of local time types", - ), - ParseCount { ref kind, ref convert } => { - write!(f, "failed to parse `{kind}`: {convert}") - } - TooShort => f.write_str("too short"), - ZeroChar => f.write_str( - "expected number of time zone abbreviations fo be at least 1", - ), - ZeroType => f.write_str( - "expected number of local time types fo be at least 1", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum CountKind { - Ut, - Std, - Leap, - Time, - Type, - Char, -} - -impl core::fmt::Display for CountKind { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::CountKind::*; - match *self { - Ut => f.write_str("tzh_ttisutcnt"), - Std => f.write_str("tzh_ttisstdcnt"), - Leap => f.write_str("tzh_leapcnt"), - Time => f.write_str("tzh_timecnt"), - Type => f.write_str("tzh_typecnt"), - Char => f.write_str("tzh_charcnt"), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum SplitAtError { - V1, - LeapSeconds, - LocalTimeTypes, - StandardWallIndicators, - TimeZoneDesignations, - TransitionTimes, - TransitionTypes, - UTLocalIndicators, -} - -impl From for TzifError { - fn from(err: SplitAtError) -> TzifError { - TzifErrorKind::SplitAt(err).into() - } -} - -impl core::fmt::Display for SplitAtError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::SplitAtError::*; - - f.write_str("expected bytes for '")?; - f.write_str(match *self { - V1 => "v1 TZif", - LeapSeconds => "leap seconds", - LocalTimeTypes => "local time types", - StandardWallIndicators => "standard/wall indicators", - TimeZoneDesignations => "time zone designations", - TransitionTimes => "transition times", - TransitionTypes => "transition types", - UTLocalIndicators => "UT/local indicators", - })?; - f.write_str("data block', but did not find enough bytes")?; - Ok(()) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct U32UsizeError; - -impl core::fmt::Display for U32UsizeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!( - f, - "failed to parse integer because it is bigger than `{max}`", - max = usize::MAX, - ) + fn ut_local_len(&self) -> Result { + Ok(self.tzh_ttisutcnt) } } @@ -1369,12 +996,16 @@ impl core::fmt::Display for U32UsizeError { /// returned. The error message will include the `what` string given, which is /// meant to describe the thing being split. fn try_split_at<'b>( - what: SplitAtError, + what: &'static str, bytes: &'b [u8], at: usize, -) -> Result<(&'b [u8], &'b [u8]), SplitAtError> { +) -> Result<(&'b [u8], &'b [u8]), Error> { if at > bytes.len() { - Err(what) + Err(err!( + "expected at least {at} bytes for {what}, \ + but found only {} bytes", + bytes.len(), + )) } else { Ok(bytes.split_at(at)) } @@ -1391,9 +1022,14 @@ fn try_split_at<'b>( /// /// This errors if the `u32` parsed from the given bytes cannot fit in a /// `usize`. -fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result { +fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result { let n = from_be_bytes_u32(bytes); - usize::try_from(n).map_err(|_| U32UsizeError) + usize::try_from(n).map_err(|_| { + err!( + "failed to parse integer {n} (too big, max allowed is {}", + usize::MAX + ) + }) } /// Interprets the given slice as an unsigned 32-bit big endian integer and diff --git a/crates/jiff-static/src/shared/util/escape.rs b/crates/jiff-static/src/shared/util/escape.rs index e5b182b..5ed8cd1 100644 --- a/crates/jiff-static/src/shared/util/escape.rs +++ b/crates/jiff-static/src/shared/util/escape.rs @@ -17,7 +17,6 @@ use super::utf8; pub(crate) struct Byte(pub u8); impl core::fmt::Display for Byte { - #[inline(never)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { if self.0 == b' ' { return write!(f, " "); @@ -38,7 +37,6 @@ impl core::fmt::Display for Byte { } impl core::fmt::Debug for Byte { - #[inline(never)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "\"")?; core::fmt::Display::fmt(self, f)?; @@ -56,16 +54,15 @@ impl core::fmt::Debug for Byte { pub(crate) struct Bytes<'a>(pub &'a [u8]); impl<'a> core::fmt::Display for Bytes<'a> { - #[inline(never)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { // This is a sad re-implementation of a similar impl found in bstr. let mut bytes = self.0; while let Some(result) = utf8::decode(bytes) { let ch = match result { Ok(ch) => ch, - Err(err) => { + Err(errant_bytes) => { // The decode API guarantees `errant_bytes` is non-empty. - write!(f, r"\x{:02x}", err.as_slice()[0])?; + write!(f, r"\x{:02x}", errant_bytes[0])?; bytes = &bytes[1..]; continue; } @@ -84,7 +81,6 @@ impl<'a> core::fmt::Display for Bytes<'a> { } impl<'a> core::fmt::Debug for Bytes<'a> { - #[inline(never)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "\"")?; core::fmt::Display::fmt(self, f)?; @@ -92,34 +88,3 @@ impl<'a> core::fmt::Debug for Bytes<'a> { Ok(()) } } - -/// A helper for repeating a single byte utilizing `Byte`. -/// -/// This is limited to repeating a byte up to `u8::MAX` times in order -/// to reduce its size overhead. And in practice, Jiff just doesn't -/// need more than this (at time of writing, 2025-11-29). -pub(crate) struct RepeatByte { - pub(crate) byte: u8, - pub(crate) count: u8, -} - -impl core::fmt::Display for RepeatByte { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - for _ in 0..self.count { - write!(f, "{}", Byte(self.byte))?; - } - Ok(()) - } -} - -impl core::fmt::Debug for RepeatByte { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "\"")?; - core::fmt::Display::fmt(self, f)?; - write!(f, "\"")?; - Ok(()) - } -} - diff --git a/crates/jiff-static/src/shared/util/itime.rs b/crates/jiff-static/src/shared/util/itime.rs index c919a26..ba012c6 100644 --- a/crates/jiff-static/src/shared/util/itime.rs +++ b/crates/jiff-static/src/shared/util/itime.rs @@ -24,6 +24,8 @@ they are internal types. Specifically, to distinguish them from Jiff's public types. For example, `Date` versus `IDate`. */ +use super::error::{err, Error}; + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] pub(crate) struct ITimestamp { pub(crate) second: i64, @@ -141,13 +143,11 @@ impl IDateTime { pub(crate) fn checked_add_seconds( &self, seconds: i32, - ) -> Result { - let day_second = self - .time - .to_second() - .second - .checked_add(seconds) - .ok_or_else(|| RangeError::DateTimeSeconds)?; + ) -> Result { + let day_second = + self.time.to_second().second.checked_add(seconds).ok_or_else( + || err!("adding `{seconds}s` to datetime overflowed"), + )?; let days = day_second.div_euclid(86400); let second = day_second.rem_euclid(86400); let date = self.date.checked_add_days(days)?; @@ -162,8 +162,8 @@ pub(crate) struct IEpochDay { } impl IEpochDay { - pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; - pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; + const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; + const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; /// Converts days since the Unix epoch to a Gregorian date. /// @@ -219,17 +219,20 @@ impl IEpochDay { /// If this would overflow an `i32` or result in an out-of-bounds epoch /// day, then this returns an error. #[inline] - pub(crate) fn checked_add( - &self, - amount: i32, - ) -> Result { + pub(crate) fn checked_add(&self, amount: i32) -> Result { let epoch_day = self.epoch_day; - let sum = epoch_day - .checked_add(amount) - .ok_or_else(|| RangeError::EpochDayI32)?; + let sum = epoch_day.checked_add(amount).ok_or_else(|| { + err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32") + })?; let ret = IEpochDay { epoch_day: sum }; if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) { - return Err(RangeError::EpochDayDays); + return Err(err!( + "adding `{amount}` to epoch day `{epoch_day}` \ + resulted in `{sum}`, which is not in the required \ + epoch day range of `{min}..={max}`", + min = IEpochDay::MIN.epoch_day, + max = IEpochDay::MAX.epoch_day, + )); } Ok(ret) } @@ -257,11 +260,14 @@ impl IDate { year: i16, month: i8, day: i8, - ) -> Result { + ) -> Result { if day > 28 { let max_day = days_in_month(year, month); if day > max_day { - return Err(RangeError::DateInvalidDays { year, month }); + return Err(err!( + "day={day} is out of range for year={year} \ + and month={month}, must be in range 1..={max_day}", + )); } } Ok(IDate { year, month, day }) @@ -277,22 +283,37 @@ impl IDate { pub(crate) fn from_day_of_year( year: i16, day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 366) { - return Err(RangeError::DateInvalidDayOfYear { year }); + return Err(err!( + "day-of-year={day} is out of range for year={year}, \ + must be in range 1..={max_day}", + max_day = days_in_year(year), + )); } let start = IDate { year, month: 1, day: 1 }.to_epoch_day(); let end = start .checked_add(i32::from(day) - 1) - // This can only happen when `year=9999` and `day=366`. - .map_err(|_| RangeError::DayOfYear)? + .map_err(|_| { + err!( + "failed to find date for \ + year={year} and day-of-year={day}: \ + adding `{day}` to `{start}` overflows \ + Jiff's range", + start = start.epoch_day, + ) + })? .to_date(); // If we overflowed into the next year, then `day` is too big. if year != end.year { // Can only happen given day=366 and this is a leap year. debug_assert_eq!(day, 366); debug_assert!(!is_leap_year(year)); - return Err(RangeError::DateInvalidDayOfYear { year }); + return Err(err!( + "day-of-year={day} is out of range for year={year}, \ + must be in range 1..={max_day}", + max_day = days_in_year(year), + )); } Ok(end) } @@ -308,9 +329,12 @@ impl IDate { pub(crate) fn from_day_of_year_no_leap( year: i16, mut day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 365) { - return Err(RangeError::DateInvalidDayOfYearNoLeap); + return Err(err!( + "day-of-year={day} is out of range for year={year}, \ + must be in range 1..=365", + )); } if day >= 60 && is_leap_year(year) { day += 1; @@ -368,9 +392,12 @@ impl IDate { &self, nth: i8, weekday: IWeekday, - ) -> Result { + ) -> Result { if nth == 0 || !(-5 <= nth && nth <= 5) { - return Err(RangeError::NthWeekdayOfMonth); + return Err(err!( + "got nth weekday of `{nth}`, but \ + must be non-zero and in range `-5..=5`", + )); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -387,10 +414,13 @@ impl IDate { // of `Day`, we can't let this boundary condition escape. So we // check it here. if day < 1 { - return Err(RangeError::DateInvalidDays { - year: self.year, - month: self.month, - }); + return Err(err!( + "day={day} is out of range for year={year} \ + and month={month}, must be in range 1..={max_day}", + year = self.year, + month = self.month, + max_day = days_in_month(self.year, self.month), + )); } IDate::try_new(self.year, self.month, day) } @@ -398,12 +428,16 @@ impl IDate { /// Returns the day before this date. #[inline] - pub(crate) fn yesterday(self) -> Result { + pub(crate) fn yesterday(self) -> Result { if self.day == 1 { if self.month == 1 { let year = self.year - 1; if year <= -10000 { - return Err(RangeError::Yesterday); + return Err(err!( + "returning yesterday for -9999-01-01 is not \ + possible because it is less than Jiff's supported + minimum date", + )); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -416,12 +450,16 @@ impl IDate { /// Returns the day after this date. #[inline] - pub(crate) fn tomorrow(self) -> Result { + pub(crate) fn tomorrow(self) -> Result { if self.day >= 28 && self.day == days_in_month(self.year, self.month) { if self.month == 12 { let year = self.year + 1; if year >= 10000 { - return Err(RangeError::Tomorrow); + return Err(err!( + "returning tomorrow for 9999-12-31 is not \ + possible because it is greater than Jiff's supported + maximum date", + )); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -433,20 +471,34 @@ impl IDate { /// Returns the year one year before this date. #[inline] - pub(crate) fn prev_year(self) -> Result { + pub(crate) fn prev_year(self) -> Result { let year = self.year - 1; if year <= -10_000 { - return Err(RangeError::YearPrevious); + return Err(err!( + "returning previous year for {year:04}-{month:02}-{day:02} is \ + not possible because it is less than Jiff's supported \ + minimum date", + year = self.year, + month = self.month, + day = self.day, + )); } Ok(year) } /// Returns the year one year from this date. #[inline] - pub(crate) fn next_year(self) -> Result { + pub(crate) fn next_year(self) -> Result { let year = self.year + 1; if year >= 10_000 { - return Err(RangeError::YearNext); + return Err(err!( + "returning next year for {year:04}-{month:02}-{day:02} is \ + not possible because it is greater than Jiff's supported \ + maximum date", + year = self.year, + month = self.month, + day = self.day, + )); } Ok(year) } @@ -456,7 +508,7 @@ impl IDate { pub(crate) fn checked_add_days( &self, amount: i32, - ) -> Result { + ) -> Result { match amount { 0 => Ok(*self), -1 => self.yesterday(), @@ -668,84 +720,6 @@ pub(crate) enum IAmbiguousOffset { Fold { before: IOffset, after: IOffset }, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum RangeError { - DateInvalidDayOfYear { year: i16 }, - DateInvalidDayOfYearNoLeap, - DateInvalidDays { year: i16, month: i8 }, - DateTimeSeconds, - DayOfYear, - EpochDayDays, - EpochDayI32, - NthWeekdayOfMonth, - Tomorrow, - YearNext, - YearPrevious, - Yesterday, -} - -impl core::fmt::Display for RangeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::RangeError::*; - - match *self { - DateInvalidDayOfYear { year } => write!( - f, - "number of days for `{year:04}` is invalid, \ - must be in range `1..={max_day}`", - max_day = days_in_year(year), - ), - DateInvalidDayOfYearNoLeap => f.write_str( - "number of days is invalid, must be in range `1..=365`", - ), - DateInvalidDays { year, month } => write!( - f, - "number of days for `{year:04}-{month:02}` is invalid, \ - must be in range `1..={max_day}`", - max_day = days_in_month(year, month), - ), - DateTimeSeconds => { - f.write_str("adding seconds to datetime overflowed") - } - DayOfYear => f.write_str("day of year is invalid"), - EpochDayDays => write!( - f, - "adding to epoch day resulted in a value outside \ - the allowed range of `{min}..={max}`", - min = IEpochDay::MIN.epoch_day, - max = IEpochDay::MAX.epoch_day, - ), - EpochDayI32 => f.write_str( - "adding to epoch day overflowed 32-bit signed integer", - ), - NthWeekdayOfMonth => f.write_str( - "invalid nth weekday of month, \ - must be non-zero and in range `-5..=5`", - ), - Tomorrow => f.write_str( - "returning tomorrow for `9999-12-31` is not \ - possible because it is greater than Jiff's supported - maximum date", - ), - YearNext => f.write_str( - "creating a date for a year following `9999` is \ - not possible because it is greater than Jiff's supported \ - maximum date", - ), - YearPrevious => f.write_str( - "creating a date for a year preceding `-9999` is \ - not possible because it is less than Jiff's supported \ - minimum date", - ), - Yesterday => f.write_str( - "returning yesterday for `-9999-01-01` is not \ - possible because it is less than Jiff's supported - minimum date", - ), - } - } -} - /// Returns true if and only if the given year is a leap year. /// /// A leap year is a year with 366 days. Typical years have 365 days. @@ -948,20 +922,4 @@ mod tests { let d1 = IDate { year: 9999, month: 12, day: 31 }; assert_eq!(d1.tomorrow().ok(), None); } - - #[test] - fn from_day_of_year() { - assert_eq!( - IDate::from_day_of_year(9999, 365), - Ok(IDate { year: 9999, month: 12, day: 31 }), - ); - assert_eq!( - IDate::from_day_of_year(9998, 366), - Err(RangeError::DateInvalidDayOfYear { year: 9998 }), - ); - assert_eq!( - IDate::from_day_of_year(9999, 366), - Err(RangeError::DayOfYear), - ); - } } diff --git a/crates/jiff-static/src/shared/util/mod.rs b/crates/jiff-static/src/shared/util/mod.rs index 3630e73..d245aed 100644 --- a/crates/jiff-static/src/shared/util/mod.rs +++ b/crates/jiff-static/src/shared/util/mod.rs @@ -1,4 +1,7 @@ // auto-generated by: jiff-cli generate shared pub(crate) mod array_str; +pub(crate) mod error; +pub(crate) mod escape; pub(crate) mod itime; +pub(crate) mod utf8; diff --git a/crates/jiff-static/src/shared/util/utf8.rs b/crates/jiff-static/src/shared/util/utf8.rs index 1738a20..08d0c7a 100644 --- a/crates/jiff-static/src/shared/util/utf8.rs +++ b/crates/jiff-static/src/shared/util/utf8.rs @@ -1,59 +1,5 @@ // auto-generated by: jiff-cli generate shared -/// Represents an invalid UTF-8 sequence. -/// -/// This is an error returned by `decode`. It is guaranteed to -/// contain 1, 2 or 3 bytes. -pub(crate) struct Utf8Error { - bytes: [u8; 3], - len: u8, -} - -impl Utf8Error { - #[cold] - #[inline(never)] - fn new(original_bytes: &[u8], err: core::str::Utf8Error) -> Utf8Error { - let len = err.error_len().unwrap_or_else(|| original_bytes.len()); - // OK because the biggest invalid UTF-8 - // sequence possible is 3. - debug_assert!(1 <= len && len <= 3); - let mut bytes = [0; 3]; - bytes[..len].copy_from_slice(&original_bytes[..len]); - Utf8Error { - bytes, - // OK because the biggest invalid UTF-8 - // sequence possible is 3. - len: u8::try_from(len).unwrap(), - } - } - - /// Returns the slice of invalid UTF-8 bytes. - /// - /// The slice returned is guaranteed to have length equivalent - /// to `Utf8Error::len`. - pub(crate) fn as_slice(&self) -> &[u8] { - &self.bytes[..self.len()] - } - - /// Returns the length of the invalid UTF-8 sequence found. - /// - /// This is guaranteed to be 1, 2 or 3. - pub(crate) fn len(&self) -> usize { - usize::from(self.len) - } -} - -impl core::fmt::Display for Utf8Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!( - f, - "found invalid UTF-8 byte {errant_bytes:?} in format \ - string (format strings must be valid UTF-8)", - errant_bytes = crate::shared::util::escape::Bytes(self.as_slice()), - ) - } -} - /// Decodes the next UTF-8 encoded codepoint from the given byte slice. /// /// If no valid encoding of a codepoint exists at the beginning of the @@ -69,24 +15,25 @@ impl core::fmt::Display for Utf8Error { /// *WARNING*: This is not designed for performance. If you're looking for /// a fast UTF-8 decoder, this is not it. If you feel like you need one in /// this crate, then please file an issue and discuss your use case. -pub(crate) fn decode(bytes: &[u8]) -> Option> { +pub(crate) fn decode(bytes: &[u8]) -> Option> { if bytes.is_empty() { return None; } let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) { Ok(s) => s, Err(ref err) if err.valid_up_to() > 0 => { - // OK because we just verified we have at least some - // valid UTF-8. core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap() } // In this case, we want to return 1-3 bytes that make up a prefix of // a potentially valid codepoint. - Err(err) => return Some(Err(Utf8Error::new(bytes, err))), + Err(err) => { + return Some(Err( + &bytes[..err.error_len().unwrap_or_else(|| bytes.len())] + )) + } }; // OK because we guaranteed above that `string` // must be non-empty. And thus, `str::chars` must // yield at least one Unicode scalar value. Some(Ok(string.chars().next().unwrap())) } - diff --git a/crates/jiff-tzdb/Cargo.toml b/crates/jiff-tzdb/Cargo.toml index 7330144..3374467 100644 --- a/crates/jiff-tzdb/Cargo.toml +++ b/crates/jiff-tzdb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jiff-tzdb" -version = "0.1.5" #:version +version = "0.1.4" #:version authors = ["Andrew Gallant "] license = "Unlicense OR MIT" homepage = "https://github.com/BurntSushi/jiff/tree/master/crates/jiff-tzdb" diff --git a/crates/jiff-tzdb/concatenated-zoneinfo.dat b/crates/jiff-tzdb/concatenated-zoneinfo.dat index 8990c8c..8b5878d 100644 Binary files a/crates/jiff-tzdb/concatenated-zoneinfo.dat and b/crates/jiff-tzdb/concatenated-zoneinfo.dat differ diff --git a/crates/jiff-tzdb/tzname.rs b/crates/jiff-tzdb/tzname.rs index ca4788f..f6fd342 100644 --- a/crates/jiff-tzdb/tzname.rs +++ b/crates/jiff-tzdb/tzname.rs @@ -1,4 +1,4 @@ -pub(super) static VERSION: Option<&str> = Some(r"2025c"); +pub(super) static VERSION: Option<&str> = Some(r"2025b"); pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Africa/Abidjan", 3982..4112), @@ -14,15 +14,15 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Africa/Blantyre", 3851..3982), (r"Africa/Brazzaville", 10414..10594), (r"Africa/Bujumbura", 3851..3982), - (r"Africa/Cairo", 162382..163691), - (r"Africa/Casablanca", 190185..192106), + (r"Africa/Cairo", 163461..164770), + (r"Africa/Casablanca", 189897..191818), (r"Africa/Ceuta", 41827..42389), (r"Africa/Conakry", 3982..4112), (r"Africa/Dakar", 3982..4112), (r"Africa/Dar_es_Salaam", 12063..12254), (r"Africa/Djibouti", 12063..12254), (r"Africa/Douala", 10414..10594), - (r"Africa/El_Aaiun", 186481..188307), + (r"Africa/El_Aaiun", 186193..188019), (r"Africa/Freetown", 3982..4112), (r"Africa/Gaborone", 3851..3982), (r"Africa/Harare", 3851..3982), @@ -74,7 +74,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"America/Argentina/Tucuman", 77548..78274), (r"America/Argentina/Ushuaia", 71856..72564), (r"America/Aruba", 9872..10049), - (r"America/Asuncion", 151478..152563), + (r"America/Asuncion", 152557..153642), (r"America/Atikokan", 5647..5796), (r"America/Atka", 122687..123656), (r"America/Bahia", 65501..66183), @@ -94,13 +94,13 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"America/Catamarca", 71148..71856), (r"America/Cayenne", 6101..6252), (r"America/Cayman", 5647..5796), - (r"America/Chicago", 184727..186481), + (r"America/Chicago", 184439..186193), (r"America/Chihuahua", 64055..64746), (r"America/Ciudad_Juarez", 66873..67591), (r"America/Coral_Harbour", 5647..5796), (r"America/Cordoba", 70440..71148), (r"America/Costa_Rica", 16497..16729), - (r"America/Coyhaique", 167760..169122), + (r"America/Coyhaique", 167472..168834), (r"America/Creston", 16966..17206), (r"America/Cuiaba", 132233..133167), (r"America/Curacao", 9872..10049), @@ -113,21 +113,21 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"America/Edmonton", 133167..134137), (r"America/Eirunepe", 30431..30867), (r"America/El_Salvador", 11514..11690), - (r"America/Ensenada", 166393..167760), - (r"America/Fort_Nelson", 171986..173434), + (r"America/Ensenada", 149270..150349), + (r"America/Fort_Nelson", 171698..173146), (r"America/Fort_Wayne", 36060..36591), (r"America/Fortaleza", 37559..38043), (r"America/Glace_Bay", 110783..111663), - (r"America/Godthab", 192106..193071), - (r"America/Goose_Bay", 176415..177995), + (r"America/Godthab", 191818..192783), + (r"America/Goose_Bay", 176127..177707), (r"America/Grand_Turk", 108139..108992), (r"America/Grenada", 9872..10049), (r"America/Guadeloupe", 9872..10049), (r"America/Guatemala", 15573..15785), (r"America/Guayaquil", 9693..9872), (r"America/Guyana", 10594..10775), - (r"America/Halifax", 179594..181266), - (r"America/Havana", 152563..153680), + (r"America/Halifax", 179306..180978), + (r"America/Havana", 153642..154759), (r"America/Hermosillo", 17472..17730), (r"America/Indiana/Indianapolis", 36060..36591), (r"America/Indiana/Knox", 139996..141012), @@ -143,14 +143,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"America/Jamaica", 21737..22076), (r"America/Jujuy", 61970..62660), (r"America/Juneau", 120800..121766), - (r"America/Kentucky/Louisville", 157318..158560), + (r"America/Kentucky/Louisville", 158397..159639), (r"America/Kentucky/Monticello", 131261..132233), (r"America/Knox_IN", 139996..141012), (r"America/Kralendijk", 9872..10049), (r"America/La_Paz", 7964..8134), (r"America/Lima", 18282..18565), - (r"America/Los_Angeles", 159794..161088), - (r"America/Louisville", 157318..158560), + (r"America/Los_Angeles", 160873..162167), + (r"America/Louisville", 158397..159639), (r"America/Lower_Princes", 9872..10049), (r"America/Maceio", 38601..39103), (r"America/Managua", 18565..18860), @@ -165,20 +165,20 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"America/Metlakatla", 42389..42975), (r"America/Mexico_City", 98432..99205), (r"America/Miquelon", 40739..41289), - (r"America/Moncton", 174922..176415), + (r"America/Moncton", 174634..176127), (r"America/Monterrey", 74688..75397), (r"America/Montevideo", 128417..129386), - (r"America/Montreal", 181266..182983), + (r"America/Montreal", 180978..182695), (r"America/Montserrat", 9872..10049), - (r"America/Nassau", 181266..182983), - (r"America/New_York", 182983..184727), - (r"America/Nipigon", 181266..182983), + (r"America/Nassau", 180978..182695), + (r"America/New_York", 182695..184439), + (r"America/Nipigon", 180978..182695), (r"America/Nome", 123656..124631), (r"America/Noronha", 36591..37075), (r"America/North_Dakota/Beulah", 146140..147183), (r"America/North_Dakota/Center", 134137..135127), (r"America/North_Dakota/New_Salem", 135127..136117), - (r"America/Nuuk", 192106..193071), + (r"America/Nuuk", 191818..192783), (r"America/Ojinaga", 67591..68309), (r"America/Panama", 5647..5796), (r"America/Pangnirtung", 106419..107274), @@ -189,24 +189,24 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"America/Porto_Acre", 28274..28692), (r"America/Porto_Velho", 26248..26642), (r"America/Puerto_Rico", 9872..10049), - (r"America/Punta_Arenas", 156100..157318), - (r"America/Rainy_River", 161088..162382), + (r"America/Punta_Arenas", 157179..158397), + (r"America/Rainy_River", 162167..163461), (r"America/Rankin_Inlet", 104795..105602), (r"America/Recife", 37075..37559), (r"America/Regina", 54642..55280), (r"America/Resolute", 103988..104795), (r"America/Rio_Branco", 28274..28692), (r"America/Rosario", 70440..71148), - (r"America/Santa_Isabel", 166393..167760), + (r"America/Santa_Isabel", 149270..150349), (r"America/Santarem", 27453..27862), - (r"America/Santiago", 196303..197657), + (r"America/Santiago", 196015..197369), (r"America/Santo_Domingo", 19436..19753), (r"America/Sao_Paulo", 137116..138068), - (r"America/Scoresbysund", 193071..194055), + (r"America/Scoresbysund", 192783..193767), (r"America/Shiprock", 147183..148225), (r"America/Sitka", 119844..120800), (r"America/St_Barthelemy", 9872..10049), - (r"America/St_Johns", 188307..190185), + (r"America/St_Johns", 188019..189897), (r"America/St_Kitts", 9872..10049), (r"America/St_Lucia", 9872..10049), (r"America/St_Thomas", 9872..10049), @@ -214,14 +214,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"America/Swift_Current", 22418..22786), (r"America/Tegucigalpa", 13299..13493), (r"America/Thule", 30867..31322), - (r"America/Thunder_Bay", 181266..182983), - (r"America/Tijuana", 166393..167760), - (r"America/Toronto", 181266..182983), + (r"America/Thunder_Bay", 180978..182695), + (r"America/Tijuana", 149270..150349), + (r"America/Toronto", 180978..182695), (r"America/Tortola", 9872..10049), - (r"America/Vancouver", 163691..165021), + (r"America/Vancouver", 164770..166100), (r"America/Virgin", 9872..10049), (r"America/Whitehorse", 141012..142041), - (r"America/Winnipeg", 161088..162382), + (r"America/Winnipeg", 162167..163461), (r"America/Yakutat", 118898..119844), (r"America/Yellowknife", 133167..134137), (r"Antarctica/Casey", 18860..19147), @@ -261,23 +261,23 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Asia/Chungking", 25461..25854), (r"Asia/Colombo", 14362..14609), (r"Asia/Dacca", 14131..14362), - (r"Asia/Damascus", 158560..159794), + (r"Asia/Damascus", 159639..160873), (r"Asia/Dhaka", 14131..14362), (r"Asia/Dili", 8651..8821), (r"Asia/Dubai", 4511..4644), (r"Asia/Dushanbe", 23152..23518), (r"Asia/Famagusta", 127477..128417), - (r"Asia/Gaza", 197657..200607), + (r"Asia/Gaza", 197369..200319), (r"Asia/Harbin", 25461..25854), - (r"Asia/Hebron", 200607..203575), + (r"Asia/Hebron", 200319..203287), (r"Asia/Ho_Chi_Minh", 15785..16021), (r"Asia/Hong_Kong", 100821..101596), (r"Asia/Hovd", 46530..47124), (r"Asia/Irkutsk", 91581..92341), - (r"Asia/Istanbul", 153680..154880), + (r"Asia/Istanbul", 154759..155959), (r"Asia/Jakarta", 14856..15104), (r"Asia/Jayapura", 8480..8651), - (r"Asia/Jerusalem", 194055..195129), + (r"Asia/Jerusalem", 193767..194841), (r"Asia/Kabul", 6859..7018), (r"Asia/Kamchatka", 80467..81194), (r"Asia/Karachi", 17206..17472), @@ -320,7 +320,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Asia/Tashkent", 22786..23152), (r"Asia/Tbilisi", 53338..53967), (r"Asia/Tehran", 103176..103988), - (r"Asia/Tel_Aviv", 194055..195129), + (r"Asia/Tel_Aviv", 193767..194841), (r"Asia/Thimbu", 7171..7325), (r"Asia/Thimphu", 7171..7325), (r"Asia/Tokyo", 15360..15573), @@ -336,14 +336,14 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Asia/Yangon", 10227..10414), (r"Asia/Yekaterinburg", 92341..93101), (r"Asia/Yerevan", 73980..74688), - (r"Atlantic/Azores", 169122..170523), + (r"Atlantic/Azores", 168834..170235), (r"Atlantic/Bermuda", 144073..145097), (r"Atlantic/Canary", 33169..33647), (r"Atlantic/Cape_Verde", 9164..9339), (r"Atlantic/Faeroe", 29129..29570), (r"Atlantic/Faroe", 29129..29570), (r"Atlantic/Jan_Mayen", 63350..64055), - (r"Atlantic/Madeira", 165021..166393), + (r"Atlantic/Madeira", 166100..167472), (r"Atlantic/Reykjavik", 3982..4112), (r"Atlantic/South_Georgia", 3585..3717), (r"Atlantic/St_Helena", 3982..4112), @@ -375,24 +375,24 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Brazil/DeNoronha", 36591..37075), (r"Brazil/East", 137116..138068), (r"Brazil/West", 27862..28274), - (r"Canada/Atlantic", 179594..181266), - (r"Canada/Central", 161088..162382), - (r"Canada/Eastern", 181266..182983), + (r"Canada/Atlantic", 179306..180978), + (r"Canada/Central", 162167..163461), + (r"Canada/Eastern", 180978..182695), (r"Canada/Mountain", 133167..134137), - (r"Canada/Newfoundland", 188307..190185), - (r"Canada/Pacific", 163691..165021), + (r"Canada/Newfoundland", 188019..189897), + (r"Canada/Pacific", 164770..166100), (r"Canada/Saskatchewan", 54642..55280), (r"Canada/Yukon", 141012..142041), - (r"CET", 150375..151478), - (r"Chile/Continental", 196303..197657), - (r"Chile/EasterIsland", 195129..196303), - (r"CST6CDT", 184727..186481), - (r"Cuba", 152563..153680), + (r"CET", 151454..152557), + (r"Chile/Continental", 196015..197369), + (r"Chile/EasterIsland", 194841..196015), + (r"CST6CDT", 184439..186193), + (r"Cuba", 153642..154759), (r"EET", 57918..58600), - (r"Egypt", 162382..163691), - (r"Eire", 173434..174922), + (r"Egypt", 163461..164770), + (r"Eire", 173146..174634), (r"EST", 5647..5796), - (r"EST5EDT", 182983..184727), + (r"EST5EDT", 182695..184439), (r"Etc/GMT", 113..224), (r"Etc/GMT+0", 113..224), (r"Etc/GMT+1", 3182..3295), @@ -428,44 +428,44 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Etc/Universal", 224..335), (r"Etc/UTC", 224..335), (r"Etc/Zulu", 224..335), - (r"Europe/Amsterdam", 150375..151478), + (r"Europe/Amsterdam", 151454..152557), (r"Europe/Andorra", 23884..24273), (r"Europe/Astrakhan", 81920..82646), (r"Europe/Athens", 57918..58600), - (r"Europe/Belfast", 177995..179594), + (r"Europe/Belfast", 177707..179306), (r"Europe/Belgrade", 34563..35041), (r"Europe/Berlin", 63350..64055), (r"Europe/Bratislava", 69009..69732), - (r"Europe/Brussels", 150375..151478), + (r"Europe/Brussels", 151454..152557), (r"Europe/Bucharest", 57257..57918), (r"Europe/Budapest", 97666..98432), (r"Europe/Busingen", 35041..35538), (r"Europe/Chisinau", 64746..65501), (r"Europe/Copenhagen", 63350..64055), - (r"Europe/Dublin", 173434..174922), - (r"Europe/Gibraltar", 154880..156100), - (r"Europe/Guernsey", 177995..179594), + (r"Europe/Dublin", 173146..174634), + (r"Europe/Gibraltar", 155959..157179), + (r"Europe/Guernsey", 177707..179306), (r"Europe/Helsinki", 32688..33169), - (r"Europe/Isle_of_Man", 177995..179594), - (r"Europe/Istanbul", 153680..154880), - (r"Europe/Jersey", 177995..179594), + (r"Europe/Isle_of_Man", 177707..179306), + (r"Europe/Istanbul", 154759..155959), + (r"Europe/Jersey", 177707..179306), (r"Europe/Kaliningrad", 113459..114363), (r"Europe/Kiev", 38043..38601), (r"Europe/Kirov", 78274..79009), (r"Europe/Kyiv", 38043..38601), - (r"Europe/Lisbon", 170523..171986), + (r"Europe/Lisbon", 170235..171698), (r"Europe/Ljubljana", 34563..35041), - (r"Europe/London", 177995..179594), - (r"Europe/Luxembourg", 150375..151478), + (r"Europe/London", 177707..179306), + (r"Europe/Luxembourg", 151454..152557), (r"Europe/Madrid", 111663..112560), (r"Europe/Malta", 126549..127477), (r"Europe/Mariehamn", 32688..33169), (r"Europe/Minsk", 99205..100013), - (r"Europe/Monaco", 149270..150375), + (r"Europe/Monaco", 150349..151454), (r"Europe/Moscow", 109875..110783), (r"Europe/Nicosia", 44735..45332), (r"Europe/Oslo", 63350..64055), - (r"Europe/Paris", 149270..150375), + (r"Europe/Paris", 150349..151454), (r"Europe/Podgorica", 34563..35041), (r"Europe/Prague", 69009..69732), (r"Europe/Riga", 55280..55974), @@ -493,8 +493,8 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Europe/Zaporozhye", 38043..38601), (r"Europe/Zurich", 35041..35538), (r"Factory", 0..113), - (r"GB", 177995..179594), - (r"GB-Eire", 177995..179594), + (r"GB", 177707..179306), + (r"GB-Eire", 177707..179306), (r"GMT", 113..224), (r"GMT+0", 113..224), (r"GMT-0", 113..224), @@ -515,13 +515,13 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Indian/Mayotte", 12063..12254), (r"Indian/Reunion", 4511..4644), (r"Iran", 103176..103988), - (r"Israel", 194055..195129), + (r"Israel", 193767..194841), (r"Jamaica", 21737..22076), (r"Japan", 15360..15573), (r"Kwajalein", 12882..13101), (r"Libya", 29570..30001), - (r"MET", 150375..151478), - (r"Mexico/BajaNorte", 166393..167760), + (r"MET", 151454..152557), + (r"Mexico/BajaNorte", 149270..150349), (r"Mexico/BajaSur", 66183..66873), (r"Mexico/General", 98432..99205), (r"MST", 16966..17206), @@ -534,7 +534,7 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Pacific/Bougainville", 12254..12455), (r"Pacific/Chatham", 100013..100821), (r"Pacific/Chuuk", 6705..6859), - (r"Pacific/Easter", 195129..196303), + (r"Pacific/Easter", 194841..196015), (r"Pacific/Efate", 22076..22418), (r"Pacific/Enderbury", 8134..8306), (r"Pacific/Fakaofo", 5796..5949), @@ -574,29 +574,29 @@ pub(super) static TZNAME_TO_OFFSET: &[(&str, core::ops::Range)] = &[ (r"Pacific/Wallis", 3717..3851), (r"Pacific/Yap", 6705..6859), (r"Poland", 116167..117090), - (r"Portugal", 170523..171986), + (r"Portugal", 170235..171698), (r"PRC", 25461..25854), - (r"PST8PDT", 159794..161088), + (r"PST8PDT", 160873..162167), (r"ROC", 39103..39614), (r"ROK", 27038..27453), (r"Singapore", 15104..15360), - (r"Turkey", 153680..154880), + (r"Turkey", 154759..155959), (r"UCT", 224..335), (r"Universal", 224..335), (r"US/Alaska", 124631..125608), (r"US/Aleutian", 122687..123656), (r"US/Arizona", 16966..17206), - (r"US/Central", 184727..186481), + (r"US/Central", 184439..186193), (r"US/East-Indiana", 36060..36591), - (r"US/Eastern", 182983..184727), + (r"US/Eastern", 182695..184439), (r"US/Hawaii", 13910..14131), (r"US/Indiana-Starke", 139996..141012), (r"US/Michigan", 112560..113459), (r"US/Mountain", 147183..148225), - (r"US/Pacific", 159794..161088), + (r"US/Pacific", 160873..162167), (r"US/Samoa", 5197..5343), (r"UTC", 224..335), (r"W-SU", 109875..110783), - (r"WET", 170523..171986), + (r"WET", 170235..171698), (r"Zulu", 224..335), ]; diff --git a/fuzz/.gitignore b/fuzz/.gitignore deleted file mode 100644 index 1a45eee..0000000 --- a/fuzz/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -corpus -artifacts -coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock deleted file mode 100644 index 5e5046c..0000000 --- a/fuzz/Cargo.lock +++ /dev/null @@ -1,244 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "cc" -version = "1.2.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "jiff" -version = "0.2.16" -dependencies = [ - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-sys", -] - -[[package]] -name = "jiff-fuzz" -version = "0.0.0" -dependencies = [ - "jiff", - "libfuzzer-sys", -] - -[[package]] -name = "jiff-static" -version = "0.2.16" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.5" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom", - "libc", -] - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml deleted file mode 100644 index 7c2a71f..0000000 --- a/fuzz/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "jiff-fuzz" -version = "0.0.0" -publish = false -edition = "2021" - -[features] -relaxed = [] - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } - -[dependencies.jiff] -path = ".." - -[workspace] -members = ["."] - -[[bin]] -name = "rfc2822_parse" -path = "fuzz_targets/rfc2822_parse.rs" -test = false -doc = false -bench = false - -[[bin]] -name = "strtime_parse" -path = "fuzz_targets/strtime_parse.rs" -test = false -doc = false -bench = false - -[[bin]] -name = "temporal_parse" -path = "fuzz_targets/temporal_parse.rs" -test = false -doc = false -bench = false - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } \ No newline at end of file diff --git a/fuzz/fuzz_targets/rfc2822_parse.rs b/fuzz/fuzz_targets/rfc2822_parse.rs deleted file mode 100644 index d26a3b9..0000000 --- a/fuzz/fuzz_targets/rfc2822_parse.rs +++ /dev/null @@ -1,50 +0,0 @@ -#![cfg_attr(fuzzing, no_main)] - -use std::borrow::Cow; - -use libfuzzer_sys::fuzz_target; - -use jiff::fmt::rfc2822; - -mod shim; - -fn do_fuzz(data: &[u8]) { - const RFC2822_PARSER: rfc2822::DateTimeParser = - rfc2822::DateTimeParser::new(); - const RFC2822_PRINTER: rfc2822::DateTimePrinter = - rfc2822::DateTimePrinter::new(); - - let Ok(first) = RFC2822_PARSER.parse_zoned(data) else { return }; - let mut unparsed = Vec::with_capacity(data.len()); - RFC2822_PRINTER - .print_zoned(&first, &mut unparsed) - .expect("We parsed it, so we should be able to print it"); - - match RFC2822_PARSER.parse_zoned(&unparsed) { - Ok(second) => { - assert_eq!( - first, second, - "expected the initially parsed value \ - to be equal to the value after printing and re-parsing", - ); - } - Err(e) if cfg!(not(feature = "relaxed")) => { - let unparsed_str = String::from_utf8_lossy(&unparsed); - panic!( - "should be able to parse a printed value; \ - failed with `{e}` at: `{unparsed_str}`{}, \ - corresponding to {first:?}", - if matches!(unparsed_str, Cow::Owned(_)) { - Cow::from(format!(" (lossy; actual bytes: {unparsed:?})")) - } else { - Cow::from("") - } - ); - } - Err(_) => {} - } -} - -fuzz_target!(|data: &[u8]| do_fuzz(data)); - -maybe_define_main!(); diff --git a/fuzz/fuzz_targets/shim.rs b/fuzz/fuzz_targets/shim.rs deleted file mode 100644 index 5bb129d..0000000 --- a/fuzz/fuzz_targets/shim.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::{ - error::Error, - ffi::c_int, - {env, fs, ptr}, -}; - -extern "C" { - // Initializer provided by libfuzzer-sys for creating an - // appropriate panic hook. - fn LLVMFuzzerInitialize( - argc: *const isize, - argv: *const *const *const u8, - ) -> c_int; - - // This is a magic function defined by libfuzzer-sys; use for replay. - #[allow(improper_ctypes)] - fn rust_fuzzer_test_input(input: &[u8]) -> i32; -} - -#[allow(unused)] -pub fn main() -> Result<(), Box> { - let mut count = 0usize; - unsafe { - let _ = LLVMFuzzerInitialize(ptr::null(), ptr::null()); - } - for testcase in env::args_os().skip(1) { - let content = fs::read(testcase)?; - unsafe { - let _ = rust_fuzzer_test_input(&content); - } - count += 1; - } - println!("Executed {count} testcases successfully!"); - if count == 0 { - println!("Did you mean to specify a testcase?"); - } - Ok(()) -} - -#[macro_export] -macro_rules! maybe_define_main { - () => { - #[cfg(not(fuzzing))] - fn main() { - let _ = $crate::shim::main(); - } - }; -} diff --git a/fuzz/fuzz_targets/strtime_parse.rs b/fuzz/fuzz_targets/strtime_parse.rs deleted file mode 100644 index 887f6a0..0000000 --- a/fuzz/fuzz_targets/strtime_parse.rs +++ /dev/null @@ -1,103 +0,0 @@ -#![cfg_attr(fuzzing, no_main)] - -use std::borrow::Cow; - -use libfuzzer_sys::{ - arbitrary, - arbitrary::{Arbitrary, Unstructured}, - fuzz_target, -}; - -use jiff::fmt::strtime::parse; - -mod shim; - -#[derive(Debug)] -struct Input<'a> { - format: &'a str, - input: &'a str, -} - -impl<'a> Arbitrary<'a> for Input<'a> { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let fmt_len: u8 = u.arbitrary()?; - let in_len: u16 = u.arbitrary()?; - let format = u.bytes(fmt_len as usize)?; - let format = core::str::from_utf8(format) - .map_err(|_| arbitrary::Error::IncorrectFormat)?; - let input = u.bytes(in_len as usize)?; - let input = core::str::from_utf8(input) - .map_err(|_| arbitrary::Error::IncorrectFormat)?; - Ok(Input { format, input }) - } - - fn arbitrary_take_rest( - mut u: Unstructured<'a>, - ) -> arbitrary::Result { - let len: u8 = u.arbitrary()?; - // ignored in take rest, but keep it consistent - let _in_len: u16 = u.arbitrary()?; - let format = u.bytes(len as usize)?; - let format = core::str::from_utf8(format) - .map_err(|_| arbitrary::Error::IncorrectFormat)?; - let input = u.take_rest(); - let input = core::str::from_utf8(input) - .map_err(|_| arbitrary::Error::IncorrectFormat)?; - Ok(Input { format, input }) - } -} - -fn do_fuzz(src: Input) { - if let Ok(first) = parse(src.format, src.input) { - let mut unparsed = Vec::with_capacity(src.input.len()); - if first.format(src.format, &mut unparsed).is_err() { - // There are a number of reasons why this may fail. the formatter - // is simply not as strong as the parser, so we accept failure - // here. - return; - } - - match parse(src.format, &unparsed) { - Ok(second) => { - // There's not a direct equality here. To get around this, we - // compare unparsed with doubly-unparsed. - - let mut unparsed_again = Vec::with_capacity(unparsed.len()); - second.format(src.format, &mut unparsed_again).expect( - "We parsed it (twice!), so we should be able to print it", - ); - - assert_eq!( - unparsed, - unparsed_again, - "expected the initially parsed value \ - to be equal to the value after \ - printing and re-parsing; \ - found `{}', expected `{}'", - String::from_utf8_lossy(&unparsed_again), - String::from_utf8_lossy(&unparsed), - ); - } - Err(e) if cfg!(not(feature = "relaxed")) => { - let unparsed_str = String::from_utf8_lossy(&unparsed); - panic!( - "should be able to parse a printed value; \ - failed with `{e}` at: `{unparsed_str}`{}, \ - corresponding to {first:?}", - if matches!(unparsed_str, Cow::Owned(_)) { - Cow::from(format!( - " (lossy; actual bytes: {unparsed:?})" - )) - } else { - Cow::from("") - } - ); - } - Err(_) => {} - } - } -} - -fuzz_target!(|data: Input<'_>| do_fuzz(data)); - -maybe_define_main!(); diff --git a/fuzz/fuzz_targets/temporal_parse.rs b/fuzz/fuzz_targets/temporal_parse.rs deleted file mode 100644 index e339a13..0000000 --- a/fuzz/fuzz_targets/temporal_parse.rs +++ /dev/null @@ -1,51 +0,0 @@ -#![cfg_attr(fuzzing, no_main)] - -use std::borrow::Cow; - -use libfuzzer_sys::fuzz_target; - -use jiff::fmt::temporal; - -mod shim; - -fn do_fuzz(data: &[u8]) { - const TEMPORAL_PARSER: temporal::SpanParser = temporal::SpanParser::new(); - const TEMPORAL_PRINTER: temporal::SpanPrinter = - temporal::SpanPrinter::new(); - - let Ok(first) = TEMPORAL_PARSER.parse_span(data) else { return }; - // get a good start at least - let mut unparsed = Vec::with_capacity(data.len()); - TEMPORAL_PRINTER - .print_span(&first, &mut unparsed) - .expect("we parsed it, so we should be able to print it"); - - match TEMPORAL_PARSER.parse_span(&unparsed) { - Ok(second) => { - assert_eq!( - first, - second.fieldwise(), - "expected the initially parsed value \ - to be equal to the value after printing and re-parsing", - ); - } - Err(e) if cfg!(not(feature = "relaxed")) => { - let unparsed_str = String::from_utf8_lossy(&unparsed); - panic!( - "should be able to parse a printed value; \ - failed with `{e}` at: `{unparsed_str}`{}, \ - corresponding to {first:?}", - if matches!(unparsed_str, Cow::Owned(_)) { - Cow::from(format!(" (lossy; actual bytes: {unparsed:?})")) - } else { - Cow::from("") - } - ); - } - Err(_) => {} - } -} - -fuzz_target!(|data: &[u8]| do_fuzz(data)); - -maybe_define_main!(); diff --git a/src/civil/date.rs b/src/civil/date.rs index e56f66f..c4e113b 100644 --- a/src/civil/date.rs +++ b/src/civil/date.rs @@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration; use crate::{ civil::{DateTime, Era, ISOWeekDate, Time, Weekday}, duration::{Duration, SDuration}, - error::{civil::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, fmt::{ self, temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, @@ -903,9 +903,7 @@ impl Date { let weekday = weekday.to_iweekday(); let idate = self.to_idate_const(); Ok(Date::from_idate_const( - idate - .nth_weekday_of_month(nth, weekday) - .map_err(Error::itime_range)?, + idate.nth_weekday_of_month(nth, weekday).map_err(Error::shared)?, )) } @@ -1059,7 +1057,7 @@ impl Date { let nth = t::SpanWeeks::try_new("nth weekday", nth)?; if nth == C(0) { - Err(Error::slim_range("nth weekday")) + Err(err!("nth weekday cannot be `0`")) } else if nth > C(0) { let nth = nth.max(C(1)); let weekday_diff = weekday.since_ranged(self.weekday().next()); @@ -1517,8 +1515,14 @@ impl Date { -1 => self.yesterday(), 1 => self.tomorrow(), days => { - let days = UnixEpochDay::try_new("days", days) - .context(E::OverflowDaysDuration)?; + let days = UnixEpochDay::try_new("days", days).with_context( + || { + err!( + "{days} computed from duration {duration:?} \ + overflows Jiff's datetime limits", + ) + }, + )?; let days = self.to_unix_epoch_day().try_checked_add("days", days)?; Ok(Date::from_unix_epoch_day(days)) @@ -2937,9 +2941,11 @@ impl DateDifference { // // NOTE: I take the above back. It's actually possible for the // months component to overflow when largest=month. - return Err(Error::from(E::RoundMustUseDaysOrBigger { - unit: largest, - })); + return Err(err!( + "rounding the span between two dates must use days \ + or bigger for its units, but found {units}", + units = largest.plural(), + )); } if largest <= Unit::Week { let mut weeks = t::SpanWeeks::rfrom(C(0)); @@ -3191,13 +3197,13 @@ impl DateWith { Some(DateWithDay::OfYear(day)) => { let year = year.get_unchecked(); let idate = IDate::from_day_of_year(year, day) - .map_err(Error::itime_range)?; + .map_err(Error::shared)?; return Ok(Date::from_idate_const(idate)); } Some(DateWithDay::OfYearNoLeap(day)) => { let year = year.get_unchecked(); let idate = IDate::from_day_of_year_no_leap(year, day) - .map_err(Error::itime_range)?; + .map_err(Error::shared)?; return Ok(Date::from_idate_const(idate)); } }; diff --git a/src/civil/datetime.rs b/src/civil/datetime.rs index f44d1ec..1c6d6e5 100644 --- a/src/civil/datetime.rs +++ b/src/civil/datetime.rs @@ -5,7 +5,7 @@ use crate::{ datetime, Date, DateWith, Era, ISOWeekDate, Time, TimeWith, Weekday, }, duration::{Duration, SDuration}, - error::{civil::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -1695,18 +1695,25 @@ impl DateTime { { (true, true) => Ok(self), (false, true) => { - let new_date = old_date - .checked_add(span) - .context(E::FailedAddSpanDate)?; + let new_date = + old_date.checked_add(span).with_context(|| { + err!("failed to add {span} to {old_date}") + })?; Ok(DateTime::from_parts(new_date, old_time)) } (true, false) => { - let (new_time, leftovers) = old_time - .overflowing_add(span) - .context(E::FailedAddSpanTime)?; - let new_date = old_date - .checked_add(leftovers) - .context(E::FailedAddSpanOverflowing)?; + let (new_time, leftovers) = + old_time.overflowing_add(span).with_context(|| { + err!("failed to add {span} to {old_time}") + })?; + let new_date = + old_date.checked_add(leftovers).with_context(|| { + err!( + "failed to add overflowing span, {leftovers}, \ + from adding {span} to {old_time}, \ + to {old_date}", + ) + })?; Ok(DateTime::from_parts(new_date, new_time)) } (false, false) => self.checked_add_span_general(&span), @@ -1720,14 +1727,20 @@ impl DateTime { let span_date = span.without_lower(Unit::Day); let span_time = span.only_lower(Unit::Day); - let (new_time, leftovers) = old_time - .overflowing_add(span_time) - .context(E::FailedAddSpanTime)?; - let new_date = - old_date.checked_add(span_date).context(E::FailedAddSpanDate)?; - let new_date = new_date - .checked_add(leftovers) - .context(E::FailedAddSpanOverflowing)?; + let (new_time, leftovers) = + old_time.overflowing_add(span_time).with_context(|| { + err!("failed to add {span_time} to {old_time}") + })?; + let new_date = old_date.checked_add(span_date).with_context(|| { + err!("failed to add {span_date} to {old_date}") + })?; + let new_date = new_date.checked_add(leftovers).with_context(|| { + err!( + "failed to add overflowing span, {leftovers}, \ + from adding {span_time} to {old_time}, \ + to {new_date}", + ) + })?; Ok(DateTime::from_parts(new_date, new_time)) } @@ -1738,9 +1751,13 @@ impl DateTime { ) -> Result { let (date, time) = (self.date(), self.time()); let (new_time, leftovers) = time.overflowing_add_duration(duration)?; - let new_date = date - .checked_add(leftovers) - .context(E::FailedAddDurationOverflowing)?; + let new_date = date.checked_add(leftovers).with_context(|| { + err!( + "failed to add overflowing signed duration, {leftovers:?}, \ + from adding {duration:?} to {time}, + to {date}", + ) + })?; Ok(DateTime::from_parts(new_date, new_time)) } @@ -3535,10 +3552,9 @@ impl DateTimeRound { // it for good reasons. match self.smallest { Unit::Year | Unit::Month | Unit::Week => { - return Err(Error::from( - crate::error::util::RoundingIncrementError::Unsupported { - unit: self.smallest, - }, + return Err(err!( + "rounding datetimes does not support {unit}", + unit = self.smallest.plural() )); } // We don't do any rounding in this case, so just bail now. @@ -3576,7 +3592,9 @@ impl DateTimeRound { // supported datetimes. let end = start .checked_add(Span::new().days_ranged(days_len)) - .context(E::FailedAddDays)?; + .with_context(|| { + err!("adding {days_len} days to {start} failed") + })?; Ok(DateTime::from_parts(end, time)) } diff --git a/src/civil/iso_week_date.rs b/src/civil/iso_week_date.rs index bea792a..7452546 100644 --- a/src/civil/iso_week_date.rs +++ b/src/civil/iso_week_date.rs @@ -1,7 +1,6 @@ use crate::{ civil::{Date, DateTime, Weekday}, - error::{civil::Error as E, Error}, - fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, + error::{err, Error}, util::{ rangeint::RInto, t::{self, ISOWeek, ISOYear, C}, @@ -36,51 +35,6 @@ use crate::{ /// specifically want a week oriented calendar, it's likely that you'll never /// need to care about this type. /// -/// # Parsing and printing -/// -/// The `ISOWeekDate` type provides convenient trait implementations of -/// [`std::str::FromStr`] and [`std::fmt::Display`]. These use the format -/// specified by ISO 8601 for week dates: -/// -/// ``` -/// use jiff::civil::ISOWeekDate; -/// -/// let week_date: ISOWeekDate = "2024-W24-7".parse()?; -/// assert_eq!(week_date.to_string(), "2024-W24-7"); -/// assert_eq!(week_date.date().to_string(), "2024-06-16"); -/// -/// # Ok::<(), Box>(()) -/// ``` -/// -/// ISO 8601 allows the `-` separator to be absent: -/// -/// ``` -/// use jiff::civil::ISOWeekDate; -/// -/// let week_date: ISOWeekDate = "2024W241".parse()?; -/// assert_eq!(week_date.to_string(), "2024-W24-1"); -/// assert_eq!(week_date.date().to_string(), "2024-06-10"); -/// -/// // But you cannot mix and match. Either `-` separates -/// // both the year and week, or neither. -/// assert!("2024W24-1".parse::().is_err()); -/// assert!("2024-W241".parse::().is_err()); -/// -/// # Ok::<(), Box>(()) -/// ``` -/// -/// And the `W` may also be lowercase: -/// -/// ``` -/// use jiff::civil::ISOWeekDate; -/// -/// let week_date: ISOWeekDate = "2024-w24-2".parse()?; -/// assert_eq!(week_date.to_string(), "2024-W24-2"); -/// assert_eq!(week_date.date().to_string(), "2024-06-11"); -/// -/// # Ok::<(), Box>(()) -/// ``` -/// /// # Default value /// /// For convenience, this type implements the `Default` trait. Its default @@ -711,7 +665,9 @@ impl ISOWeekDate { debug_assert_eq!(t::Year::MIN, ISOYear::MIN); debug_assert_eq!(t::Year::MAX, ISOYear::MAX); if week == C(53) && !is_long_year(year) { - return Err(Error::from(E::InvalidISOWeekNumber)); + return Err(err!( + "ISO week number `{week}` is invalid for year `{year}`" + )); } // And also, the maximum Date constrains what we can utter with // ISOWeekDate so that we can preserve infallible conversions between @@ -791,24 +747,6 @@ impl core::fmt::Debug for ISOWeekDate { } } -impl core::fmt::Display for ISOWeekDate { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use crate::fmt::StdFmtWrite; - - DEFAULT_DATETIME_PRINTER - .print_iso_week_date(self, StdFmtWrite(f)) - .map_err(|_| core::fmt::Error) - } -} - -impl core::str::FromStr for ISOWeekDate { - type Err = Error; - - fn from_str(string: &str) -> Result { - DEFAULT_DATETIME_PARSER.parse_iso_week_date(string) - } -} - impl Eq for ISOWeekDate {} impl PartialEq for ISOWeekDate { @@ -870,60 +808,6 @@ impl<'a> From<&'a Zoned> for ISOWeekDate { } } -#[cfg(feature = "serde")] -impl serde_core::Serialize for ISOWeekDate { - #[inline] - fn serialize( - &self, - serializer: S, - ) -> Result { - serializer.collect_str(self) - } -} - -#[cfg(feature = "serde")] -impl<'de> serde_core::Deserialize<'de> for ISOWeekDate { - #[inline] - fn deserialize>( - deserializer: D, - ) -> Result { - use serde_core::de; - - struct ISOWeekDateVisitor; - - impl<'de> de::Visitor<'de> for ISOWeekDateVisitor { - type Value = ISOWeekDate; - - fn expecting( - &self, - f: &mut core::fmt::Formatter, - ) -> core::fmt::Result { - f.write_str("an ISO 8601 week date string") - } - - #[inline] - fn visit_bytes( - self, - value: &[u8], - ) -> Result { - DEFAULT_DATETIME_PARSER - .parse_iso_week_date(value) - .map_err(de::Error::custom) - } - - #[inline] - fn visit_str( - self, - value: &str, - ) -> Result { - self.visit_bytes(value.as_bytes()) - } - } - - deserializer.deserialize_str(ISOWeekDateVisitor) - } -} - #[cfg(test)] impl quickcheck::Arbitrary for ISOWeekDate { fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate { diff --git a/src/civil/time.rs b/src/civil/time.rs index 127a7d1..4dd651b 100644 --- a/src/civil/time.rs +++ b/src/civil/time.rs @@ -3,7 +3,7 @@ use core::time::Duration as UnsignedDuration; use crate::{ civil::{Date, DateTime}, duration::{Duration, SDuration}, - error::{civil::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -950,6 +950,7 @@ impl Time { self, duration: SignedDuration, ) -> Result { + let original = duration; let start = t::NoUnits128::rfrom(self.to_nanosecond()); let duration = t::NoUnits128::new_unchecked(duration.as_nanos()); // This can never fail because the maximum duration fits into a @@ -957,7 +958,15 @@ impl Time { // integer can never overflow a 128-bit integer. let end = start.try_checked_add("nanoseconds", duration).unwrap(); let end = CivilDayNanosecond::try_rfrom("nanoseconds", end) - .context(E::OverflowTimeNanoseconds)?; + .with_context(|| { + err!( + "adding signed duration {duration:?}, equal to + {nanos} nanoseconds, to {time} overflowed", + duration = original, + nanos = original.as_nanos(), + time = self, + ) + })?; Ok(Time::from_nanosecond(end)) } @@ -2594,9 +2603,11 @@ impl TimeDifference { } let largest = self.round.get_largest().unwrap_or(Unit::Hour); if largest > Unit::Hour { - return Err(Error::from(E::RoundMustUseHoursOrSmaller { - unit: largest, - })); + return Err(err!( + "rounding the span between two times must use hours \ + or smaller for its units, but found {units}", + units = largest.plural(), + )); } let start = t1.to_nanosecond(); let end = t2.to_nanosecond(); @@ -3001,13 +3012,22 @@ impl TimeWith { None => self.original.subsec_nanosecond_ranged(), Some(subsec_nanosecond) => { if self.millisecond.is_some() { - return Err(Error::from(E::IllegalTimeWithMillisecond)); + return Err(err!( + "cannot set both TimeWith::millisecond \ + and TimeWith::subsec_nanosecond", + )); } if self.microsecond.is_some() { - return Err(Error::from(E::IllegalTimeWithMicrosecond)); + return Err(err!( + "cannot set both TimeWith::microsecond \ + and TimeWith::subsec_nanosecond", + )); } if self.nanosecond.is_some() { - return Err(Error::from(E::IllegalTimeWithNanosecond)); + return Err(err!( + "cannot set both TimeWith::nanosecond \ + and TimeWith::subsec_nanosecond", + )); } SubsecNanosecond::try_new( "subsec_nanosecond", diff --git a/src/duration.rs b/src/duration.rs index 6d42792..27ceae3 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -1,7 +1,7 @@ use core::time::Duration as UnsignedDuration; use crate::{ - error::{duration::Error as E, ErrorContext}, + error::{err, ErrorContext}, Error, SignedDuration, Span, }; @@ -24,8 +24,12 @@ impl Duration { Duration::Span(span) => Ok(SDuration::Span(span)), Duration::Signed(sdur) => Ok(SDuration::Absolute(sdur)), Duration::Unsigned(udur) => { - let sdur = SignedDuration::try_from(udur) - .context(E::RangeUnsignedDuration)?; + let sdur = + SignedDuration::try_from(udur).with_context(|| { + err!( + "unsigned duration {udur:?} exceeds Jiff's limits" + ) + })?; Ok(SDuration::Absolute(sdur)) } } @@ -87,8 +91,9 @@ impl Duration { // Otherwise, this is the only failure point in this entire // routine. And specifically, we fail here in precisely // the cases where `udur.as_secs() > |i64::MIN|`. - -SignedDuration::try_from(udur) - .context(E::FailedNegateUnsignedDuration)? + -SignedDuration::try_from(udur).with_context(|| { + err!("failed to negate unsigned duration {udur:?}") + })? }; Ok(Duration::Signed(sdur)) } diff --git a/src/error/mod.rs b/src/error.rs similarity index 54% rename from src/error/mod.rs rename to src/error.rs index c320a6d..0675547 100644 --- a/src/error/mod.rs +++ b/src/error.rs @@ -1,14 +1,16 @@ -use crate::util::sync::Arc; +use crate::{shared::util::error::Error as SharedError, util::sync::Arc}; -pub(crate) mod civil; -pub(crate) mod duration; -pub(crate) mod fmt; -pub(crate) mod signed_duration; -pub(crate) mod span; -pub(crate) mod timestamp; -pub(crate) mod tz; -pub(crate) mod util; -pub(crate) mod zoned; +/// Creates a new ad hoc error with no causal chain. +/// +/// This accepts the same arguments as the `format!` macro. The error it +/// creates is just a wrapper around the string created by `format!`. +macro_rules! err { + ($($tt:tt)*) => {{ + crate::error::Error::adhoc_from_args(format_args!($($tt)*)) + }} +} + +pub(crate) use err; /// An error that can occur in this crate. /// @@ -27,11 +29,8 @@ pub(crate) mod zoned; /// /// Other than implementing the [`std::error::Error`] trait when the /// `std` feature is enabled, the [`core::fmt::Debug`] trait and the -/// [`core::fmt::Display`] trait, this error type currently provides -/// very limited introspection capabilities. Simple predicates like -/// `Error::is_range` are provided, but the predicates are not -/// exhaustive. That is, there exist some errors that do not return -/// `true` for any of the `Error::is_*` predicates. +/// [`core::fmt::Display`] trait, this error type currently provides no +/// introspection capabilities. /// /// # Design /// @@ -66,6 +65,50 @@ struct ErrorInner { cause: Option, } +/// The underlying kind of a [`Error`]. +#[derive(Debug)] +#[cfg_attr(not(feature = "alloc"), derive(Clone))] +enum ErrorKind { + /// An ad hoc error that is constructed from anything that implements + /// the `core::fmt::Display` trait. + /// + /// In theory we try to avoid these, but they tend to be awfully + /// convenient. In practice, we use them a lot, and only use a structured + /// representation when a lot of different error cases fit neatly into a + /// structure (like range errors). + Adhoc(AdhocError), + /// An error that occurs when a number is not within its allowed range. + /// + /// This can occur directly as a result of a number provided by the caller + /// of a public API, or as a result of an operation on a number that + /// results in it being out of range. + Range(RangeError), + /// An error that occurs within `jiff::shared`. + /// + /// It has its own error type to avoid bringing in this much bigger error + /// type. + Shared(SharedError), + /// An error associated with a file path. + /// + /// This is generally expected to always have a cause attached to it + /// explaining what went wrong. The error variant is just a path to make + /// it composable with other error types. + /// + /// The cause is typically `Adhoc` or `IO`. + /// + /// When `std` is not enabled, this variant can never be constructed. + #[allow(dead_code)] // not used in some feature configs + FilePath(FilePathError), + /// An error that occurs when interacting with the file system. + /// + /// This is effectively a wrapper around `std::io::Error` coupled with a + /// `std::path::PathBuf`. + /// + /// When `std` is not enabled, this variant can never be constructed. + #[allow(dead_code)] // not used in some feature configs + IO(IOError), +} + impl Error { /// Creates a new error value from `core::fmt::Arguments`. /// @@ -77,14 +120,6 @@ impl Error { /// circumstances, it can be convenient to manufacture a Jiff error value /// specifically. /// - /// # Core-only environments - /// - /// In core-only environments without a dynamic memory allocator, error - /// messages may be degraded in some cases. For example, if the given - /// `core::fmt::Arguments` could not be converted to a simple borrowed - /// `&str`, then this will ignore the input given and return an "unknown" - /// Jiff error. - /// /// # Example /// /// ``` @@ -97,114 +132,79 @@ impl Error { Error::from(ErrorKind::Adhoc(AdhocError::from_args(message))) } - /// Returns true when this error originated as a result of a value being - /// out of Jiff's supported range. - /// - /// # Example - /// - /// ``` - /// use jiff::civil::Date; - /// - /// assert!(Date::new(2025, 2, 29).unwrap_err().is_range()); - /// assert!("2025-02-29".parse::().unwrap_err().is_range()); - /// assert!(Date::strptime("%Y-%m-%d", "2025-02-29").unwrap_err().is_range()); - /// ``` - pub fn is_range(&self) -> bool { - use self::ErrorKind::*; - matches!(*self.root().kind(), Range(_) | SlimRange(_) | ITimeRange(_)) - } - - /// Returns true when this error originated as a result of an invalid - /// configuration of parameters to a function call. - /// - /// This particular error category is somewhat nebulous, but it's generally - /// meant to cover errors that _could_ have been statically prevented by - /// Jiff with more types in its API. Instead, a smaller API is preferred. - /// - /// # Example: invalid rounding options - /// - /// ``` - /// use jiff::{SpanRound, ToSpan, Unit}; - /// - /// let span = 44.seconds(); - /// let err = span.round( - /// SpanRound::new().smallest(Unit::Second).increment(45), - /// ).unwrap_err(); - /// // Rounding increments for seconds must divide evenly into `60`. - /// // But `45` does not. Thus, this is a "configuration" error. - /// assert!(err.is_invalid_parameter()); - /// ``` - /// - /// # Example: invalid units - /// - /// One cannot round a span between dates to units less than days: - /// - /// ``` - /// use jiff::{civil::date, Unit}; - /// - /// let date1 = date(2025, 3, 18); - /// let date2 = date(2025, 12, 21); - /// let err = date1.until((Unit::Hour, date2)).unwrap_err(); - /// assert!(err.is_invalid_parameter()); - /// ``` - /// - /// Similarly, one cannot round a span between times to units greater than - /// hours: - /// - /// ``` - /// use jiff::{civil::time, Unit}; - /// - /// let time1 = time(9, 39, 0, 0); - /// let time2 = time(17, 0, 0, 0); - /// let err = time1.until((Unit::Day, time2)).unwrap_err(); - /// assert!(err.is_invalid_parameter()); - /// ``` - pub fn is_invalid_parameter(&self) -> bool { - use self::ErrorKind::*; - use self::{ - civil::Error as CivilError, span::Error as SpanError, - tz::offset::Error as OffsetError, util::RoundingIncrementError, - }; - - matches!( - *self.root().kind(), - RoundingIncrement( - RoundingIncrementError::GreaterThanZero { .. } - | RoundingIncrementError::InvalidDivide { .. } - | RoundingIncrementError::Unsupported { .. } - ) | Span( - SpanError::NotAllowedCalendarUnits { .. } - | SpanError::NotAllowedLargestSmallerThanSmallest { .. } - | SpanError::RequiresRelativeWeekOrDay { .. } - | SpanError::RequiresRelativeYearOrMonth { .. } - | SpanError::RequiresRelativeYearOrMonthGivenDaysAre24Hours { .. } - ) | Civil( - CivilError::IllegalTimeWithMicrosecond - | CivilError::IllegalTimeWithMillisecond - | CivilError::IllegalTimeWithNanosecond - | CivilError::RoundMustUseDaysOrBigger { .. } - | CivilError::RoundMustUseHoursOrSmaller { .. } - ) | TzOffset(OffsetError::RoundInvalidUnit { .. }) - ) - } - - /// Returns true when this error originated as a result of an operation - /// failing because an appropriate Jiff crate feature was not enabled. - /// - /// # Example - /// - /// ```ignore - /// use jiff::tz::TimeZone; - /// - /// // This passes when the `tz-system` crate feature is NOT enabled. - /// assert!(TimeZone::try_system().unwrap_err().is_crate_feature()); - /// ``` - pub fn is_crate_feature(&self) -> bool { - matches!(*self.root().kind(), ErrorKind::CrateFeature(_)) + #[inline(never)] + #[cold] + fn context_impl(self, consequent: Error) -> Error { + #[cfg(feature = "alloc")] + { + let mut err = consequent; + if err.inner.is_none() { + err = err!("unknown jiff error"); + } + let inner = err.inner.as_mut().unwrap(); + assert!( + inner.cause.is_none(), + "cause of consequence must be `None`" + ); + // OK because we just created this error so the Arc + // has one reference. + Arc::get_mut(inner).unwrap().cause = Some(self); + err + } + #[cfg(not(feature = "alloc"))] + { + // We just completely drop `self`. :-( + consequent + } } } impl Error { + /// Creates a new "ad hoc" error value. + /// + /// An ad hoc error value is just an opaque string. + #[cfg(feature = "alloc")] + #[inline(never)] + #[cold] + pub(crate) fn adhoc<'a>(message: impl core::fmt::Display + 'a) -> Error { + Error::from(ErrorKind::Adhoc(AdhocError::from_display(message))) + } + + /// Like `Error::adhoc`, but accepts a `core::fmt::Arguments`. + /// + /// This is used with the `err!` macro so that we can thread a + /// `core::fmt::Arguments` down. This lets us extract a `&'static str` + /// from some messages in core-only mode and provide somewhat decent error + /// messages in some cases. + #[inline(never)] + #[cold] + pub(crate) fn adhoc_from_args<'a>( + message: core::fmt::Arguments<'a>, + ) -> Error { + Error::from(ErrorKind::Adhoc(AdhocError::from_args(message))) + } + + /// Like `Error::adhoc`, but creates an error from a `String` directly. + /// + /// This exists to explicitly monomorphize a very common case. + #[cfg(feature = "alloc")] + #[inline(never)] + #[cold] + fn adhoc_from_string(message: alloc::string::String) -> Error { + Error::adhoc(message) + } + + /// Like `Error::adhoc`, but creates an error from a `&'static str` + /// directly. + /// + /// This is useful in contexts where you know you have a `&'static str`, + /// and avoids relying on `alloc`-only routines like `Error::adhoc`. + #[inline(never)] + #[cold] + pub(crate) fn adhoc_from_static_str(message: &'static str) -> Error { + Error::from(ErrorKind::Adhoc(AdhocError::from_static_str(message))) + } + /// Creates a new error indicating that a `given` value is out of the /// specified `min..=max` range. The given `what` label is used in the /// error message as a human readable description of what exactly is out @@ -220,36 +220,9 @@ impl Error { Error::from(ErrorKind::Range(RangeError::new(what, given, min, max))) } - /// Creates a new error indicating that a `given` value is out of the - /// allowed range. - /// - /// This is similar to `Error::range`, but the error message doesn't - /// include the illegal value or the allowed range. This is useful for - /// ad hoc range errors but should generally be used sparingly. - #[inline(never)] - #[cold] - pub(crate) fn slim_range(what: &'static str) -> Error { - Error::from(ErrorKind::SlimRange(SlimRangeError::new(what))) - } - /// Creates a new error from the special "shared" error type. - pub(crate) fn itime_range( - err: crate::shared::util::itime::RangeError, - ) -> Error { - Error::from(ErrorKind::ITimeRange(err)) - } - - /// Creates a new error from the special TZif error type. - #[cfg(feature = "alloc")] - pub(crate) fn tzif(err: crate::shared::tzif::TzifError) -> Error { - Error::from(ErrorKind::Tzif(err)) - } - - /// Creates a new error from the special `PosixTimeZoneError` type. - pub(crate) fn posix_tz( - err: crate::shared::posix::PosixTimeZoneError, - ) -> Error { - Error::from(ErrorKind::PosixTz(err)) + pub(crate) fn shared(err: SharedError) -> Error { + Error::from(ErrorKind::Shared(err)) } /// A convenience constructor for building an I/O error. @@ -285,7 +258,7 @@ impl Error { /// /// The benefit of this API is that it permits creating an `Error` in a /// `const` context. But the error message quality is currently pretty - /// bad: it's just a generic "unknown Jiff error" message. + /// bad: it's just a generic "unknown jiff error" message. /// /// This could be improved to take a `&'static str`, but I believe this /// will require pointer tagging in order to avoid increasing the size of @@ -295,83 +268,6 @@ impl Error { Error { inner: None } } */ - - #[cfg_attr(feature = "perf-inline", inline(always))] - pub(crate) fn context(self, consequent: impl IntoError) -> Error { - self.context_impl(consequent.into_error()) - } - - #[inline(never)] - #[cold] - fn context_impl(self, _consequent: Error) -> Error { - #[cfg(feature = "alloc")] - { - let mut err = _consequent; - if err.inner.is_none() { - err = Error::from(ErrorKind::Unknown); - } - let inner = err.inner.as_mut().unwrap(); - assert!( - inner.cause.is_none(), - "cause of consequence must be `None`" - ); - // OK because we just created this error so the Arc - // has one reference. - Arc::get_mut(inner).unwrap().cause = Some(self); - err - } - #[cfg(not(feature = "alloc"))] - { - // We just completely drop `self`. :-( - // - // 2025-12-21: ... actually, we used to drop self, but this - // ends up dropping the root cause. And the root cause - // is how the predicates on `Error` work. So we drop the - // consequent instead. - self - } - } - - /// Returns the root error in this chain. - fn root(&self) -> &Error { - // OK because `Error::chain` is guaranteed to return a non-empty - // iterator. - self.chain().last().unwrap() - } - - /// Returns a chain of error values. - /// - /// This starts with the most recent error added to the chain. That is, - /// the highest level context. The last error in the chain is always the - /// "root" cause. That is, the error closest to the point where something - /// has gone wrong. - /// - /// The iterator returned is guaranteed to yield at least one error. - fn chain(&self) -> impl Iterator { - #[cfg(feature = "alloc")] - { - let mut err = self; - core::iter::once(err).chain(core::iter::from_fn(move || { - err = err - .inner - .as_ref() - .and_then(|inner| inner.cause.as_ref())?; - Some(err) - })) - } - #[cfg(not(feature = "alloc"))] - { - core::iter::once(self) - } - } - - /// Returns the kind of this error. - fn kind(&self) -> &ErrorKind { - self.inner - .as_ref() - .map(|inner| &inner.kind) - .unwrap_or(&ErrorKind::Unknown) - } } #[cfg(feature = "std")] @@ -379,14 +275,30 @@ impl std::error::Error for Error {} impl core::fmt::Display for Error { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - let mut it = self.chain().peekable(); - while let Some(err) = it.next() { - core::fmt::Display::fmt(err.kind(), f)?; - if it.peek().is_some() { - f.write_str(": ")?; + #[cfg(feature = "alloc")] + { + let mut err = self; + loop { + let Some(ref inner) = err.inner else { + write!(f, "unknown jiff error")?; + break; + }; + write!(f, "{}", inner.kind)?; + err = match inner.cause.as_ref() { + None => break, + Some(err) => err, + }; + write!(f, ": ")?; + } + Ok(()) + } + #[cfg(not(feature = "alloc"))] + { + match self.inner { + None => write!(f, "unknown jiff error"), + Some(ref inner) => write!(f, "{}", inner.kind), } } - Ok(()) } } @@ -416,98 +328,14 @@ impl core::fmt::Debug for Error { } } -/// The underlying kind of a [`Error`]. -#[derive(Debug)] -#[cfg_attr(not(feature = "alloc"), derive(Clone))] -enum ErrorKind { - Adhoc(AdhocError), - Civil(self::civil::Error), - CrateFeature(CrateFeatureError), - Duration(self::duration::Error), - #[allow(dead_code)] // not used in some feature configs - FilePath(FilePathError), - Fmt(self::fmt::Error), - FmtFriendly(self::fmt::friendly::Error), - FmtOffset(self::fmt::offset::Error), - FmtRfc2822(self::fmt::rfc2822::Error), - FmtRfc9557(self::fmt::rfc9557::Error), - FmtTemporal(self::fmt::temporal::Error), - FmtUtil(self::fmt::util::Error), - FmtStrtime(self::fmt::strtime::Error), - FmtStrtimeFormat(self::fmt::strtime::FormatError), - FmtStrtimeParse(self::fmt::strtime::ParseError), - #[allow(dead_code)] // not used in some feature configs - IO(IOError), - ITimeRange(crate::shared::util::itime::RangeError), - OsStrUtf8(self::util::OsStrUtf8Error), - ParseInt(self::util::ParseIntError), - ParseFraction(self::util::ParseFractionError), - PosixTz(crate::shared::posix::PosixTimeZoneError), - Range(RangeError), - RoundingIncrement(self::util::RoundingIncrementError), - SignedDuration(self::signed_duration::Error), - SlimRange(SlimRangeError), - Span(self::span::Error), - Timestamp(self::timestamp::Error), - TzAmbiguous(self::tz::ambiguous::Error), - TzDb(self::tz::db::Error), - TzConcatenated(self::tz::concatenated::Error), - TzOffset(self::tz::offset::Error), - TzPosix(self::tz::posix::Error), - TzSystem(self::tz::system::Error), - TzTimeZone(self::tz::timezone::Error), - #[allow(dead_code)] - TzZic(self::tz::zic::Error), - #[cfg(feature = "alloc")] - Tzif(crate::shared::tzif::TzifError), - Unknown, - Zoned(self::zoned::Error), -} - impl core::fmt::Display for ErrorKind { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::ErrorKind::*; - match *self { - Adhoc(ref msg) => msg.fmt(f), - Civil(ref err) => err.fmt(f), - CrateFeature(ref err) => err.fmt(f), - Duration(ref err) => err.fmt(f), - FilePath(ref err) => err.fmt(f), - Fmt(ref err) => err.fmt(f), - FmtFriendly(ref err) => err.fmt(f), - FmtOffset(ref err) => err.fmt(f), - FmtRfc2822(ref err) => err.fmt(f), - FmtRfc9557(ref err) => err.fmt(f), - FmtUtil(ref err) => err.fmt(f), - FmtStrtime(ref err) => err.fmt(f), - FmtStrtimeFormat(ref err) => err.fmt(f), - FmtStrtimeParse(ref err) => err.fmt(f), - FmtTemporal(ref err) => err.fmt(f), - IO(ref err) => err.fmt(f), - ITimeRange(ref err) => err.fmt(f), - OsStrUtf8(ref err) => err.fmt(f), - ParseInt(ref err) => err.fmt(f), - ParseFraction(ref err) => err.fmt(f), - PosixTz(ref err) => err.fmt(f), - Range(ref err) => err.fmt(f), - RoundingIncrement(ref err) => err.fmt(f), - SignedDuration(ref err) => err.fmt(f), - SlimRange(ref err) => err.fmt(f), - Span(ref err) => err.fmt(f), - Timestamp(ref err) => err.fmt(f), - TzAmbiguous(ref err) => err.fmt(f), - TzDb(ref err) => err.fmt(f), - TzConcatenated(ref err) => err.fmt(f), - TzOffset(ref err) => err.fmt(f), - TzPosix(ref err) => err.fmt(f), - TzSystem(ref err) => err.fmt(f), - TzTimeZone(ref err) => err.fmt(f), - TzZic(ref err) => err.fmt(f), - #[cfg(feature = "alloc")] - Tzif(ref err) => err.fmt(f), - Unknown => f.write_str("unknown Jiff error"), - Zoned(ref err) => err.fmt(f), + ErrorKind::Adhoc(ref msg) => msg.fmt(f), + ErrorKind::Range(ref err) => err.fmt(f), + ErrorKind::Shared(ref err) => err.fmt(f), + ErrorKind::FilePath(ref err) => err.fmt(f), + ErrorKind::IO(ref err) => err.fmt(f), } } } @@ -527,10 +355,10 @@ impl From for Error { /// A generic error message. /// -/// This used to be used to represent most errors in Jiff. But then I switched -/// to more structured error types (internally). We still keep this around to -/// support the `Error::from_args` public API, which permits users of Jiff to -/// manifest their own `Error` values from an arbitrary message. +/// This somewhat unfortunately represents most of the errors in Jiff. When I +/// first started building Jiff, I had a goal of making every error structured. +/// But this ended up being a ton of work, and I find it much easier and nicer +/// for error messages to be embedded where they occur. #[cfg_attr(not(feature = "alloc"), derive(Clone))] struct AdhocError { #[cfg(feature = "alloc")] @@ -540,13 +368,18 @@ struct AdhocError { } impl AdhocError { + #[cfg(feature = "alloc")] + fn from_display<'a>(message: impl core::fmt::Display + 'a) -> AdhocError { + use alloc::string::ToString; + + let message = message.to_string().into_boxed_str(); + AdhocError { message } + } + fn from_args<'a>(message: core::fmt::Arguments<'a>) -> AdhocError { #[cfg(feature = "alloc")] { - use alloc::string::ToString; - - let message = message.to_string().into_boxed_str(); - AdhocError { message } + AdhocError::from_display(message) } #[cfg(not(feature = "alloc"))] { @@ -554,6 +387,17 @@ impl AdhocError { "unknown Jiff error (better error messages require \ enabling the `alloc` feature for the `jiff` crate)", ); + AdhocError::from_static_str(message) + } + } + + fn from_static_str(message: &'static str) -> AdhocError { + #[cfg(feature = "alloc")] + { + AdhocError::from_display(message) + } + #[cfg(not(feature = "alloc"))] + { AdhocError { message } } } @@ -632,75 +476,6 @@ impl core::fmt::Display for RangeError { } } -/// A slim error that occurs when an input value is out of bounds. -/// -/// Unlike `RangeError`, this only includes a static description of the -/// value that is out of bounds. It doesn't include the out-of-range value -/// or the min/max values. -#[derive(Clone, Debug)] -struct SlimRangeError { - what: &'static str, -} - -impl SlimRangeError { - fn new(what: &'static str) -> SlimRangeError { - SlimRangeError { what } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SlimRangeError {} - -impl core::fmt::Display for SlimRangeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - let SlimRangeError { what } = *self; - write!(f, "parameter '{what}' is not in the required range") - } -} - -/// An error used whenever a failure is caused by a missing crate feature. -/// -/// This enum doesn't necessarily contain every Jiff crate feature. It only -/// contains the features whose absence can result in an error. -#[derive(Clone, Debug)] -pub(crate) enum CrateFeatureError { - #[cfg(not(feature = "tz-system"))] - TzSystem, - #[cfg(not(feature = "tzdb-concatenated"))] - TzdbConcatenated, - #[cfg(not(feature = "tzdb-zoneinfo"))] - TzdbZoneInfo, -} - -impl From for Error { - #[cold] - #[inline(never)] - fn from(err: CrateFeatureError) -> Error { - ErrorKind::CrateFeature(err).into() - } -} - -impl core::fmt::Display for CrateFeatureError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - #[allow(unused_imports)] - use self::CrateFeatureError::*; - - f.write_str("operation failed because Jiff crate feature `")?; - #[allow(unused_variables)] - let name: &str = match *self { - #[cfg(not(feature = "tz-system"))] - TzSystem => "tz-system", - #[cfg(not(feature = "tzdb-concatenated"))] - TzdbConcatenated => "tzdb-concatenated", - #[cfg(not(feature = "tzdb-zoneinfo"))] - TzdbZoneInfo => "tzdb-zoneinfo", - }; - #[allow(unreachable_code)] - core::fmt::Display::fmt(name, f)?; - f.write_str("` is not enabled") - } -} - /// A `std::io::Error`. /// /// This type is itself always available, even when the `std` feature is not @@ -806,6 +581,21 @@ impl IntoError for Error { } } +impl IntoError for &'static str { + #[inline(always)] + fn into_error(self) -> Error { + Error::adhoc_from_static_str(self) + } +} + +#[cfg(feature = "alloc")] +impl IntoError for alloc::string::String { + #[inline(always)] + fn into_error(self) -> Error { + Error::adhoc_from_string(self) + } +} + /// A trait for contextualizing error values. /// /// This makes it easy to contextualize either `Error` or `Result`. @@ -813,7 +603,7 @@ impl IntoError for Error { /// `map_err` everywhere one wants to add context to an error. /// /// This trick was borrowed from `anyhow`. -pub(crate) trait ErrorContext { +pub(crate) trait ErrorContext { /// Contextualize the given consequent error with this (`self`) error as /// the cause. /// @@ -822,7 +612,7 @@ pub(crate) trait ErrorContext { /// Note that if an `Error` is given for `kind`, then this panics if it has /// a cause. (Because the cause would otherwise be dropped. An error causal /// chain is just a linked list, not a tree.) - fn context(self, consequent: impl IntoError) -> Result; + fn context(self, consequent: impl IntoError) -> Self; /// Like `context`, but hides error construction within a closure. /// @@ -833,31 +623,39 @@ pub(crate) trait ErrorContext { /// /// Usually this only makes sense to use on a `Result`, otherwise /// the closure is just executed immediately anyway. - fn with_context( + fn with_context( self, - consequent: impl FnOnce() -> C, - ) -> Result; + consequent: impl FnOnce() -> E, + ) -> Self; } -impl ErrorContext for Result -where - E: IntoError, -{ +impl ErrorContext for Error { #[cfg_attr(feature = "perf-inline", inline(always))] - fn context(self, consequent: impl IntoError) -> Result { - self.map_err(|err| { - err.into_error().context_impl(consequent.into_error()) - }) + fn context(self, consequent: impl IntoError) -> Error { + self.context_impl(consequent.into_error()) } #[cfg_attr(feature = "perf-inline", inline(always))] - fn with_context( + fn with_context( self, - consequent: impl FnOnce() -> C, + consequent: impl FnOnce() -> E, + ) -> Error { + self.context_impl(consequent().into_error()) + } +} + +impl ErrorContext for Result { + #[cfg_attr(feature = "perf-inline", inline(always))] + fn context(self, consequent: impl IntoError) -> Result { + self.map_err(|err| err.context_impl(consequent.into_error())) + } + + #[cfg_attr(feature = "perf-inline", inline(always))] + fn with_context( + self, + consequent: impl FnOnce() -> E, ) -> Result { - self.map_err(|err| { - err.into_error().context_impl(consequent().into_error()) - }) + self.map_err(|err| err.context_impl(consequent().into_error())) } } @@ -892,12 +690,7 @@ mod tests { // then we could make `Error` a zero sized type. Which might // actually be the right trade-off for core-only, but I'll hold off // until we have some real world use cases. - // - // OK... after switching to structured errors, this jumped - // back up to `expected_size *= 6`. And that was with me being - // conscientious about what data we store inside of error types. - // Blech. - expected_size *= 6; + expected_size *= 3; } assert_eq!(expected_size, core::mem::size_of::()); } diff --git a/src/error/civil.rs b/src/error/civil.rs deleted file mode 100644 index 40fce64..0000000 --- a/src/error/civil.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::{error, Unit}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - FailedAddDays, - FailedAddDurationOverflowing, - FailedAddSpanDate, - FailedAddSpanOverflowing, - FailedAddSpanTime, - IllegalTimeWithMicrosecond, - IllegalTimeWithMillisecond, - IllegalTimeWithNanosecond, - InvalidISOWeekNumber, - OverflowDaysDuration, - OverflowTimeNanoseconds, - RoundMustUseDaysOrBigger { unit: Unit }, - RoundMustUseHoursOrSmaller { unit: Unit }, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::Civil(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - FailedAddDays => f.write_str("failed to add days to date"), - FailedAddDurationOverflowing => { - f.write_str("failed to add overflowing duration") - } - FailedAddSpanDate => f.write_str("failed to add span to date"), - FailedAddSpanOverflowing => { - f.write_str("failed to add overflowing span") - } - FailedAddSpanTime => f.write_str("failed to add span to time"), - IllegalTimeWithMicrosecond => f.write_str( - "cannot set both `TimeWith::microsecond` \ - and `TimeWith::subsec_nanosecond`", - ), - IllegalTimeWithMillisecond => f.write_str( - "cannot set both `TimeWith::millisecond` \ - and `TimeWith::subsec_nanosecond`", - ), - IllegalTimeWithNanosecond => f.write_str( - "cannot set both `TimeWith::nanosecond` \ - and `TimeWith::subsec_nanosecond`", - ), - InvalidISOWeekNumber => { - f.write_str("ISO week number is invalid for given year") - } - OverflowDaysDuration => f.write_str( - "number of days derived from duration exceed's \ - Jiff's datetime limits", - ), - OverflowTimeNanoseconds => { - f.write_str("adding duration to time overflowed") - } - RoundMustUseDaysOrBigger { unit } => write!( - f, - "rounding the span between two dates must use days \ - or bigger for its units, but found {unit}", - unit = unit.plural(), - ), - RoundMustUseHoursOrSmaller { unit } => write!( - f, - "rounding the span between two times must use hours \ - or smaller for its units, but found {unit}", - unit = unit.plural(), - ), - } - } -} diff --git a/src/error/duration.rs b/src/error/duration.rs deleted file mode 100644 index 56a8a29..0000000 --- a/src/error/duration.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::error; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - FailedNegateUnsignedDuration, - RangeUnsignedDuration, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::Duration(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - FailedNegateUnsignedDuration => { - f.write_str("failed to negate unsigned duration") - } - RangeUnsignedDuration => { - f.write_str("unsigned duration exceeds Jiff's limits") - } - } - } -} diff --git a/src/error/fmt/friendly.rs b/src/error/fmt/friendly.rs deleted file mode 100644 index e8d0eed..0000000 --- a/src/error/fmt/friendly.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{error, util::escape}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - Empty, - ExpectedColonAfterMinute, - ExpectedIntegerAfterSign, - ExpectedMinuteAfterHour, - ExpectedOneMoreUnitAfterComma, - ExpectedOneSign, - ExpectedSecondAfterMinute, - ExpectedUnitSuffix, - ExpectedWhitespaceAfterComma { byte: u8 }, - ExpectedWhitespaceAfterCommaEndOfInput, - Failed, -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::FmtFriendly(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - Empty => f.write_str("an empty string is not valid"), - ExpectedColonAfterMinute => f.write_str( - "when parsing the `HH:MM:SS` format, \ - expected to parse `:` following minute", - ), - ExpectedIntegerAfterSign => f.write_str( - "expected duration to start \ - with a unit value (a decimal integer) after an \ - optional sign, but no integer was found", - ), - ExpectedMinuteAfterHour => f.write_str( - "when parsing the `HH:MM:SS` format, \ - expected to parse minute following hour", - ), - ExpectedOneMoreUnitAfterComma => f.write_str( - "found comma at the end of duration, \ - but a comma indicates at least one more \ - unit follows", - ), - ExpectedOneSign => f.write_str( - "expected to find either a prefix sign (+/-) or \ - a suffix sign (`ago`), but found both", - ), - ExpectedSecondAfterMinute => f.write_str( - "when parsing the `HH:MM:SS` format, \ - expected to parse second following minute", - ), - ExpectedUnitSuffix => f.write_str( - "expected to find unit designator suffix \ - (e.g., `years` or `secs`) after parsing \ - integer", - ), - ExpectedWhitespaceAfterComma { byte } => write!( - f, - "expected whitespace after comma, but found `{byte}`", - byte = escape::Byte(byte), - ), - ExpectedWhitespaceAfterCommaEndOfInput => f.write_str( - "expected whitespace after comma, but found end of input", - ), - Failed => f.write_str( - "failed to parse input in the \"friendly\" duration format", - ), - } - } -} diff --git a/src/error/fmt/mod.rs b/src/error/fmt/mod.rs deleted file mode 100644 index 14fadb9..0000000 --- a/src/error/fmt/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::{error, util::escape}; - -pub(crate) mod friendly; -pub(crate) mod offset; -pub(crate) mod rfc2822; -pub(crate) mod rfc9557; -pub(crate) mod strtime; -pub(crate) mod temporal; -pub(crate) mod util; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - HybridDurationEmpty, - HybridDurationPrefix { - sign: u8, - }, - IntoFull { - #[cfg(feature = "alloc")] - value: alloc::boxed::Box, - #[cfg(feature = "alloc")] - unparsed: alloc::boxed::Box<[u8]>, - }, - StdFmtWriteAdapter, -} - -impl Error { - pub(crate) fn into_full_error( - _value: &dyn core::fmt::Display, - _unparsed: &[u8], - ) -> Error { - Error::IntoFull { - #[cfg(feature = "alloc")] - value: alloc::string::ToString::to_string(_value).into(), - #[cfg(feature = "alloc")] - unparsed: _unparsed.into(), - } - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::Fmt(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - HybridDurationEmpty => f.write_str( - "an empty string is not a valid duration in either \ - the ISO 8601 format or Jiff's \"friendly\" format", - ), - HybridDurationPrefix { sign } => write!( - f, - "found nothing after sign `{sign}`, \ - which is not a valid duration in either \ - the ISO 8601 format or Jiff's \"friendly\" format", - sign = escape::Byte(sign), - ), - #[cfg(not(feature = "alloc"))] - IntoFull { .. } => f.write_str( - "parsed value, but unparsed input remains \ - (expected no unparsed input)", - ), - #[cfg(feature = "alloc")] - IntoFull { ref value, ref unparsed } => write!( - f, - "parsed value '{value}', but unparsed input {unparsed:?} \ - remains (expected no unparsed input)", - unparsed = escape::Bytes(unparsed), - ), - StdFmtWriteAdapter => { - f.write_str("an error occurred when formatting an argument") - } - } - } -} diff --git a/src/error/fmt/offset.rs b/src/error/fmt/offset.rs deleted file mode 100644 index af81804..0000000 --- a/src/error/fmt/offset.rs +++ /dev/null @@ -1,153 +0,0 @@ -use crate::{error, util::escape}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ColonAfterHours, - EndOfInput, - EndOfInputHour, - EndOfInputMinute, - EndOfInputNumeric, - EndOfInputSecond, - InvalidHours, - InvalidMinutes, - InvalidSeconds, - InvalidSecondsFractional, - InvalidSign, - InvalidSignPlusOrMinus, - MissingMinuteAfterHour, - MissingSecondAfterMinute, - NoColonAfterHours, - ParseHours, - ParseMinutes, - ParseSeconds, - PrecisionLoss, - RangeHours, - RangeMinutes, - RangeSeconds, - SeparatorAfterHours, - SeparatorAfterMinutes, - SubminutePrecisionNotEnabled, - SubsecondPrecisionNotEnabled, - UnexpectedLetterOffsetNoZulu(u8), -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::FmtOffset(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ColonAfterHours => f.write_str( - "parsed hour component of time zone offset, \ - but found colon after hours which is not allowed", - ), - EndOfInput => { - f.write_str("expected UTC offset, but found end of input") - } - EndOfInputHour => f.write_str( - "expected two digit hour after sign, but found end of input", - ), - EndOfInputMinute => f.write_str( - "expected two digit minute after hours, \ - but found end of input", - ), - EndOfInputNumeric => f.write_str( - "expected UTC numeric offset, but found end of input", - ), - EndOfInputSecond => f.write_str( - "expected two digit second after minutes, \ - but found end of input", - ), - InvalidHours => { - f.write_str("failed to parse hours in UTC numeric offset") - } - InvalidMinutes => { - f.write_str("failed to parse minutes in UTC numeric offset") - } - InvalidSeconds => { - f.write_str("failed to parse seconds in UTC numeric offset") - } - InvalidSecondsFractional => f.write_str( - "failed to parse fractional seconds in UTC numeric offset", - ), - InvalidSign => { - f.write_str("failed to parse sign in UTC numeric offset") - } - InvalidSignPlusOrMinus => f.write_str( - "expected `+` or `-` sign at start of UTC numeric offset", - ), - MissingMinuteAfterHour => f.write_str( - "parsed hour component of time zone offset, \ - but could not find required minute component", - ), - MissingSecondAfterMinute => f.write_str( - "parsed hour and minute components of time zone offset, \ - but could not find required second component", - ), - NoColonAfterHours => f.write_str( - "parsed hour component of time zone offset, \ - but could not find required colon separator", - ), - ParseHours => f.write_str( - "failed to parse hours (requires a two digit integer)", - ), - ParseMinutes => f.write_str( - "failed to parse minutes (requires a two digit integer)", - ), - ParseSeconds => f.write_str( - "failed to parse seconds (requires a two digit integer)", - ), - PrecisionLoss => f.write_str( - "due to precision loss, offset is \ - rounded to a value that is out of bounds", - ), - RangeHours => { - f.write_str("hour in time zone offset is out of range") - } - RangeMinutes => { - f.write_str("minute in time zone offset is out of range") - } - RangeSeconds => { - f.write_str("second in time zone offset is out of range") - } - SeparatorAfterHours => f.write_str( - "failed to parse separator after hours in \ - UTC numeric offset", - ), - SeparatorAfterMinutes => f.write_str( - "failed to parse separator after minutes in \ - UTC numeric offset", - ), - SubminutePrecisionNotEnabled => f.write_str( - "subminute precision for UTC numeric offset \ - is not enabled in this context (must provide only \ - integral minutes)", - ), - SubsecondPrecisionNotEnabled => f.write_str( - "subsecond precision for UTC numeric offset \ - is not enabled in this context (must provide only \ - integral minutes or seconds)", - ), - UnexpectedLetterOffsetNoZulu(byte) => write!( - f, - "found `{z}` where a numeric UTC offset \ - was expected (this context does not permit \ - the Zulu offset)", - z = escape::Byte(byte), - ), - } - } -} diff --git a/src/error/fmt/rfc2822.rs b/src/error/fmt/rfc2822.rs deleted file mode 100644 index 764b267..0000000 --- a/src/error/fmt/rfc2822.rs +++ /dev/null @@ -1,231 +0,0 @@ -use crate::{civil::Weekday, error, util::escape}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - CommentClosingParenWithoutOpen, - CommentOpeningParenWithoutClose, - CommentTooManyNestedParens, - EndOfInputDay, - Empty, - EmptyAfterWhitespace, - EndOfInputComma, - EndOfInputHour, - EndOfInputMinute, - EndOfInputMonth, - EndOfInputOffset, - EndOfInputSecond, - EndOfInputTimeSeparator, - FailedTimestamp, - FailedZoned, - InconsistentWeekday { parsed: Weekday, from_date: Weekday }, - InvalidDate, - InvalidHour, - InvalidMinute, - InvalidMonth, - InvalidObsoleteOffset, - InvalidOffsetHour, - InvalidOffsetMinute, - InvalidSecond, - InvalidWeekday { got_non_digit: u8 }, - InvalidYear, - NegativeYear, - ParseDay, - ParseHour, - ParseMinute, - ParseOffsetHour, - ParseOffsetMinute, - ParseSecond, - ParseYear, - TooShortMonth { len: u8 }, - TooShortOffset, - TooShortWeekday { got_non_digit: u8, len: u8 }, - TooShortYear { len: u8 }, - UnexpectedByteComma { byte: u8 }, - UnexpectedByteTimeSeparator { byte: u8 }, - WhitespaceAfterDay, - WhitespaceAfterMonth, - WhitespaceAfterTime, - WhitespaceAfterTimeForObsoleteOffset, - WhitespaceAfterYear, -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::FmtRfc2822(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - CommentClosingParenWithoutOpen => f.write_str( - "found closing parenthesis in comment with \ - no matching opening parenthesis", - ), - CommentOpeningParenWithoutClose => f.write_str( - "found opening parenthesis in comment with \ - no matching closing parenthesis", - ), - CommentTooManyNestedParens => { - f.write_str("found too many nested parenthesis in comment") - } - Empty => { - f.write_str("expected RFC 2822 datetime, but got empty string") - } - EmptyAfterWhitespace => f.write_str( - "expected RFC 2822 datetime, but got empty string \ - after trimming leading whitespace", - ), - EndOfInputComma => f.write_str( - "expected comma after parsed weekday in \ - RFC 2822 datetime, but found end of input instead", - ), - EndOfInputDay => { - f.write_str("expected numeric day, but found end of input") - } - EndOfInputHour => { - f.write_str("expected two digit hour, but found end of input") - } - EndOfInputMinute => f.write_str( - "expected two digit minute, but found end of input", - ), - EndOfInputMonth => f.write_str( - "expected abbreviated month name, but found end of input", - ), - EndOfInputOffset => f.write_str( - "expected sign for time zone offset, \ - (or a legacy time zone name abbreviation), \ - but found end of input", - ), - EndOfInputSecond => f.write_str( - "expected two digit second, but found end of input", - ), - EndOfInputTimeSeparator => f.write_str( - "expected time separator of `:`, but found end of input", - ), - FailedTimestamp => f.write_str( - "failed to parse RFC 2822 datetime into Jiff timestamp", - ), - FailedZoned => f.write_str( - "failed to parse RFC 2822 datetime into Jiff zoned datetime", - ), - InconsistentWeekday { parsed, from_date } => write!( - f, - "found parsed weekday of `{parsed:?}`, \ - but parsed datetime has weekday `{from_date:?}`", - ), - InvalidDate => f.write_str("invalid date"), - InvalidHour => f.write_str("invalid hour"), - InvalidMinute => f.write_str("invalid minute"), - InvalidMonth => f.write_str( - "expected abbreviated month name, \ - but did not recognize a valid abbreviated month name", - ), - InvalidObsoleteOffset => f.write_str( - "expected obsolete RFC 2822 time zone abbreviation, \ - but did not recognize a valid abbreviation", - ), - InvalidOffsetHour => f.write_str("invalid time zone offset hour"), - InvalidOffsetMinute => { - f.write_str("invalid time zone offset minute") - } - InvalidSecond => f.write_str("invalid second"), - InvalidWeekday { got_non_digit } => write!( - f, - "expected day at beginning of RFC 2822 datetime \ - since first non-whitespace byte, `{first}`, \ - is not a digit, but did not recognize a valid \ - weekday abbreviation", - first = escape::Byte(got_non_digit), - ), - InvalidYear => f.write_str("invalid year"), - NegativeYear => f.write_str( - "datetime has negative year, \ - which cannot be formatted with RFC 2822", - ), - ParseDay => f.write_str("failed to parse day"), - ParseHour => f.write_str( - "failed to parse hour (expects a two digit integer)", - ), - ParseMinute => f.write_str( - "failed to parse minute (expects a two digit integer)", - ), - ParseOffsetHour => { - f.write_str("failed to parse hours from time zone offset") - } - ParseOffsetMinute => { - f.write_str("failed to parse minutes from time zone offset") - } - ParseSecond => f.write_str( - "failed to parse second (expects a two digit integer)", - ), - ParseYear => f.write_str( - "failed to parse year \ - (expects a two, three or four digit integer)", - ), - TooShortMonth { len } => write!( - f, - "expected abbreviated month name, but remaining input \ - is too short (remaining bytes is {len})", - ), - TooShortOffset => write!( - f, - "expected at least four digits for time zone offset \ - after sign, but found fewer than four bytes remaining", - ), - TooShortWeekday { got_non_digit, len } => write!( - f, - "expected day at beginning of RFC 2822 datetime \ - since first non-whitespace byte, `{first}`, \ - is not a digit, but given string is too short \ - (length is {len})", - first = escape::Byte(got_non_digit), - ), - TooShortYear { len } => write!( - f, - "expected at least two ASCII digits for parsing \ - a year, but only found {len}", - ), - UnexpectedByteComma { byte } => write!( - f, - "expected comma after parsed weekday in \ - RFC 2822 datetime, but found `{got}` instead", - got = escape::Byte(byte), - ), - UnexpectedByteTimeSeparator { byte } => write!( - f, - "expected time separator of `:`, but found `{got}`", - got = escape::Byte(byte), - ), - WhitespaceAfterDay => { - f.write_str("expected whitespace after parsing day") - } - WhitespaceAfterMonth => f.write_str( - "expected whitespace after parsing abbreviated month name", - ), - WhitespaceAfterTime => f.write_str( - "expected whitespace after parsing time: \ - expected at least one whitespace character \ - (space or tab), but found none", - ), - WhitespaceAfterTimeForObsoleteOffset => f.write_str( - "expected obsolete RFC 2822 time zone abbreviation, \ - but found no remaining non-whitespace characters \ - after time", - ), - WhitespaceAfterYear => { - f.write_str("expected whitespace after parsing year") - } - } - } -} diff --git a/src/error/fmt/rfc9557.rs b/src/error/fmt/rfc9557.rs deleted file mode 100644 index 28a0ea6..0000000 --- a/src/error/fmt/rfc9557.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::{error, util::escape}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - EndOfInputAnnotation, - EndOfInputAnnotationClose, - EndOfInputAnnotationKey, - EndOfInputAnnotationSeparator, - EndOfInputAnnotationValue, - EndOfInputTzAnnotationClose, - UnexpectedByteAnnotation { byte: u8 }, - UnexpectedByteAnnotationClose { byte: u8 }, - UnexpectedByteAnnotationKey { byte: u8 }, - UnexpectedByteAnnotationValue { byte: u8 }, - UnexpectedByteAnnotationSeparator { byte: u8 }, - UnexpectedByteTzAnnotationClose { byte: u8 }, - UnexpectedSlashAnnotationSeparator, - UnsupportedAnnotationCritical, -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::FmtRfc9557(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - EndOfInputAnnotation => f.write_str( - "expected the start of an RFC 9557 annotation or IANA \ - time zone component name, but found end of input instead", - ), - EndOfInputAnnotationClose => f.write_str( - "expected an `]` after parsing an RFC 9557 annotation key \ - and value, but found end of input instead", - ), - EndOfInputAnnotationKey => f.write_str( - "expected the start of an RFC 9557 annotation key, \ - but found end of input instead", - ), - EndOfInputAnnotationSeparator => f.write_str( - "expected an `=` after parsing an RFC 9557 annotation key, \ - but found end of input instead", - ), - EndOfInputAnnotationValue => f.write_str( - "expected the start of an RFC 9557 annotation value, \ - but found end of input instead", - ), - EndOfInputTzAnnotationClose => f.write_str( - "expected an `]` after parsing an RFC 9557 time zone \ - annotation, but found end of input instead", - ), - UnexpectedByteAnnotation { byte } => write!( - f, - "expected ASCII alphabetic byte (or underscore or period) \ - at the start of an RFC 9557 annotation or time zone \ - component name, but found `{byte}` instead", - byte = escape::Byte(byte), - ), - UnexpectedByteAnnotationClose { byte } => write!( - f, - "expected an `]` after parsing an RFC 9557 annotation key \ - and value, but found `{byte}` instead", - byte = escape::Byte(byte), - ), - UnexpectedByteAnnotationKey { byte } => write!( - f, - "expected lowercase alphabetic byte (or underscore) \ - at the start of an RFC 9557 annotation key, \ - but found `{byte}` instead", - byte = escape::Byte(byte), - ), - UnexpectedByteAnnotationValue { byte } => write!( - f, - "expected alphanumeric ASCII byte \ - at the start of an RFC 9557 annotation value, \ - but found `{byte}` instead", - byte = escape::Byte(byte), - ), - UnexpectedByteAnnotationSeparator { byte } => write!( - f, - "expected an `=` after parsing an RFC 9557 annotation \ - key, but found `{byte}` instead", - byte = escape::Byte(byte), - ), - UnexpectedByteTzAnnotationClose { byte } => write!( - f, - "expected an `]` after parsing an RFC 9557 time zone \ - annotation, but found `{byte}` instead", - byte = escape::Byte(byte), - ), - UnexpectedSlashAnnotationSeparator => f.write_str( - "expected an `=` after parsing an RFC 9557 annotation \ - key, but found `/` instead (time zone annotations must \ - come first)", - ), - UnsupportedAnnotationCritical => f.write_str( - "found unsupported RFC 9557 annotation \ - with the critical flag (`!`) set", - ), - } - } -} diff --git a/src/error/fmt/strtime.rs b/src/error/fmt/strtime.rs deleted file mode 100644 index efa5d0e..0000000 --- a/src/error/fmt/strtime.rs +++ /dev/null @@ -1,517 +0,0 @@ -use crate::{civil::Weekday, error, tz::Offset, util::escape}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ColonCount { - directive: u8, - }, - DirectiveFailure { - directive: u8, - colons: u8, - }, - DirectiveFailureDot { - directive: u8, - }, - ExpectedDirectiveAfterColons, - ExpectedDirectiveAfterFlag { - flag: u8, - }, - ExpectedDirectiveAfterWidth, - FailedStrftime, - FailedStrptime, - FailedWidth, - InvalidDate, - InvalidISOWeekDate, - InvalidWeekdayMonday { - got: Weekday, - }, - InvalidWeekdaySunday { - got: Weekday, - }, - MismatchOffset { - parsed: Offset, - got: Offset, - }, - MismatchWeekday { - parsed: Weekday, - got: Weekday, - }, - MissingTimeHourForFractional, - MissingTimeHourForMinute, - MissingTimeHourForSecond, - MissingTimeMinuteForFractional, - MissingTimeMinuteForSecond, - MissingTimeSecondForFractional, - RangeTimestamp, - RangeWidth, - RequiredDateForDateTime, - RequiredDateTimeForTimestamp, - RequiredDateTimeForZoned, - RequiredOffsetForTimestamp, - RequiredSomeDayForDate, - RequiredTimeForDateTime, - RequiredYearForDate, - UnconsumedStrptime { - #[cfg(feature = "alloc")] - remaining: alloc::boxed::Box<[u8]>, - }, - UnexpectedEndAfterDot, - UnexpectedEndAfterPercent, - UnknownDirectiveAfterDot { - directive: u8, - }, - UnknownDirective { - directive: u8, - }, - ZonedOffsetOrTz, -} - -impl Error { - pub(crate) fn unconsumed(_remaining: &[u8]) -> Error { - Error::UnconsumedStrptime { - #[cfg(feature = "alloc")] - remaining: _remaining.into(), - } - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::FmtStrtime(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ColonCount { directive } => write!( - f, - "invalid number of `:` in `%{directive}` directive", - directive = escape::Byte(directive), - ), - DirectiveFailure { directive, colons } => write!( - f, - "%{colons}{directive} failed", - colons = escape::RepeatByte { byte: b':', count: colons }, - directive = escape::Byte(directive), - ), - DirectiveFailureDot { directive } => write!( - f, - "%.{directive} failed", - directive = escape::Byte(directive), - ), - ExpectedDirectiveAfterColons => f.write_str( - "expected to find specifier directive after colons, \ - but found end of format string", - ), - ExpectedDirectiveAfterFlag { flag } => write!( - f, - "expected to find specifier directive after flag \ - `{flag}`, but found end of format string", - flag = escape::Byte(flag), - ), - ExpectedDirectiveAfterWidth => f.write_str( - "expected to find specifier directive after parsed width, \ - but found end of format string", - ), - FailedStrftime => f.write_str("strftime formatting failed"), - FailedStrptime => f.write_str("strptime parsing failed"), - FailedWidth => { - f.write_str("failed to parse conversion specifier width") - } - InvalidDate => f.write_str("invalid date"), - InvalidISOWeekDate => f.write_str("invalid ISO 8601 week date"), - InvalidWeekdayMonday { got } => write!( - f, - "weekday `{got:?}` is not valid for \ - Monday based week number", - ), - InvalidWeekdaySunday { got } => write!( - f, - "weekday `{got:?}` is not valid for \ - Sunday based week number", - ), - MissingTimeHourForFractional => f.write_str( - "parsing format did not include hour directive, \ - but did include fractional second directive (cannot have \ - smaller time units with bigger time units missing)", - ), - MissingTimeHourForMinute => f.write_str( - "parsing format did not include hour directive, \ - but did include minute directive (cannot have \ - smaller time units with bigger time units missing)", - ), - MissingTimeHourForSecond => f.write_str( - "parsing format did not include hour directive, \ - but did include second directive (cannot have \ - smaller time units with bigger time units missing)", - ), - MissingTimeMinuteForFractional => f.write_str( - "parsing format did not include minute directive, \ - but did include fractional second directive (cannot have \ - smaller time units with bigger time units missing)", - ), - MissingTimeMinuteForSecond => f.write_str( - "parsing format did not include minute directive, \ - but did include second directive (cannot have \ - smaller time units with bigger time units missing)", - ), - MissingTimeSecondForFractional => f.write_str( - "parsing format did not include second directive, \ - but did include fractional second directive (cannot have \ - smaller time units with bigger time units missing)", - ), - MismatchOffset { parsed, got } => write!( - f, - "parsed time zone offset `{parsed}`, but \ - offset from timestamp and time zone is `{got}`", - ), - MismatchWeekday { parsed, got } => write!( - f, - "parsed weekday `{parsed:?}` does not match \ - weekday `{got:?}` from parsed date", - ), - RangeTimestamp => f.write_str( - "parsed datetime and offset, \ - but combining them into a zoned datetime \ - is outside Jiff's supported timestamp range", - ), - RangeWidth => write!( - f, - "parsed width is too big, max is {max}", - max = u8::MAX - ), - RequiredDateForDateTime => { - f.write_str("date required to parse datetime") - } - RequiredDateTimeForTimestamp => { - f.write_str("datetime required to parse timestamp") - } - RequiredDateTimeForZoned => { - f.write_str("datetime required to parse zoned datetime") - } - RequiredOffsetForTimestamp => { - f.write_str("offset required to parse timestamp") - } - RequiredSomeDayForDate => f.write_str( - "a month/day, day-of-year or week date must be \ - present to create a date, but none were found", - ), - RequiredTimeForDateTime => { - f.write_str("time required to parse datetime") - } - RequiredYearForDate => f.write_str("year required to parse date"), - #[cfg(feature = "alloc")] - UnconsumedStrptime { ref remaining } => write!( - f, - "strptime expects to consume the entire input, but \ - `{remaining}` remains unparsed", - remaining = escape::Bytes(remaining), - ), - #[cfg(not(feature = "alloc"))] - UnconsumedStrptime {} => f.write_str( - "strptime expects to consume the entire input, but \ - there is unparsed input remaining", - ), - UnexpectedEndAfterDot => f.write_str( - "invalid format string, expected directive after `%.`", - ), - UnexpectedEndAfterPercent => f.write_str( - "invalid format string, expected byte after `%`, \ - but found end of format string", - ), - UnknownDirective { directive } => write!( - f, - "found unrecognized specifier directive `{directive}`", - directive = escape::Byte(directive), - ), - UnknownDirectiveAfterDot { directive } => write!( - f, - "found unrecognized specifier directive `{directive}` \ - following `%.`", - directive = escape::Byte(directive), - ), - ZonedOffsetOrTz => f.write_str( - "either offset (from `%z`) or IANA time zone identifier \ - (from `%Q`) is required for parsing zoned datetime", - ), - } - } -} - -#[derive(Clone, Debug)] -pub(crate) enum ParseError { - ExpectedAmPm, - ExpectedAmPmTooShort, - ExpectedIanaTz, - ExpectedIanaTzEndOfInput, - ExpectedMonthAbbreviation, - ExpectedMonthAbbreviationTooShort, - ExpectedWeekdayAbbreviation, - ExpectedWeekdayAbbreviationTooShort, - ExpectedChoice { - available: &'static [&'static [u8]], - }, - ExpectedFractionalDigit, - ExpectedMatchLiteralByte { - expected: u8, - got: u8, - }, - ExpectedMatchLiteralEndOfInput { - expected: u8, - }, - ExpectedNonEmpty { - directive: u8, - }, - #[cfg(not(feature = "alloc"))] - NotAllowedAlloc { - directive: u8, - colons: u8, - }, - NotAllowedLocaleClockTime, - NotAllowedLocaleDate, - NotAllowedLocaleDateAndTime, - NotAllowedLocaleTwelveHourClockTime, - NotAllowedTimeZoneAbbreviation, - ParseDay, - ParseDayOfYear, - ParseCentury, - ParseFractionalSeconds, - ParseHour, - ParseIsoWeekNumber, - ParseIsoWeekYear, - ParseIsoWeekYearTwoDigit, - ParseMinute, - ParseMondayWeekNumber, - ParseMonth, - ParseSecond, - ParseSundayWeekNumber, - ParseTimestamp, - ParseWeekdayNumber, - ParseYear, - ParseYearTwoDigit, - UnknownMonthName, - UnknownWeekdayAbbreviation, -} - -impl error::IntoError for ParseError { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: ParseError) -> error::Error { - error::ErrorKind::FmtStrtimeParse(err).into() - } -} - -impl core::fmt::Display for ParseError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::ParseError::*; - - match *self { - ExpectedAmPm => f.write_str("expected to find `AM` or `PM`"), - ExpectedAmPmTooShort => f.write_str( - "expected to find `AM` or `PM`, \ - but the remaining input is too short \ - to contain one", - ), - ExpectedIanaTz => f.write_str( - "expected to find the start of an IANA time zone \ - identifier name or component", - ), - ExpectedIanaTzEndOfInput => f.write_str( - "expected to find the start of an IANA time zone \ - identifier name or component, \ - but found end of input instead", - ), - ExpectedMonthAbbreviation => { - f.write_str("expected to find month name abbreviation") - } - ExpectedMonthAbbreviationTooShort => f.write_str( - "expected to find month name abbreviation, \ - but the remaining input is too short \ - to contain one", - ), - ExpectedWeekdayAbbreviation => { - f.write_str("expected to find weekday abbreviation") - } - ExpectedWeekdayAbbreviationTooShort => f.write_str( - "expected to find weekday abbreviation, \ - but the remaining input is too short \ - to contain one", - ), - ExpectedChoice { available } => { - f.write_str( - "failed to find expected value, available choices are: ", - )?; - for (i, choice) in available.iter().enumerate() { - if i > 0 { - f.write_str(", ")?; - } - write!(f, "{}", escape::Bytes(choice))?; - } - Ok(()) - } - ExpectedFractionalDigit => f.write_str( - "expected at least one fractional decimal digit, \ - but did not find any", - ), - ExpectedMatchLiteralByte { expected, got } => write!( - f, - "expected to match literal byte `{expected}` from \ - format string, but found byte `{got}` in input", - expected = escape::Byte(expected), - got = escape::Byte(got), - ), - ExpectedMatchLiteralEndOfInput { expected } => write!( - f, - "expected to match literal byte `{expected}` from \ - format string, but found end of input", - expected = escape::Byte(expected), - ), - ExpectedNonEmpty { directive } => write!( - f, - "expected non-empty input for directive `%{directive}`, \ - but found end of input", - directive = escape::Byte(directive), - ), - #[cfg(not(feature = "alloc"))] - NotAllowedAlloc { directive, colons } => write!( - f, - "cannot parse `%{colons}{directive}` \ - without Jiff's `alloc` feature enabled", - colons = escape::RepeatByte { byte: b':', count: colons }, - directive = escape::Byte(directive), - ), - NotAllowedLocaleClockTime => { - f.write_str("parsing locale clock time is not allowed") - } - NotAllowedLocaleDate => { - f.write_str("parsing locale date is not allowed") - } - NotAllowedLocaleDateAndTime => { - f.write_str("parsing locale date and time is not allowed") - } - NotAllowedLocaleTwelveHourClockTime => { - f.write_str("parsing locale 12-hour clock time is not allowed") - } - NotAllowedTimeZoneAbbreviation => { - f.write_str("parsing time zone abbreviation is not allowed") - } - ParseCentury => { - f.write_str("failed to parse year number for century") - } - ParseDay => f.write_str("failed to parse day number"), - ParseDayOfYear => { - f.write_str("failed to parse day of year number") - } - ParseFractionalSeconds => f.write_str( - "failed to parse fractional second component \ - (up to 9 digits, nanosecond precision)", - ), - ParseHour => f.write_str("failed to parse hour number"), - ParseMinute => f.write_str("failed to parse minute number"), - ParseWeekdayNumber => { - f.write_str("failed to parse weekday number") - } - ParseIsoWeekNumber => { - f.write_str("failed to parse ISO 8601 week number") - } - ParseIsoWeekYear => { - f.write_str("failed to parse ISO 8601 week year") - } - ParseIsoWeekYearTwoDigit => { - f.write_str("failed to parse 2-digit ISO 8601 week year") - } - ParseMondayWeekNumber => { - f.write_str("failed to parse Monday-based week number") - } - ParseMonth => f.write_str("failed to parse month number"), - ParseSecond => f.write_str("failed to parse second number"), - ParseSundayWeekNumber => { - f.write_str("failed to parse Sunday-based week number") - } - ParseTimestamp => { - f.write_str("failed to parse Unix timestamp (in seconds)") - } - ParseYear => f.write_str("failed to parse year"), - ParseYearTwoDigit => f.write_str("failed to parse 2-digit year"), - UnknownMonthName => f.write_str("unrecognized month name"), - UnknownWeekdayAbbreviation => { - f.write_str("unrecognized weekday abbreviation") - } - } - } -} - -#[derive(Clone, Debug)] -pub(crate) enum FormatError { - RequiresDate, - RequiresInstant, - RequiresOffset, - RequiresTime, - RequiresTimeZone, - RequiresTimeZoneOrOffset, - InvalidUtf8, - ZeroPrecisionFloat, - ZeroPrecisionNano, -} - -impl error::IntoError for FormatError { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: FormatError) -> error::Error { - error::ErrorKind::FmtStrtimeFormat(err).into() - } -} - -impl core::fmt::Display for FormatError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::FormatError::*; - - match *self { - RequiresDate => f.write_str("requires date to format"), - RequiresInstant => f.write_str( - "requires instant (a timestamp or a date, time and offset)", - ), - RequiresTime => f.write_str("requires time to format"), - RequiresOffset => f.write_str("requires time zone offset"), - RequiresTimeZone => { - f.write_str("requires IANA time zone identifier") - } - RequiresTimeZoneOrOffset => f.write_str( - "requires IANA time zone identifier or \ - time zone offset, but neither were present", - ), - InvalidUtf8 => { - f.write_str("invalid format string, it must be valid UTF-8") - } - ZeroPrecisionFloat => { - f.write_str("zero precision with %f is not allowed") - } - ZeroPrecisionNano => { - f.write_str("zero precision with %N is not allowed") - } - } - } -} diff --git a/src/error/fmt/temporal.rs b/src/error/fmt/temporal.rs deleted file mode 100644 index 4f0cd34..0000000 --- a/src/error/fmt/temporal.rs +++ /dev/null @@ -1,354 +0,0 @@ -use crate::{error, tz::Offset, util::escape}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - #[cfg(not(feature = "alloc"))] - AllocPosixTimeZone, - AmbiguousTimeMonthDay, - AmbiguousTimeYearMonth, - CivilDateTimeZulu, - ConvertDateTimeToTimestamp { - offset: Offset, - }, - EmptyTimeZone, - ExpectedDateDesignatorFoundByte { - byte: u8, - }, - ExpectedDateDesignatorFoundEndOfInput, - ExpectedDurationDesignatorFoundByte { - byte: u8, - }, - ExpectedDurationDesignatorFoundEndOfInput, - ExpectedFourDigitYear, - ExpectedNoSeparator, - ExpectedOneDigitWeekday, - ExpectedSeparatorFoundByte { - byte: u8, - }, - ExpectedSeparatorFoundEndOfInput, - ExpectedSixDigitYear, - ExpectedTimeDesignator, - ExpectedTimeDesignatorFoundByte { - byte: u8, - }, - ExpectedTimeDesignatorFoundEndOfInput, - ExpectedTimeUnits, - ExpectedTwoDigitDay, - ExpectedTwoDigitHour, - ExpectedTwoDigitMinute, - ExpectedTwoDigitMonth, - ExpectedTwoDigitSecond, - ExpectedTwoDigitWeekNumber, - ExpectedWeekPrefixFoundByte { - byte: u8, - }, - ExpectedWeekPrefixFoundEndOfInput, - FailedDayInDate, - FailedDayInMonthDay, - FailedFractionalSecondInTime, - FailedHourInTime, - FailedMinuteInTime, - FailedMonthInDate, - FailedMonthInMonthDay, - FailedMonthInYearMonth, - FailedOffsetNumeric, - FailedSecondInTime, - FailedSeparatorAfterMonth, - FailedSeparatorAfterWeekNumber, - FailedSeparatorAfterYear, - FailedTzdbLookup, - FailedWeekNumberInDate, - FailedWeekNumberPrefixInDate, - FailedWeekdayInDate, - FailedYearInDate, - FailedYearInYearMonth, - InvalidDate, - InvalidDay, - InvalidHour, - InvalidMinute, - InvalidMonth, - InvalidMonthDay, - InvalidSecond, - InvalidTimeZoneUtf8, - InvalidWeekDate, - InvalidWeekNumber, - InvalidWeekday, - InvalidYear, - InvalidYearMonth, - InvalidYearZero, - MissingOffsetInTimestamp, - MissingTimeInDate, - MissingTimeInTimestamp, - MissingTimeZoneAnnotation, - ParseDayTwoDigit, - ParseHourTwoDigit, - ParseMinuteTwoDigit, - ParseMonthTwoDigit, - ParseSecondTwoDigit, - ParseWeekNumberTwoDigit, - ParseWeekdayOneDigit, - ParseYearFourDigit, - ParseYearSixDigit, - // This is the only error for formatting a Temporal value. And - // actually, it's not even part of Temporal, but just lives in that - // module (for convenience reasons). - PrintTimeZoneFailure, -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::FmtTemporal(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - #[cfg(not(feature = "alloc"))] - AllocPosixTimeZone => f.write_str( - "cannot parsed time zones other than IANA time zone \ - identifiers or fixed offsets \ - without the `alloc` crate feature enabled for `jiff`", - ), - AmbiguousTimeMonthDay => { - f.write_str("parsed time is ambiguous with a month-day date") - } - AmbiguousTimeYearMonth => { - f.write_str("parsed time is ambiguous with a year-month date") - } - CivilDateTimeZulu => f.write_str( - "cannot parse civil date/time from string with a Zulu \ - offset, parse as a `jiff::Timestamp` first \ - and convert to a civil date/time instead", - ), - ConvertDateTimeToTimestamp { offset } => write!( - f, - "failed to convert civil datetime to timestamp \ - with offset {offset}", - ), - EmptyTimeZone => { - f.write_str("an empty string is not a valid time zone") - } - ExpectedDateDesignatorFoundByte { byte } => write!( - f, - "expected to find date unit designator suffix \ - (`Y`, `M`, `W` or `D`), but found `{byte}` instead", - byte = escape::Byte(byte), - ), - ExpectedDateDesignatorFoundEndOfInput => f.write_str( - "expected to find date unit designator suffix \ - (`Y`, `M`, `W` or `D`), but found end of input", - ), - ExpectedDurationDesignatorFoundByte { byte } => write!( - f, - "expected to find duration beginning with `P` or `p`, \ - but found `{byte}` instead", - byte = escape::Byte(byte), - ), - ExpectedDurationDesignatorFoundEndOfInput => f.write_str( - "expected to find duration beginning with `P` or `p`, \ - but found end of input", - ), - ExpectedFourDigitYear => f.write_str( - "expected four digit year (or leading sign for \ - six digit year), but found end of input", - ), - ExpectedNoSeparator => f.write_str( - "expected no separator since none was \ - found after the year, but found a `-` separator", - ), - ExpectedOneDigitWeekday => f.write_str( - "expected one digit weekday, but found end of input", - ), - ExpectedSeparatorFoundByte { byte } => write!( - f, - "expected `-` separator, but found `{byte}`", - byte = escape::Byte(byte), - ), - ExpectedSeparatorFoundEndOfInput => { - f.write_str("expected `-` separator, but found end of input") - } - ExpectedSixDigitYear => f.write_str( - "expected six digit year (because of a leading sign), \ - but found end of input", - ), - ExpectedTimeDesignator => f.write_str( - "parsing ISO 8601 duration in this context requires \ - that the duration contain a time component and no \ - components of days or greater", - ), - ExpectedTimeDesignatorFoundByte { byte } => write!( - f, - "expected to find time unit designator suffix \ - (`H`, `M` or `S`), but found `{byte}` instead", - byte = escape::Byte(byte), - ), - ExpectedTimeDesignatorFoundEndOfInput => f.write_str( - "expected to find time unit designator suffix \ - (`H`, `M` or `S`), but found end of input", - ), - ExpectedTimeUnits => f.write_str( - "found a time designator (`T` or `t`) in an ISO 8601 \ - duration string, but did not find any time units", - ), - ExpectedTwoDigitDay => { - f.write_str("expected two digit day, but found end of input") - } - ExpectedTwoDigitHour => { - f.write_str("expected two digit hour, but found end of input") - } - ExpectedTwoDigitMinute => f.write_str( - "expected two digit minute, but found end of input", - ), - ExpectedTwoDigitMonth => { - f.write_str("expected two digit month, but found end of input") - } - ExpectedTwoDigitSecond => f.write_str( - "expected two digit second, but found end of input", - ), - ExpectedTwoDigitWeekNumber => f.write_str( - "expected two digit week number, but found end of input", - ), - ExpectedWeekPrefixFoundByte { byte } => write!( - f, - "expected `W` or `w`, but found `{byte}` instead", - byte = escape::Byte(byte), - ), - ExpectedWeekPrefixFoundEndOfInput => { - f.write_str("expected `W` or `w`, but found end of input") - } - FailedDayInDate => f.write_str("failed to parse day in date"), - FailedDayInMonthDay => { - f.write_str("failed to payse day in month-day") - } - FailedFractionalSecondInTime => { - f.write_str("failed to parse fractional seconds in time") - } - FailedHourInTime => f.write_str("failed to parse hour in time"), - FailedMinuteInTime => { - f.write_str("failed to parse minute in time") - } - FailedMonthInDate => f.write_str("failed to parse month in date"), - FailedMonthInMonthDay => { - f.write_str("failed to parse month in month-day") - } - FailedMonthInYearMonth => { - f.write_str("failed to parse month in year-month") - } - FailedOffsetNumeric => f.write_str( - "offset successfully parsed, \ - but failed to convert to numeric `jiff::tz::Offset`", - ), - FailedSecondInTime => { - f.write_str("failed to parse second in time") - } - FailedSeparatorAfterMonth => { - f.write_str("failed to parse separator after month") - } - FailedSeparatorAfterWeekNumber => { - f.write_str("failed to parse separator after week number") - } - FailedSeparatorAfterYear => { - f.write_str("failed to parse separator after year") - } - FailedTzdbLookup => f.write_str( - "parsed apparent IANA time zone identifier, \ - but the tzdb lookup failed", - ), - FailedWeekNumberInDate => { - f.write_str("failed to parse week number in date") - } - FailedWeekNumberPrefixInDate => { - f.write_str("failed to parse week number prefix in date") - } - FailedWeekdayInDate => { - f.write_str("failed to parse weekday in date") - } - FailedYearInDate => f.write_str("failed to parse year in date"), - FailedYearInYearMonth => { - f.write_str("failed to parse year in year-month") - } - InvalidDate => f.write_str("parsed date is not valid"), - InvalidDay => f.write_str("parsed day is not valid"), - InvalidHour => f.write_str("parsed hour is not valid"), - InvalidMinute => f.write_str("parsed minute is not valid"), - InvalidMonth => f.write_str("parsed month is not valid"), - InvalidMonthDay => f.write_str("parsed month-day is not valid"), - InvalidSecond => f.write_str("parsed second is not valid"), - InvalidTimeZoneUtf8 => f.write_str( - "found plausible IANA time zone identifier, \ - but it is not valid UTF-8", - ), - InvalidWeekDate => f.write_str("parsed week date is not valid"), - InvalidWeekNumber => { - f.write_str("parsed week number is not valid") - } - InvalidWeekday => f.write_str("parsed weekday is not valid"), - InvalidYear => f.write_str("parsed year is not valid"), - InvalidYearMonth => f.write_str("parsed year-month is not valid"), - InvalidYearZero => f.write_str( - "year zero must be written without a sign or a \ - positive sign, but not a negative sign", - ), - MissingOffsetInTimestamp => f.write_str( - "failed to find offset component, \ - which is required for parsing a timestamp", - ), - MissingTimeInDate => f.write_str( - "successfully parsed date, but no time component was found", - ), - MissingTimeInTimestamp => f.write_str( - "failed to find time component, \ - which is required for parsing a timestamp", - ), - MissingTimeZoneAnnotation => f.write_str( - "failed to find time zone annotation in square brackets, \ - which is required for parsing a zoned datetime", - ), - ParseDayTwoDigit => { - f.write_str("failed to parse two digit integer as day") - } - ParseHourTwoDigit => { - f.write_str("failed to parse two digit integer as hour") - } - ParseMinuteTwoDigit => { - f.write_str("failed to parse two digit integer as minute") - } - ParseMonthTwoDigit => { - f.write_str("failed to parse two digit integer as month") - } - ParseSecondTwoDigit => { - f.write_str("failed to parse two digit integer as second") - } - ParseWeekNumberTwoDigit => { - f.write_str("failed to parse two digit integer as week number") - } - ParseWeekdayOneDigit => { - f.write_str("failed to parse one digit integer as weekday") - } - ParseYearFourDigit => { - f.write_str("failed to parse four digit integer as year") - } - ParseYearSixDigit => { - f.write_str("failed to parse six digit integer as year") - } - PrintTimeZoneFailure => f.write_str( - "time zones without IANA identifiers that aren't either \ - fixed offsets or a POSIX time zone can't be serialized \ - (this typically occurs when this is a system time zone \ - derived from `/etc/localtime` on Unix systems that \ - isn't symlinked to an entry in `/usr/share/zoneinfo`)", - ), - } - } -} diff --git a/src/error/fmt/util.rs b/src/error/fmt/util.rs deleted file mode 100644 index ae0ef1a..0000000 --- a/src/error/fmt/util.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::{error, Unit}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ConversionToSecondsFailed { unit: Unit }, - EmptyDuration, - FailedValueSet { unit: Unit }, - InvalidFraction, - InvalidFractionNanos, - MissingFractionalDigits, - NotAllowedCalendarUnit { unit: Unit }, - NotAllowedFractionalUnit { found: Unit }, - NotAllowedNegative, - OutOfOrderHMS { found: Unit }, - OutOfOrderUnits { found: Unit, previous: Unit }, - OverflowForUnit { unit: Unit }, - OverflowForUnitFractional { unit: Unit }, - SignedOverflowForUnit { unit: Unit }, -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::FmtUtil(err).into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ConversionToSecondsFailed { unit } => write!( - f, - "converting {unit} to seconds overflows \ - a signed 64-bit integer", - unit = unit.plural(), - ), - EmptyDuration => f.write_str("no parsed duration components"), - FailedValueSet { unit } => write!( - f, - "failed to set value for {unit} unit on span", - unit = unit.singular(), - ), - InvalidFraction => f.write_str( - "failed to parse fractional component \ - (up to 9 digits, nanosecond precision is allowed)", - ), - InvalidFractionNanos => f.write_str( - "failed to set nanosecond value from fractional component", - ), - MissingFractionalDigits => f.write_str( - "found decimal after seconds component, \ - but did not find any digits after decimal", - ), - NotAllowedCalendarUnit { unit } => write!( - f, - "parsing calendar units ({unit} in this case) \ - in this context is not supported \ - (perhaps try parsing into a `jiff::Span` instead)", - unit = unit.plural(), - ), - NotAllowedFractionalUnit { found } => write!( - f, - "fractional {found} are not supported", - found = found.plural(), - ), - NotAllowedNegative => f.write_str( - "cannot parse negative duration into unsigned \ - `std::time::Duration`", - ), - OutOfOrderHMS { found } => write!( - f, - "found `HH:MM:SS` after unit {found}, \ - but `HH:MM:SS` can only appear after \ - years, months, weeks or days", - found = found.singular(), - ), - OutOfOrderUnits { found, previous } => write!( - f, - "found value with unit {found} \ - after unit {previous}, but units must be \ - written from largest to smallest \ - (and they can't be repeated)", - found = found.singular(), - previous = previous.singular(), - ), - OverflowForUnit { unit } => write!( - f, - "accumulated duration \ - overflowed when adding value to unit {unit}", - unit = unit.singular(), - ), - OverflowForUnitFractional { unit } => write!( - f, - "accumulated duration \ - overflowed when adding fractional value to unit {unit}", - unit = unit.singular(), - ), - SignedOverflowForUnit { unit } => write!( - f, - "value for {unit} is too big (or small) to fit into \ - a signed 64-bit integer", - unit = unit.plural(), - ), - } - } -} diff --git a/src/error/signed_duration.rs b/src/error/signed_duration.rs deleted file mode 100644 index 06191b9..0000000 --- a/src/error/signed_duration.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::{error, Unit}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ConvertNonFinite, - ConvertSystemTime, - RoundCalendarUnit { unit: Unit }, - RoundOverflowed { unit: Unit }, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::SignedDuration(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ConvertNonFinite => f.write_str( - "could not convert non-finite \ - floating point seconds to signed duration \ - (floating point seconds must be finite)", - ), - ConvertSystemTime => f.write_str( - "failed to get duration between \ - `std::time::SystemTime` values", - ), - RoundCalendarUnit { unit } => write!( - f, - "rounding `jiff::SignedDuration` failed because \ - a calendar unit of '{plural}' was provided \ - (to round by calendar units, you must use a `jiff::Span`)", - plural = unit.plural(), - ), - RoundOverflowed { unit } => write!( - f, - "rounding signed duration to nearest {singular} \ - resulted in a value outside the supported range \ - of a `jiff::SignedDuration`", - singular = unit.singular(), - ), - } - } -} diff --git a/src/error/span.rs b/src/error/span.rs deleted file mode 100644 index 3f8d1b0..0000000 --- a/src/error/span.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::{error, Unit}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ConvertDateTimeToTimestamp, - ConvertNanoseconds { unit: Unit }, - ConvertNegative, - ConvertSpanToSignedDuration, - FailedSpanBetweenDateTimes { unit: Unit }, - FailedSpanBetweenZonedDateTimes { unit: Unit }, - NotAllowedCalendarUnits { unit: Unit }, - NotAllowedLargestSmallerThanSmallest { smallest: Unit, largest: Unit }, - OptionLargest, - OptionLargestInSpan, - OptionSmallest, - RequiresRelativeWeekOrDay { unit: Unit }, - RequiresRelativeYearOrMonth { unit: Unit }, - RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit: Unit }, - ToDurationCivil, - ToDurationDaysAre24Hours, - ToDurationZoned, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::Span(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ConvertDateTimeToTimestamp => f.write_str( - "failed to interpret datetime in UTC \ - in order to convert it to a timestamp", - ), - ConvertNanoseconds { unit } => write!( - f, - "failed to convert rounded nanoseconds \ - to span for largest unit set to '{unit}'", - unit = unit.plural(), - ), - ConvertNegative => f.write_str( - "cannot convert negative span \ - to unsigned `std::time::Duration`", - ), - ConvertSpanToSignedDuration => f.write_str( - "failed to convert span to duration without relative datetime \ - (must use `jiff::Span::to_duration` instead)", - ), - FailedSpanBetweenDateTimes { unit } => write!( - f, - "failed to get span between datetimes \ - with largest unit set to '{unit}'", - unit = unit.plural(), - ), - FailedSpanBetweenZonedDateTimes { unit } => write!( - f, - "failed to get span between zoned datetimes \ - with largest unit set to '{unit}'", - unit = unit.plural(), - ), - OptionLargest => { - f.write_str("error with `largest` rounding option") - } - OptionLargestInSpan => { - f.write_str("error with largest unit in span to be rounded") - } - OptionSmallest => { - f.write_str("error with `smallest` rounding option") - } - NotAllowedCalendarUnits { unit } => write!( - f, - "operation can only be performed with units of hours \ - or smaller, but found non-zero '{unit}' units \ - (operations on `jiff::Timestamp`, `jiff::tz::Offset` \ - and `jiff::civil::Time` don't support calendar \ - units in a `jiff::Span`)", - unit = unit.singular(), - ), - NotAllowedLargestSmallerThanSmallest { smallest, largest } => { - write!( - f, - "largest unit ('{largest}') cannot be smaller than \ - smallest unit ('{smallest}')", - largest = largest.singular(), - smallest = smallest.singular(), - ) - } - RequiresRelativeWeekOrDay { unit } => write!( - f, - "using unit '{unit}' in a span or configuration \ - requires that either a relative reference time be given \ - or `jiff::SpanRelativeTo::days_are_24_hours()` is used to \ - indicate invariant 24-hour days, \ - but neither were provided", - unit = unit.singular(), - ), - RequiresRelativeYearOrMonth { unit } => write!( - f, - "using unit '{unit}' in a span or configuration \ - requires that a relative reference time be given, \ - but none was provided", - unit = unit.singular(), - ), - RequiresRelativeYearOrMonthGivenDaysAre24Hours { unit } => write!( - f, - "using unit '{unit}' in span or configuration \ - requires that a relative reference time be given \ - (`jiff::SpanRelativeTo::days_are_24_hours()` was given \ - but this only permits using days and weeks \ - without a relative reference time)", - unit = unit.singular(), - ), - ToDurationCivil => f.write_str( - "could not compute normalized relative span \ - from civil datetime", - ), - ToDurationDaysAre24Hours => f.write_str( - "could not compute normalized relative span \ - when all days are assumed to be 24 hours", - ), - ToDurationZoned => f.write_str( - "could not compute normalized relative span \ - from zoned datetime", - ), - } - } -} diff --git a/src/error/timestamp.rs b/src/error/timestamp.rs deleted file mode 100644 index bdd2591..0000000 --- a/src/error/timestamp.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::error; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - OverflowAddDuration, - OverflowAddSpan, - RequiresSaturatingTimeUnits, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::Timestamp(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - OverflowAddDuration => { - f.write_str("adding duration overflowed timestamp") - } - OverflowAddSpan => f.write_str("adding span overflowed timestamp"), - RequiresSaturatingTimeUnits => f.write_str( - "saturating timestamp arithmetic requires only time units", - ), - } - } -} diff --git a/src/error/tz/ambiguous.rs b/src/error/tz/ambiguous.rs deleted file mode 100644 index 3346034..0000000 --- a/src/error/tz/ambiguous.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::{ - error, - tz::{Offset, TimeZone}, -}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - BecauseFold { before: Offset, after: Offset }, - BecauseGap { before: Offset, after: Offset }, - InTimeZone { tz: TimeZone }, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzAmbiguous(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - BecauseFold { before, after } => write!( - f, - "datetime is ambiguous since it falls into a \ - fold between offsets {before} and {after}", - ), - BecauseGap { before, after } => write!( - f, - "datetime is ambiguous since it falls into a \ - gap between offsets {before} and {after}", - ), - InTimeZone { ref tz } => write!( - f, - "error converting datetime to instant in time zone {tz}", - tz = tz.diagnostic_name(), - ), - } - } -} diff --git a/src/error/tz/concatenated.rs b/src/error/tz/concatenated.rs deleted file mode 100644 index 72c5d29..0000000 --- a/src/error/tz/concatenated.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::error; - -// At time of writing, the biggest TZif data file is a few KB. And the -// index block is tens of KB. So impose a limit that is a couple of orders -// of magnitude bigger, but still overall pretty small for... some systems. -// Anyway, I welcome improvements to this heuristic! -pub(crate) const ALLOC_LIMIT: usize = 10 * 1 << 20; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - AllocRequestOverLimit, - AllocFailed, - AllocOverflow, - ExpectedFirstSixBytes, - ExpectedIanaName, - ExpectedLastByte, - #[cfg(test)] - ExpectedMoreData, - ExpectedVersion, - FailedReadData, - FailedReadHeader, - FailedReadIndex, - #[cfg(all(feature = "std", all(not(unix), not(windows))))] - FailedSeek, - InvalidIndexDataOffsets, - InvalidLengthIndexBlock, - #[cfg(all(feature = "std", windows))] - InvalidOffsetOverflowFile, - #[cfg(test)] - InvalidOffsetOverflowSlice, - #[cfg(test)] - InvalidOffsetTooBig, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzConcatenated(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - AllocRequestOverLimit => write!( - f, - "attempted to allocate more than {ALLOC_LIMIT} bytes \ - while reading concatenated TZif data, which \ - exceeds a heuristic limit to prevent huge allocations \ - (please file a bug if this error is inappropriate)", - ), - AllocFailed => f.write_str( - "failed to allocate additional room \ - for reading concatenated TZif data", - ), - AllocOverflow => { - f.write_str("total allocation length overflowed `usize`") - } - ExpectedFirstSixBytes => f.write_str( - "expected first 6 bytes of concatenated TZif header \ - to be `tzdata`", - ), - ExpectedIanaName => f.write_str( - "expected IANA time zone identifier to be valid UTF-8", - ), - ExpectedLastByte => f.write_str( - "expected last byte of concatenated TZif header \ - to be `NUL`", - ), - #[cfg(test)] - ExpectedMoreData => f.write_str( - "unexpected EOF, expected more bytes based on size \ - of caller provided buffer", - ), - ExpectedVersion => f.write_str( - "expected version in concatenated TZif header to \ - be valid UTF-8", - ), - FailedReadData => f.write_str("failed to read TZif data block"), - FailedReadHeader => { - f.write_str("failed to read concatenated TZif header") - } - FailedReadIndex => f.write_str("failed to read index block"), - #[cfg(all(feature = "std", all(not(unix), not(windows))))] - FailedSeek => { - f.write_str("failed to seek to offset in `std::fs::File`") - } - InvalidIndexDataOffsets => f.write_str( - "invalid index and data offsets, \ - expected index offset to be less than or equal \ - to data offset", - ), - InvalidLengthIndexBlock => { - f.write_str("length of index block is not a valid multiple") - } - #[cfg(all(feature = "std", windows))] - InvalidOffsetOverflowFile => f.write_str( - "offset overflow when reading from `std::fs::File`", - ), - #[cfg(test)] - InvalidOffsetOverflowSlice => { - f.write_str("offset overflowed `usize`") - } - #[cfg(test)] - InvalidOffsetTooBig => { - f.write_str("offset too big for given slice of data") - } - } - } -} diff --git a/src/error/tz/db.rs b/src/error/tz/db.rs deleted file mode 100644 index 70349c9..0000000 --- a/src/error/tz/db.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::error; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - #[cfg(feature = "tzdb-concatenated")] - ConcatenatedMissingIanaIdentifiers, - #[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))] - DisabledConcatenated, - #[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))] - DisabledZoneInfo, - FailedTimeZone { - #[cfg(feature = "alloc")] - name: alloc::boxed::Box, - }, - FailedTimeZoneNoDatabaseConfigured { - #[cfg(feature = "alloc")] - name: alloc::boxed::Box, - }, - #[cfg(feature = "tzdb-zoneinfo")] - ZoneInfoNoTzifFiles, - #[cfg(feature = "tzdb-zoneinfo")] - ZoneInfoStripPrefix, -} - -impl Error { - pub(crate) fn failed_time_zone(_time_zone_name: &str) -> Error { - Error::FailedTimeZone { - #[cfg(feature = "alloc")] - name: _time_zone_name.into(), - } - } - - pub(crate) fn failed_time_zone_no_database_configured( - _time_zone_name: &str, - ) -> Error { - Error::FailedTimeZoneNoDatabaseConfigured { - #[cfg(feature = "alloc")] - name: _time_zone_name.into(), - } - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzDb(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - #[cfg(feature = "tzdb-concatenated")] - ConcatenatedMissingIanaIdentifiers => f.write_str( - "found no IANA time zone identifiers in \ - concatenated tzdata file", - ), - #[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))] - DisabledConcatenated => { - f.write_str("system concatenated tzdb unavailable") - } - #[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))] - DisabledZoneInfo => { - f.write_str("system zoneinfo tzdb unavailable") - } - FailedTimeZone { - #[cfg(feature = "alloc")] - ref name, - } => { - #[cfg(feature = "alloc")] - { - write!( - f, - "failed to find time zone `{name}` \ - in time zone database", - ) - } - #[cfg(not(feature = "alloc"))] - { - f.write_str( - "failed to find time zone in time zone database", - ) - } - } - FailedTimeZoneNoDatabaseConfigured { - #[cfg(feature = "alloc")] - ref name, - } => { - #[cfg(feature = "std")] - { - write!( - f, - "failed to find time zone `{name}` since there is no \ - time zone database configured", - ) - } - #[cfg(all(not(feature = "std"), feature = "alloc"))] - { - write!( - f, - "failed to find time zone `{name}`, since there is no \ - global time zone database configured (and is \ - currently impossible to do so without Jiff's `std` \ - feature enabled, if you need this functionality, \ - please file an issue on Jiff's tracker with your \ - use case)", - ) - } - #[cfg(all(not(feature = "std"), not(feature = "alloc")))] - { - f.write_str( - "failed to find time zone, since there is no \ - global time zone database configured (and is \ - currently impossible to do so without Jiff's `std` \ - feature enabled, if you need this functionality, \ - please file an issue on Jiff's tracker with your \ - use case)", - ) - } - } - #[cfg(feature = "tzdb-zoneinfo")] - ZoneInfoNoTzifFiles => f.write_str( - "did not find any TZif files in zoneinfo time zone database", - ), - #[cfg(feature = "tzdb-zoneinfo")] - ZoneInfoStripPrefix => f.write_str( - "failed to strip zoneinfo time zone database directory \ - path from path to TZif file", - ), - } - } -} diff --git a/src/error/tz/mod.rs b/src/error/tz/mod.rs deleted file mode 100644 index 4c5991b..0000000 --- a/src/error/tz/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub(crate) mod ambiguous; -pub(crate) mod concatenated; -pub(crate) mod db; -pub(crate) mod offset; -pub(crate) mod posix; -pub(crate) mod system; -pub(crate) mod timezone; -pub(crate) mod zic; diff --git a/src/error/tz/offset.rs b/src/error/tz/offset.rs deleted file mode 100644 index 4ced302..0000000 --- a/src/error/tz/offset.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::{ - error, - tz::{Offset, TimeZone}, - Unit, -}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ConvertDateTimeToTimestamp { - offset: Offset, - }, - OverflowAddSignedDuration, - OverflowSignedDuration, - ResolveRejectFold { - given: Offset, - before: Offset, - after: Offset, - tz: TimeZone, - }, - ResolveRejectGap { - given: Offset, - before: Offset, - after: Offset, - tz: TimeZone, - }, - ResolveRejectUnambiguous { - given: Offset, - offset: Offset, - tz: TimeZone, - }, - RoundInvalidUnit { - unit: Unit, - }, - RoundOverflow, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzOffset(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ConvertDateTimeToTimestamp { offset } => write!( - f, - "converting datetime with time zone offset `{offset}` \ - to timestamp overflowed", - ), - OverflowAddSignedDuration => f.write_str( - "adding signed duration to time zone offset overflowed", - ), - OverflowSignedDuration => { - f.write_str("signed duration overflows time zone offset") - } - ResolveRejectFold { given, before, after, ref tz } => write!( - f, - "datetime could not resolve to timestamp \ - since `reject` conflict resolution was chosen, and \ - because datetime has offset `{given}`, but the time \ - zone `{tzname}` for the given datetime falls in a fold \ - between offsets `{before}` and `{after}`, neither of which \ - match the offset", - tzname = tz.diagnostic_name(), - ), - ResolveRejectGap { given, before, after, ref tz } => write!( - f, - "datetime could not resolve to timestamp \ - since `reject` conflict resolution was chosen, and \ - because datetime has offset `{given}`, but the time \ - zone `{tzname}` for the given datetime falls in a gap \ - (between offsets `{before}` and `{after}`), and all \ - offsets for a gap are regarded as invalid", - tzname = tz.diagnostic_name(), - ), - ResolveRejectUnambiguous { given, offset, ref tz } => write!( - f, - "datetime could not resolve to a timestamp since \ - `reject` conflict resolution was chosen, and because \ - datetime has offset `{given}`, but the time \ - zone `{tzname}` for the given datetime \ - unambiguously has offset `{offset}`", - tzname = tz.diagnostic_name(), - ), - RoundInvalidUnit { unit } => write!( - f, - "rounding time zone offset failed because \ - a unit of {unit} was provided, \ - but time zone offset rounding \ - can only use hours, minutes or seconds", - unit = unit.plural(), - ), - RoundOverflow => f.write_str( - "rounding time zone offset resulted in a duration \ - that overflows", - ), - } - } -} diff --git a/src/error/tz/posix.rs b/src/error/tz/posix.rs deleted file mode 100644 index d9f10f2..0000000 --- a/src/error/tz/posix.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::error; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ColonPrefixInvalidUtf8, - InvalidPosixTz, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzPosix(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ColonPrefixInvalidUtf8 => f.write_str( - "POSIX time zone string with a `:` prefix \ - contains invalid UTF-8", - ), - InvalidPosixTz => f.write_str("invalid POSIX time zone string"), - } - } -} diff --git a/src/error/tz/system.rs b/src/error/tz/system.rs deleted file mode 100644 index 23b6471..0000000 --- a/src/error/tz/system.rs +++ /dev/null @@ -1,104 +0,0 @@ -#[cfg(not(feature = "tz-system"))] -pub(crate) use self::disabled::*; -#[cfg(feature = "tz-system")] -pub(crate) use self::enabled::*; - -#[cfg(not(feature = "tz-system"))] -mod disabled { - #[derive(Clone, Debug)] - pub(crate) enum Error {} - - impl core::fmt::Display for Error { - fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result { - unreachable!() - } - } -} - -#[cfg(feature = "tz-system")] -mod enabled { - use crate::error; - - #[derive(Clone, Debug)] - pub(crate) enum Error { - FailedEnvTz, - FailedEnvTzAsTzif, - FailedPosixTzAndUtf8, - FailedSystemTimeZone, - FailedUnnamedTzifInvalid, - FailedUnnamedTzifRead, - #[cfg(windows)] - WindowsMissingIanaMapping, - #[cfg(windows)] - WindowsTimeZoneKeyName, - #[cfg(windows)] - WindowsUtf16DecodeInvalid, - #[cfg(windows)] - WindowsUtf16DecodeNul, - } - - impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzSystem(err).into() - } - } - - impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - FailedEnvTz => f.write_str( - "`TZ` environment variable set, but failed to read value", - ), - FailedEnvTzAsTzif => f.write_str( - "failed to read `TZ` environment variable value \ - as a TZif file after attempting (and failing) a tzdb \ - lookup for that same value", - ), - FailedPosixTzAndUtf8 => f.write_str( - "failed to parse `TZ` environment variable as either \ - a POSIX time zone transition string or as valid UTF-8", - ), - FailedSystemTimeZone => { - f.write_str("failed to find system time zone") - } - FailedUnnamedTzifInvalid => f.write_str( - "found invalid TZif data in unnamed time zone file", - ), - FailedUnnamedTzifRead => f.write_str( - "failed to read TZif data from unnamed time zone file", - ), - #[cfg(windows)] - WindowsMissingIanaMapping => f.write_str( - "found Windows time zone name, \ - but could not find a mapping for it to an \ - IANA time zone name", - ), - #[cfg(windows)] - WindowsTimeZoneKeyName => f.write_str( - "could not get `TimeZoneKeyName` from \ - winapi `DYNAMIC_TIME_ZONE_INFORMATION`", - ), - #[cfg(windows)] - WindowsUtf16DecodeInvalid => f.write_str( - "failed to convert `u16` slice to UTF-8 \ - (invalid UTF-16)", - ), - #[cfg(windows)] - WindowsUtf16DecodeNul => f.write_str( - "failed to convert `u16` slice to UTF-8 \ - (no NUL terminator found)", - ), - } - } - } -} diff --git a/src/error/tz/timezone.rs b/src/error/tz/timezone.rs deleted file mode 100644 index f3d66b1..0000000 --- a/src/error/tz/timezone.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::error; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - ConvertNonFixed { - kind: &'static str, - }, - #[cfg(not(feature = "tz-system"))] - FailedSystem, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzTimeZone(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - ConvertNonFixed { kind } => write!( - f, - "cannot convert non-fixed {kind} time zone to offset \ - without a timestamp or civil datetime", - ), - #[cfg(not(feature = "tz-system"))] - FailedSystem => f.write_str("failed to get system time zone"), - } - } -} diff --git a/src/error/tz/zic.rs b/src/error/tz/zic.rs deleted file mode 100644 index 7273f70..0000000 --- a/src/error/tz/zic.rs +++ /dev/null @@ -1,323 +0,0 @@ -#[cfg(not(test))] -pub(crate) use self::disabled::*; -#[cfg(test)] -pub(crate) use self::enabled::*; - -#[cfg(not(test))] -mod disabled { - #[derive(Clone, Debug)] - pub(crate) enum Error {} - - impl core::fmt::Display for Error { - fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result { - unreachable!() - } - } -} - -#[cfg(test)] -mod enabled { - use alloc::boxed::Box; - - use crate::error; - - // `man zic` says that the max line length including the line - // terminator is 2048. The `core::str::Lines` iterator doesn't include - // the terminator, so we subtract 1 to account for that. Note that this - // could potentially allow one extra byte in the case of a \r\n line - // terminator, but this seems fine. - pub(crate) const MAX_LINE_LEN: usize = 2047; - - #[derive(Clone, Debug)] - pub(crate) enum Error { - DuplicateLink { name: Box }, - DuplicateLinkZone { name: Box }, - DuplicateZone { name: Box }, - DuplicateZoneLink { name: Box }, - ExpectedCloseQuote, - ExpectedColonAfterHour, - ExpectedColonAfterMinute, - ExpectedContinuationZoneThreeFields, - ExpectedContinuationZoneLine { name: Box }, - ExpectedDotAfterSeconds, - ExpectedFirstZoneFourFields, - ExpectedLinkTwoFields, - ExpectedMinuteAfterHours, - ExpectedNameBegin, - ExpectedNanosecondDigits, - ExpectedNonEmptyAbbreviation, - ExpectedNonEmptyAt, - ExpectedNonEmptyName, - ExpectedNonEmptySave, - ExpectedNonEmptyZoneName, - ExpectedNothingAfterTime, - ExpectedRuleNineFields { got: usize }, - ExpectedSecondAfterMinutes, - ExpectedTimeOneHour, - ExpectedUntilYear, - ExpectedWhitespaceAfterQuotedField, - ExpectedZoneNameComponentNoDots { component: Box }, - FailedContinuationZone, - FailedLinkLine, - FailedParseDay, - FailedParseFieldAt, - FailedParseFieldFormat, - FailedParseFieldFrom, - FailedParseFieldIn, - FailedParseFieldLetters, - FailedParseFieldLinkName, - FailedParseFieldLinkTarget, - FailedParseFieldName, - FailedParseFieldOn, - FailedParseFieldRules, - FailedParseFieldSave, - FailedParseFieldStdOff, - FailedParseFieldTo, - FailedParseFieldUntil, - FailedParseHour, - FailedParseMinute, - FailedParseMonth, - FailedParseNanosecond, - FailedParseSecond, - FailedParseTimeDuration, - FailedParseYear, - FailedRule { name: Box }, - FailedRuleLine, - FailedZoneFirst, - Line { number: usize }, - LineMaxLength, - LineNul, - LineOverflow, - InvalidAbbreviation, - InvalidRuleYear { start: i16, end: i16 }, - InvalidUtf8, - UnrecognizedAtTimeSuffix, - UnrecognizedDayOfMonthFormat, - UnrecognizedDayOfWeek, - UnrecognizedMonthName, - UnrecognizedSaveTimeSuffix, - UnrecognizedTrailingTimeDuration, - UnrecognizedZicLine, - } - - impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::TzZic(err).into() - } - } - - impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } - } - - impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - DuplicateLink { ref name } => { - write!(f, "found duplicate link with name `{name}`") - } - DuplicateLinkZone { ref name } => write!( - f, - "found link with name `{name}` that conflicts \ - with a zone of the same name", - ), - DuplicateZone { ref name } => { - write!(f, "found duplicate zone with name `{name}`") - } - DuplicateZoneLink { ref name } => write!( - f, - "found zone with name `{name}` that conflicts \ - with a link of the same name", - ), - ExpectedCloseQuote => { - f.write_str("found unclosed quote for field") - } - ExpectedColonAfterHour => { - f.write_str("expected `:` after hours") - } - ExpectedColonAfterMinute => { - f.write_str("expected `:` after minutes") - } - ExpectedContinuationZoneLine { ref name } => write!( - f, - "expected continuation zone line for `{name}`, \ - but found end of data instead", - ), - ExpectedContinuationZoneThreeFields => f.write_str( - "expected continuation `ZONE` line \ - to have at least 3 fields", - ), - ExpectedDotAfterSeconds => { - f.write_str("expected `.` after seconds") - } - ExpectedFirstZoneFourFields => f.write_str( - "expected first `ZONE` line to have at least 4 fields", - ), - ExpectedLinkTwoFields => { - f.write_str("expected exactly 2 fields after `LINK`") - } - ExpectedMinuteAfterHours => { - f.write_str("expected minute digits after `HH:`") - } - ExpectedNameBegin => f.write_str( - "`NAME` field cannot begin with a digit, `+` or `-`, \ - but found `NAME` that begins with one of those", - ), - ExpectedNanosecondDigits => { - f.write_str("expected nanosecond digits after `HH:MM:SS.`") - } - ExpectedNonEmptyAbbreviation => f.write_str( - "empty time zone abbreviations are not allowed", - ), - ExpectedNonEmptyAt => { - f.write_str("`AT` field for rule cannot be empty") - } - ExpectedNonEmptyName => { - f.write_str("`NAME` field for rule cannot be empty") - } - ExpectedNonEmptySave => { - f.write_str("`SAVE` field for rule cannot be empty") - } - ExpectedNonEmptyZoneName => { - f.write_str("zone names cannot be empty") - } - ExpectedNothingAfterTime => f.write_str( - "expected no more fields after time of day, \ - but found at least one", - ), - ExpectedRuleNineFields { got } => write!( - f, - "expected exactly 9 fields for rule, \ - but found {got} fields", - ), - ExpectedSecondAfterMinutes => { - f.write_str("expected second digits after `HH:MM:`") - } - ExpectedTimeOneHour => f.write_str( - "expected time duration to contain \ - at least one hour digit", - ), - ExpectedUntilYear => f.write_str("expected at least a year"), - ExpectedWhitespaceAfterQuotedField => { - f.write_str("expected whitespace after quoted field") - } - ExpectedZoneNameComponentNoDots { ref component } => write!( - f, - "component `{component}` in zone name cannot \ - be \".\" or \"..\"", - ), - FailedContinuationZone => { - f.write_str("failed to parse continuation `Zone` line") - } - FailedLinkLine => f.write_str("failed to parse `Link` line"), - FailedParseDay => f.write_str("failed to parse day"), - FailedParseFieldAt => { - f.write_str("failed to parse `NAME` field") - } - FailedParseFieldFormat => { - f.write_str("failed to parse `FORMAT` field") - } - FailedParseFieldFrom => { - f.write_str("failed to parse `FROM` field") - } - FailedParseFieldIn => { - f.write_str("failed to parse `IN` field") - } - FailedParseFieldLetters => { - f.write_str("failed to parse `LETTERS` field") - } - FailedParseFieldLinkName => { - f.write_str("failed to parse `LINK` name field") - } - FailedParseFieldLinkTarget => { - f.write_str("failed to parse `LINK` target field") - } - FailedParseFieldName => { - f.write_str("failed to parse `NAME` field") - } - FailedParseFieldOn => { - f.write_str("failed to parse `ON` field") - } - FailedParseFieldRules => { - f.write_str("failed to parse `RULES` field") - } - FailedParseFieldSave => { - f.write_str("failed to parse `SAVE` field") - } - FailedParseFieldStdOff => { - f.write_str("failed to parse `STDOFF` field") - } - FailedParseFieldTo => { - f.write_str("failed to parse `TO` field") - } - FailedParseFieldUntil => { - f.write_str("failed to parse `UNTIL` field") - } - FailedParseHour => f.write_str("failed to parse hour"), - FailedParseMinute => f.write_str("failed to parse minute"), - FailedParseMonth => f.write_str("failed to parse month"), - FailedParseNanosecond => { - f.write_str("failed to parse nanosecond") - } - FailedParseSecond => f.write_str("failed to parse second"), - FailedParseTimeDuration => { - f.write_str("failed to parse time duration") - } - FailedParseYear => f.write_str("failed to parse year"), - FailedRule { name: ref rule } => { - write!(f, "failed to parse rule `{rule}`") - } - FailedRuleLine => f.write_str("failed to parse `Rule` line"), - FailedZoneFirst => { - f.write_str("failed to parse first `Zone` line") - } - InvalidAbbreviation => f.write_str( - "time zone abbreviation \ - contains invalid character; only \"+\", \"-\" and \ - ASCII alpha-numeric characters are allowed", - ), - InvalidRuleYear { start, end } => write!( - f, - "found start year={start} \ - to be greater than end year={end}" - ), - InvalidUtf8 => f.write_str("invalid UTF-8"), - Line { number } => write!(f, "line {number}"), - LineMaxLength => write!( - f, - "found line with length that exceeds \ - max length of {MAX_LINE_LEN}", - ), - LineNul => f.write_str( - "found line with NUL byte, which isn't allowed", - ), - LineOverflow => f.write_str("line count overflowed"), - UnrecognizedAtTimeSuffix => { - f.write_str("unrecognized `AT` time suffix") - } - UnrecognizedDayOfMonthFormat => { - f.write_str("unrecognized format for day-of-month") - } - UnrecognizedDayOfWeek => { - f.write_str("unrecognized day of the week") - } - UnrecognizedMonthName => { - f.write_str("unrecognized month name") - } - UnrecognizedSaveTimeSuffix => { - f.write_str("unrecognized `SAVE` time suffix") - } - UnrecognizedTrailingTimeDuration => { - f.write_str("found unrecognized suffix in time duration") - } - UnrecognizedZicLine => f.write_str("unrecognized zic line"), - } - } - } -} diff --git a/src/error/util.rs b/src/error/util.rs deleted file mode 100644 index 8009936..0000000 --- a/src/error/util.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::{error, util::escape::Byte, Unit}; - -#[derive(Clone, Debug)] -pub(crate) enum RoundingIncrementError { - ForDateTime, - ForSpan, - ForTime, - ForTimestamp, - GreaterThanZero { unit: Unit }, - InvalidDivide { unit: Unit, must_divide: i64 }, - Unsupported { unit: Unit }, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: RoundingIncrementError) -> error::Error { - error::ErrorKind::RoundingIncrement(err).into() - } -} - -impl error::IntoError for RoundingIncrementError { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for RoundingIncrementError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::RoundingIncrementError::*; - - match *self { - ForDateTime => f.write_str("failed rounding datetime"), - ForSpan => f.write_str("failed rounding span"), - ForTime => f.write_str("failed rounding time"), - ForTimestamp => f.write_str("failed rounding timestamp"), - GreaterThanZero { unit } => write!( - f, - "rounding increment for {unit} must be greater than zero", - unit = unit.plural(), - ), - InvalidDivide { unit, must_divide } => write!( - f, - "increment for rounding to {unit} \ - must be 1) less than {must_divide}, 2) divide into \ - it evenly and 3) greater than zero", - unit = unit.plural(), - ), - Unsupported { unit } => write!( - f, - "rounding to {unit} is not supported", - unit = unit.plural(), - ), - } - } -} - -#[derive(Clone, Debug)] -pub(crate) enum ParseIntError { - NoDigitsFound, - InvalidDigit(u8), - TooBig, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: ParseIntError) -> error::Error { - error::ErrorKind::ParseInt(err).into() - } -} - -impl error::IntoError for ParseIntError { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for ParseIntError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::ParseIntError::*; - - match *self { - NoDigitsFound => write!(f, "invalid number, no digits found"), - InvalidDigit(got) => { - write!(f, "invalid digit, expected 0-9 but got {}", Byte(got)) - } - TooBig => { - write!(f, "number too big to parse into 64-bit integer") - } - } - } -} - -#[derive(Clone, Debug)] -pub(crate) enum ParseFractionError { - NoDigitsFound, - TooManyDigits, - InvalidDigit(u8), - TooBig, -} - -impl ParseFractionError { - pub(crate) const MAX_PRECISION: usize = 9; -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: ParseFractionError) -> error::Error { - error::ErrorKind::ParseFraction(err).into() - } -} - -impl error::IntoError for ParseFractionError { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for ParseFractionError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::ParseFractionError::*; - - match *self { - NoDigitsFound => write!(f, "invalid fraction, no digits found"), - TooManyDigits => write!( - f, - "invalid fraction, too many digits \ - (at most {max} are allowed)", - max = ParseFractionError::MAX_PRECISION, - ), - InvalidDigit(got) => { - write!( - f, - "invalid fractional digit, expected 0-9 but got {}", - Byte(got) - ) - } - TooBig => { - write!( - f, - "fractional number too big to parse into 64-bit integer" - ) - } - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct OsStrUtf8Error { - #[cfg(feature = "std")] - value: alloc::boxed::Box, -} - -#[cfg(feature = "std")] -impl From<&std::ffi::OsStr> for OsStrUtf8Error { - #[cold] - #[inline(never)] - fn from(value: &std::ffi::OsStr) -> OsStrUtf8Error { - OsStrUtf8Error { value: value.into() } - } -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: OsStrUtf8Error) -> error::Error { - error::ErrorKind::OsStrUtf8(err).into() - } -} - -impl error::IntoError for OsStrUtf8Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for OsStrUtf8Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - #[cfg(feature = "std")] - { - write!( - f, - "environment value `{value:?}` is not valid UTF-8", - value = self.value - ) - } - #[cfg(not(feature = "std"))] - { - write!(f, "") - } - } -} diff --git a/src/error/zoned.rs b/src/error/zoned.rs deleted file mode 100644 index c106e6c..0000000 --- a/src/error/zoned.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::{error, Unit}; - -#[derive(Clone, Debug)] -pub(crate) enum Error { - AddDateTime, - AddDays, - AddTimestamp, - ConvertDateTimeToTimestamp, - ConvertIntermediateDatetime, - FailedLengthOfDay, - FailedSpanNanoseconds, - FailedStartOfDay, - MismatchTimeZoneUntil { largest: Unit }, -} - -impl From for error::Error { - #[cold] - #[inline(never)] - fn from(err: Error) -> error::Error { - error::ErrorKind::Zoned(err).into() - } -} - -impl error::IntoError for Error { - fn into_error(self) -> error::Error { - self.into() - } -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::Error::*; - - match *self { - AddDateTime => f.write_str( - "failed to add span to datetime from zoned datetime", - ), - AddDays => { - f.write_str("failed to add days to date in zoned datetime") - } - AddTimestamp => f.write_str( - "failed to add span to timestamp from zoned datetime", - ), - ConvertDateTimeToTimestamp => { - f.write_str("failed to convert civil datetime to timestamp") - } - ConvertIntermediateDatetime => f.write_str( - "failed to convert intermediate datetime \ - to zoned timestamp", - ), - FailedLengthOfDay => f.write_str( - "failed to add 1 day to zoned datetime to find length of day", - ), - FailedSpanNanoseconds => f.write_str( - "failed to compute span in nanoseconds \ - between zoned datetimes", - ), - FailedStartOfDay => { - f.write_str("failed to find start of day for zoned datetime") - } - MismatchTimeZoneUntil { largest } => write!( - f, - "computing the span between zoned datetimes, with \ - {largest} units, requires that the time zones are \ - equivalent, but the zoned datetimes have distinct \ - time zones", - largest = largest.singular(), - ), - } - } -} diff --git a/src/fmt/friendly/mod.rs b/src/fmt/friendly/mod.rs index 57b3eaa..cbdaf9b 100644 --- a/src/fmt/friendly/mod.rs +++ b/src/fmt/friendly/mod.rs @@ -256,9 +256,7 @@ assert_eq!(dur, Duration::new(30 * 24 * 60 * 60 + 38_016, 0)); // In contrast, Jiff will reject `1M`: assert_eq!( "1M".parse::().unwrap_err().to_string(), - "failed to parse input in the \"friendly\" duration format: \ - expected to find unit designator suffix \ - (e.g., `years` or `secs`) after parsing integer", + "failed to parse \"1M\" in the \"friendly\" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with \"M\" instead", ); # Ok::<(), Box>(()) @@ -337,9 +335,7 @@ assert_eq!( // Jiff is saving you from doing something wrong assert_eq!( "1 day".parse::().unwrap_err().to_string(), - "failed to parse input in the \"friendly\" duration format: \ - parsing calendar units (days in this case) in this context \ - is not supported (perhaps try parsing into a `jiff::Span` instead)", + "failed to parse \"1 day\" in the \"friendly\" format: parsing day units into a `SignedDuration` is not supported (perhaps try parsing into a `Span` instead)", ); ``` diff --git a/src/fmt/friendly/parser.rs b/src/fmt/friendly/parser.rs index af73977..9132c2d 100644 --- a/src/fmt/friendly/parser.rs +++ b/src/fmt/friendly/parser.rs @@ -1,11 +1,11 @@ use crate::{ - error::{fmt::friendly::Error as E, ErrorContext}, + error::{err, ErrorContext}, fmt::{ friendly::parser_label, util::{parse_temporal_fraction, DurationUnits}, Parsed, }, - util::{c::Sign, parse}, + util::{c::Sign, escape, parse}, Error, SignedDuration, Span, Unit, }; @@ -188,7 +188,12 @@ impl SpanParser { } let input = input.as_ref(); - imp(self, input).context(E::Failed) + imp(self, input).with_context(|| { + err!( + "failed to parse {input:?} in the \"friendly\" format", + input = escape::Bytes(input) + ) + }) } /// Run the parser on the given string (which may be plain bytes) and, @@ -243,7 +248,12 @@ impl SpanParser { } let input = input.as_ref(); - imp(self, input).context(E::Failed) + imp(self, input).with_context(|| { + err!( + "failed to parse {input:?} in the \"friendly\" format", + input = escape::Bytes(input) + ) + }) } /// Run the parser on the given string (which may be plain bytes) and, @@ -302,7 +312,12 @@ impl SpanParser { } let input = input.as_ref(); - imp(self, input).context(E::Failed) + imp(self, input).with_context(|| { + err!( + "failed to parse {input:?} in the \"friendly\" format", + input = escape::Bytes(input) + ) + }) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -312,7 +327,7 @@ impl SpanParser { builder: &mut DurationUnits, ) -> Result, Error> { if input.is_empty() { - return Err(Error::from(E::Empty)); + return Err(err!("an empty string is not a valid duration")); } // Guard prefix sign parsing to avoid the function call, which is // marked unlineable to keep the fast path tighter. @@ -327,7 +342,11 @@ impl SpanParser { let Parsed { value, input } = self.parse_unit_value(input)?; let Some(first_unit_value) = value else { - return Err(Error::from(E::ExpectedIntegerAfterSign)); + return Err(err!( + "parsing a friendly duration requires it to start \ + with a unit value (a decimal integer) after an \ + optional sign, but no integer was found", + )); }; let Parsed { input, .. } = @@ -415,7 +434,11 @@ impl SpanParser { parsed_any_after_comma = true; } if !parsed_any_after_comma { - return Err(Error::from(E::ExpectedOneMoreUnitAfterComma)); + return Err(err!( + "found comma at the end of duration, \ + but a comma indicates at least one more \ + unit follows", + )); } Ok(Parsed { value: (), input }) } @@ -431,13 +454,10 @@ impl SpanParser { input: &'i [u8], hour: u64, ) -> Result>, Error> { - let Some((&first, tail)) = input.split_first() else { - return Ok(Parsed { input, value: None }); - }; - if first != b':' { + if !input.first().map_or(false, |&b| b == b':') { return Ok(Parsed { input, value: None }); } - let Parsed { input, value } = self.parse_hms(tail, hour)?; + let Parsed { input, value } = self.parse_hms(&input[1..], hour)?; Ok(Parsed { input, value: Some(value) }) } @@ -457,16 +477,26 @@ impl SpanParser { hour: u64, ) -> Result, Error> { let Parsed { input, value } = self.parse_unit_value(input)?; - let minute = value.ok_or(E::ExpectedMinuteAfterHour)?; - - let (&first, input) = - input.split_first().ok_or(E::ExpectedColonAfterMinute)?; - if first != b':' { - return Err(Error::from(E::ExpectedColonAfterMinute)); + let Some(minute) = value else { + return Err(err!( + "expected to parse minute in 'HH:MM:SS' format \ + following parsed hour of {hour}", + )); + }; + if !input.first().map_or(false, |&b| b == b':') { + return Err(err!( + "when parsing 'HH:MM:SS' format, expected to \ + see a ':' after the parsed minute of {minute}", + )); } - + let input = &input[1..]; let Parsed { input, value } = self.parse_unit_value(input)?; - let second = value.ok_or(E::ExpectedSecondAfterMinute)?; + let Some(second) = value else { + return Err(err!( + "expected to parse second in 'HH:MM:SS' format \ + following parsed minute of {minute}", + )); + }; let (fraction, input) = if input.first().map_or(false, |&b| b == b'.' || b == b',') { let parsed = parse_temporal_fraction(input)?; @@ -510,8 +540,22 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - let (unit, len) = - parser_label::find(input).ok_or(E::ExpectedUnitSuffix)?; + let Some((unit, len)) = parser_label::find(input) else { + if input.is_empty() { + return Err(err!( + "expected to find unit designator suffix \ + (e.g., 'years' or 'secs'), \ + but found end of input", + )); + } else { + return Err(err!( + "expected to find unit designator suffix \ + (e.g., 'years' or 'secs'), \ + but found input beginning with {found:?} instead", + found = escape::Bytes(&input[..input.len().min(20)]), + )); + } + }; Ok(Parsed { value: unit, input: &input[len..] }) } @@ -562,15 +606,17 @@ impl SpanParser { } // Eat any additional whitespace we find before looking for 'ago'. input = self.parse_optional_whitespace(&input[1..]).input; - let (suffix_sign, input) = - if let Some(tail) = input.strip_prefix(b"ago") { - (Some(Sign::Negative), tail) - } else { - (None, input) - }; + let (suffix_sign, input) = if input.starts_with(b"ago") { + (Some(Sign::Negative), &input[3..]) + } else { + (None, input) + }; let sign = match (prefix_sign, suffix_sign) { (Some(_), Some(_)) => { - return Err(Error::from(E::ExpectedOneSign)); + return Err(err!( + "expected to find either a prefix sign (+/-) or \ + a suffix sign (ago), but found both", + )) } (Some(sign), None) => sign, (None, Some(sign)) => sign, @@ -591,24 +637,24 @@ impl SpanParser { #[inline(never)] fn parse_optional_comma<'i>( &self, - input: &'i [u8], + mut input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Ok(Parsed { value: (), input }); - }; - if first != b',' { + if !input.first().map_or(false, |&b| b == b',') { return Ok(Parsed { value: (), input }); } - - let (second, input) = tail - .split_first() - .ok_or(E::ExpectedWhitespaceAfterCommaEndOfInput)?; - if !is_whitespace(second) { - return Err(Error::from(E::ExpectedWhitespaceAfterComma { - byte: *second, - })); + input = &input[1..]; + if input.is_empty() { + return Err(err!( + "expected whitespace after comma, but found end of input" + )); } - Ok(Parsed { value: (), input }) + if !is_whitespace(&input[0]) { + return Err(err!( + "expected whitespace after comma, but found {found:?}", + found = escape::Byte(input[0]), + )); + } + Ok(Parsed { value: (), input: &input[1..] }) } /// Parses zero or more bytes of ASCII whitespace. @@ -730,35 +776,35 @@ mod tests { insta::assert_snapshot!( p(""), - @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#, + @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###, ); insta::assert_snapshot!( p(" "), - @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#, + @r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###, ); insta::assert_snapshot!( p("a"), - @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#, + @r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###, ); insta::assert_snapshot!( p("2 months 1 year"), - @r#"failed to parse input in the "friendly" duration format: found value with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"#, + @r###"failed to parse "2 months 1 year" in the "friendly" format: found value 1 with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"###, ); insta::assert_snapshot!( p("1 year 1 mont"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"#, + @r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###, ); insta::assert_snapshot!( p("2 months,"), - @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#, + @r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###, ); insta::assert_snapshot!( p("2 months, "), - @r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#, + @r#"failed to parse "2 months, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#, ); insta::assert_snapshot!( p("2 months ,"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"#, + @r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###, ); } @@ -768,19 +814,19 @@ mod tests { insta::assert_snapshot!( p("1yago"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"#, + @r###"failed to parse "1yago" in the "friendly" format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"###, ); insta::assert_snapshot!( p("1 year 1 monthago"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"#, + @r###"failed to parse "1 year 1 monthago" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"###, ); insta::assert_snapshot!( p("+1 year 1 month ago"), - @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#, + @r###"failed to parse "+1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, ); insta::assert_snapshot!( p("-1 year 1 month ago"), - @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#, + @r###"failed to parse "-1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, ); } @@ -794,7 +840,7 @@ mod tests { // the maximum number of microseconds is subtracted off, and we're // left over with a value that overflows an i64. pe("640330789636854776 micros"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for microsecond unit on span: failed to set nanosecond value from fractional component"#, + @r#"failed to parse "640330789636854776 micros" in the "friendly" format: failed to set value 640330789636854776 as microsecond unit on span: failed to set nanosecond value 9223372036854776000 (it overflows `i64`) on span determined from 640330789636854776.0"#, ); // one fewer is okay insta::assert_snapshot!( @@ -807,7 +853,7 @@ mod tests { // different error path by using an explicit fraction. Here, if // we had x.807 micros, it would parse successfully. pe("640330789636854775.808 micros"), - @r#"failed to parse input in the "friendly" duration format: failed to set nanosecond value from fractional component"#, + @r#"failed to parse "640330789636854775.808 micros" in the "friendly" format: failed to set nanosecond value 9223372036854775808 (it overflows `i64`) on span determined from 640330789636854775.808000000"#, ); // one fewer is okay insta::assert_snapshot!( @@ -822,47 +868,47 @@ mod tests { insta::assert_snapshot!( p("19999 years"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"#, + @r###"failed to parse "19999 years" in the "friendly" format: failed to set value 19999 as year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"###, ); insta::assert_snapshot!( p("19999 years ago"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#, + @r#"failed to parse "19999 years ago" in the "friendly" format: failed to set value -19999 as year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#, ); insta::assert_snapshot!( p("239977 months"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"#, + @r###"failed to parse "239977 months" in the "friendly" format: failed to set value 239977 as month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"###, ); insta::assert_snapshot!( p("239977 months ago"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#, + @r#"failed to parse "239977 months ago" in the "friendly" format: failed to set value -239977 as month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#, ); insta::assert_snapshot!( p("1043498 weeks"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"#, + @r###"failed to parse "1043498 weeks" in the "friendly" format: failed to set value 1043498 as week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"###, ); insta::assert_snapshot!( p("1043498 weeks ago"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#, + @r#"failed to parse "1043498 weeks ago" in the "friendly" format: failed to set value -1043498 as week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#, ); insta::assert_snapshot!( p("7304485 days"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"#, + @r###"failed to parse "7304485 days" in the "friendly" format: failed to set value 7304485 as day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"###, ); insta::assert_snapshot!( p("7304485 days ago"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#, + @r#"failed to parse "7304485 days ago" in the "friendly" format: failed to set value -7304485 as day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#, ); insta::assert_snapshot!( p("9223372036854775808 nanoseconds"), - @r#"failed to parse input in the "friendly" duration format: value for nanoseconds is too big (or small) to fit into a signed 64-bit integer"#, + @r#"failed to parse "9223372036854775808 nanoseconds" in the "friendly" format: `9223372036854775808` nanoseconds is too big (or small) to fit into a signed 64-bit integer"#, ); insta::assert_snapshot!( p("9223372036854775808 nanoseconds ago"), - @r#"failed to parse input in the "friendly" duration format: failed to set value for nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#, + @r#"failed to parse "9223372036854775808 nanoseconds ago" in the "friendly" format: failed to set value -9223372036854775808 as nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#, ); } @@ -872,11 +918,11 @@ mod tests { insta::assert_snapshot!( p("1.5 years"), - @r#"failed to parse input in the "friendly" duration format: fractional years are not supported"#, + @r#"failed to parse "1.5 years" in the "friendly" format: fractional years are not supported"#, ); insta::assert_snapshot!( p("1.5 nanos"), - @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#, + @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#, ); } @@ -886,19 +932,19 @@ mod tests { insta::assert_snapshot!( p("05:"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#, + @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###, ); insta::assert_snapshot!( p("05:06"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#, + @r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###, ); insta::assert_snapshot!( p("05:06:"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#, + @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###, ); insta::assert_snapshot!( p("2 hours, 05:06:07"), - @r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#, + @r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#, ); } @@ -922,7 +968,7 @@ mod tests { ); insta::assert_snapshot!( perr("9223372036854775808s"), - @r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#, + @r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#, ); insta::assert_snapshot!( p("-9223372036854775808s"), @@ -986,21 +1032,21 @@ mod tests { insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H"); insta::assert_snapshot!( pe("2562047788015216hrs"), - @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#, + @r#"failed to parse "2562047788015216hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 2562047788015216 of unit hour"#, ); insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M"); insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M"); insta::assert_snapshot!( pe("153722867280912931mins"), - @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#, + @r#"failed to parse "153722867280912931mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 153722867280912931 of unit minute"#, ); insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S"); insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S"); insta::assert_snapshot!( pe("9223372036854775808s"), - @r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#, + @r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#, ); insta::assert_snapshot!( p("-9223372036854775808s"), @@ -1014,39 +1060,39 @@ mod tests { insta::assert_snapshot!( p(""), - @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#, + @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###, ); insta::assert_snapshot!( p(" "), - @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#, + @r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###, ); insta::assert_snapshot!( p("5"), - @r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#, + @r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###, ); insta::assert_snapshot!( p("a"), - @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#, + @r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###, ); insta::assert_snapshot!( p("2 minutes 1 hour"), - @r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#, + @r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###, ); insta::assert_snapshot!( p("1 hour 1 minut"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"#, + @r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###, ); insta::assert_snapshot!( p("2 minutes,"), - @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#, + @r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###, ); insta::assert_snapshot!( p("2 minutes, "), - @r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#, + @r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#, ); insta::assert_snapshot!( p("2 minutes ,"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"#, + @r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###, ); } @@ -1056,19 +1102,19 @@ mod tests { insta::assert_snapshot!( p("1hago"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"#, + @r###"failed to parse "1hago" in the "friendly" format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"###, ); insta::assert_snapshot!( p("1 hour 1 minuteago"), - @r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"#, + @r###"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"###, ); insta::assert_snapshot!( p("+1 hour 1 minute ago"), - @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#, + @r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, ); insta::assert_snapshot!( p("-1 hour 1 minute ago"), - @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#, + @r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, ); } @@ -1081,7 +1127,7 @@ mod tests { // Unlike `Span`, this just overflows because it can't be parsed // as a 64-bit integer. pe("9223372036854775808 micros"), - @r#"failed to parse input in the "friendly" duration format: value for microseconds is too big (or small) to fit into a signed 64-bit integer"#, + @r#"failed to parse "9223372036854775808 micros" in the "friendly" format: `9223372036854775808` microseconds is too big (or small) to fit into a signed 64-bit integer"#, ); // one fewer is okay insta::assert_snapshot!( @@ -1096,7 +1142,7 @@ mod tests { insta::assert_snapshot!( p("1.5 nanos"), - @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#, + @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#, ); } @@ -1106,19 +1152,19 @@ mod tests { insta::assert_snapshot!( p("05:"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#, + @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###, ); insta::assert_snapshot!( p("05:06"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#, + @r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###, ); insta::assert_snapshot!( p("05:06:"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#, + @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###, ); insta::assert_snapshot!( p("2 hours, 05:06:07"), - @r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#, + @r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#, ); } @@ -1159,11 +1205,11 @@ mod tests { ); insta::assert_snapshot!( perr("18446744073709551616s"), - @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#, + @r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#, ); insta::assert_snapshot!( perr("-1s"), - @r#"failed to parse input in the "friendly" duration format: cannot parse negative duration into unsigned `std::time::Duration`"#, + @r#"failed to parse "-1s" in the "friendly" format: cannot parse negative duration into unsigned `std::time::Duration`"#, ); } @@ -1217,19 +1263,19 @@ mod tests { insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H"); insta::assert_snapshot!( pe("5124095576030432hrs"), - @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#, + @r#"failed to parse "5124095576030432hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 5124095576030432 of unit hour"#, ); insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H"); insta::assert_snapshot!( pe("307445734561825861mins"), - @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#, + @r#"failed to parse "307445734561825861mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 307445734561825861 of unit minute"#, ); insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S"); insta::assert_snapshot!( pe("18446744073709551616s"), - @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#, + @r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#, ); } @@ -1241,39 +1287,39 @@ mod tests { insta::assert_snapshot!( p(""), - @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#, + @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###, ); insta::assert_snapshot!( p(" "), - @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#, + @r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###, ); insta::assert_snapshot!( p("5"), - @r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#, + @r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###, ); insta::assert_snapshot!( p("a"), - @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#, + @r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###, ); insta::assert_snapshot!( p("2 minutes 1 hour"), - @r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#, + @r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###, ); insta::assert_snapshot!( p("1 hour 1 minut"), - @r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#, + @r#"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#, ); insta::assert_snapshot!( p("2 minutes,"), - @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#, + @r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###, ); insta::assert_snapshot!( p("2 minutes, "), - @r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#, + @r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#, ); insta::assert_snapshot!( p("2 minutes ,"), - @r#"failed to parse input in the "friendly" duration format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#, + @r#"failed to parse "2 minutes ," in the "friendly" format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#, ); } @@ -1285,19 +1331,19 @@ mod tests { insta::assert_snapshot!( p("1hago"), - @r#"failed to parse input in the "friendly" duration format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#, + @r#"failed to parse "1hago" in the "friendly" format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#, ); insta::assert_snapshot!( p("1 hour 1 minuteago"), - @r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#, + @r#"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#, ); insta::assert_snapshot!( p("+1 hour 1 minute ago"), - @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#, + @r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, ); insta::assert_snapshot!( p("-1 hour 1 minute ago"), - @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#, + @r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, ); } @@ -1316,7 +1362,7 @@ mod tests { // Unlike `Span`, this just overflows because it can't be parsed // as a 64-bit integer. pe("18446744073709551616 micros"), - @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#, + @r#"failed to parse "18446744073709551616 micros" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#, ); // one fewer is okay insta::assert_snapshot!( @@ -1333,7 +1379,7 @@ mod tests { insta::assert_snapshot!( p("1.5 nanos"), - @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#, + @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#, ); } @@ -1345,19 +1391,19 @@ mod tests { insta::assert_snapshot!( p("05:"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#, + @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###, ); insta::assert_snapshot!( p("05:06"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#, + @r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###, ); insta::assert_snapshot!( p("05:06:"), - @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#, + @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###, ); insta::assert_snapshot!( p("2 hours, 05:06:07"), - @r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#, + @r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#, ); } } diff --git a/src/fmt/friendly/printer.rs b/src/fmt/friendly/printer.rs index c2a9a07..b3d5bd6 100644 --- a/src/fmt/friendly/printer.rs +++ b/src/fmt/friendly/printer.rs @@ -1,6 +1,6 @@ use crate::{ fmt::{ - util::{FractionalFormatter, IntegerFormatter}, + util::{DecimalFormatter, FractionalFormatter}, Write, WriteExt, }, Error, SignedDuration, Span, Unit, @@ -1223,19 +1223,19 @@ impl SpanPrinter { ) -> Result<(), Error> { let span = span.abs(); if span.get_years() != 0 { - wtr.write(Unit::Year, span.get_years().unsigned_abs().into())?; + wtr.write(Unit::Year, span.get_years().unsigned_abs())?; } if span.get_months() != 0 { - wtr.write(Unit::Month, span.get_months().unsigned_abs().into())?; + wtr.write(Unit::Month, span.get_months().unsigned_abs())?; } if span.get_weeks() != 0 { - wtr.write(Unit::Week, span.get_weeks().unsigned_abs().into())?; + wtr.write(Unit::Week, span.get_weeks().unsigned_abs())?; } if span.get_days() != 0 { - wtr.write(Unit::Day, span.get_days().unsigned_abs().into())?; + wtr.write(Unit::Day, span.get_days().unsigned_abs())?; } if span.get_hours() != 0 { - wtr.write(Unit::Hour, span.get_hours().unsigned_abs().into())?; + wtr.write(Unit::Hour, span.get_hours().unsigned_abs())?; } if span.get_minutes() != 0 { wtr.write(Unit::Minute, span.get_minutes().unsigned_abs())?; @@ -1310,7 +1310,7 @@ impl SpanPrinter { span_time = span_time.abs(); let fmtint = - IntegerFormatter::new().padding(self.padding.unwrap_or(2)); + DecimalFormatter::new().padding(self.padding.unwrap_or(2)); let fmtfraction = FractionalFormatter::new().precision(self.precision); wtr.wtr.write_int(&fmtint, span_time.get_hours_ranged().get())?; wtr.wtr.write_str(":")?; @@ -1366,16 +1366,10 @@ impl SpanPrinter { wtr.write(Unit::Minute, secs / SECS_PER_MIN)?; wtr.write(Unit::Second, secs % SECS_PER_MIN)?; let mut nanos = dur.subsec_nanos(); - wtr.write( - Unit::Millisecond, - (nanos / NANOS_PER_MILLI).into(), - )?; + wtr.write(Unit::Millisecond, nanos / NANOS_PER_MILLI)?; nanos %= NANOS_PER_MILLI; - wtr.write( - Unit::Microsecond, - (nanos / NANOS_PER_MICRO).into(), - )?; - wtr.write(Unit::Nanosecond, (nanos % NANOS_PER_MICRO).into())?; + wtr.write(Unit::Microsecond, nanos / NANOS_PER_MICRO)?; + wtr.write(Unit::Nanosecond, nanos % NANOS_PER_MICRO)?; } Some(FractionalUnit::Hour) => { wtr.write_fractional_duration(FractionalUnit::Hour, &dur)?; @@ -1427,10 +1421,7 @@ impl SpanPrinter { wtr.write(Unit::Minute, secs / SECS_PER_MIN)?; wtr.write(Unit::Second, secs % SECS_PER_MIN)?; let mut nanos = dur.subsec_nanos(); - wtr.write( - Unit::Millisecond, - (nanos / NANOS_PER_MILLI).into(), - )?; + wtr.write(Unit::Millisecond, nanos / NANOS_PER_MILLI)?; nanos %= NANOS_PER_MILLI; let leftovers = core::time::Duration::new(0, nanos); @@ -1488,7 +1479,7 @@ impl SpanPrinter { // bigger. let fmtint = - IntegerFormatter::new().padding(self.padding.unwrap_or(2)); + DecimalFormatter::new().padding(self.padding.unwrap_or(2)); let fmtfraction = FractionalFormatter::new().precision(self.precision); let mut secs = udur.as_secs(); @@ -1618,7 +1609,7 @@ struct DesignatorWriter<'p, 'w, W> { wtr: &'w mut W, desig: Designators, sign: Option, - fmtint: IntegerFormatter, + fmtint: DecimalFormatter, fmtfraction: FractionalFormatter, written_non_zero_unit: bool, } @@ -1633,7 +1624,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> { let desig = Designators::new(printer.designator); let sign = printer.direction.sign(printer, has_calendar, signum); let fmtint = - IntegerFormatter::new().padding(printer.padding.unwrap_or(0)); + DecimalFormatter::new().padding(printer.padding.unwrap_or(0)); let fmtfraction = FractionalFormatter::new().precision(printer.precision); DesignatorWriter { @@ -1679,7 +1670,12 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> { Ok(()) } - fn write(&mut self, unit: Unit, value: u64) -> Result<(), Error> { + fn write( + &mut self, + unit: Unit, + value: impl Into, + ) -> Result<(), Error> { + let value = value.into(); if value == 0 { return Ok(()); } @@ -1733,7 +1729,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> { struct FractionalPrinter { integer: u64, fraction: u32, - fmtint: IntegerFormatter, + fmtint: DecimalFormatter, fmtfraction: FractionalFormatter, } @@ -1750,7 +1746,7 @@ impl FractionalPrinter { fn from_span( span: &Span, unit: FractionalUnit, - fmtint: IntegerFormatter, + fmtint: DecimalFormatter, fmtfraction: FractionalFormatter, ) -> FractionalPrinter { debug_assert!(span.largest_unit() <= Unit::from(unit)); @@ -1762,7 +1758,7 @@ impl FractionalPrinter { fn from_duration( dur: &core::time::Duration, unit: FractionalUnit, - fmtint: IntegerFormatter, + fmtint: DecimalFormatter, fmtfraction: FractionalFormatter, ) -> FractionalPrinter { match unit { diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index ec247bb..b5886fa 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -166,11 +166,11 @@ and features.) */ use crate::{ - error::{fmt::Error as E, Error}, + error::{err, Error}, util::escape, }; -use self::util::{Fractional, FractionalFormatter, Integer, IntegerFormatter}; +use self::util::{Decimal, DecimalFormatter, Fractional, FractionalFormatter}; pub mod friendly; mod offset; @@ -218,7 +218,12 @@ impl<'i, V: core::fmt::Display> Parsed<'i, V> { if self.input.is_empty() { return Ok(self.value); } - Err(Error::from(E::into_full_error(&self.value, self.input))) + Err(err!( + "parsed value '{value}', but unparsed input {unparsed:?} \ + remains (expected no unparsed input)", + value = self.value, + unparsed = escape::Bytes(self.input), + )) } } @@ -239,7 +244,12 @@ impl<'i, V> Parsed<'i, V> { if self.input.is_empty() { return Ok(self.value); } - Err(Error::from(E::into_full_error(&display, self.input))) + Err(err!( + "parsed value '{value}', but unparsed input {unparsed:?} \ + remains (expected no unparsed input)", + value = display, + unparsed = escape::Bytes(self.input), + )) } } @@ -324,17 +334,6 @@ impl Write for &mut W { } } -impl Write for &mut dyn Write { - fn write_str(&mut self, string: &str) -> Result<(), Error> { - (**self).write_str(string) - } - - #[inline] - fn write_char(&mut self, char: char) -> Result<(), Error> { - (**self).write_char(char) - } -} - /// An adapter for using `std::io::Write` implementations with `fmt::Write`. /// /// This is useful when one wants to format a datetime or span directly @@ -369,7 +368,7 @@ pub struct StdIoWrite(pub W); impl Write for StdIoWrite { #[inline] fn write_str(&mut self, string: &str) -> Result<(), Error> { - self.0.write_all(string.as_bytes()).map_err(Error::io) + self.0.write_all(string.as_bytes()).map_err(Error::adhoc) } } @@ -412,7 +411,7 @@ impl Write for StdFmtWrite { fn write_str(&mut self, string: &str) -> Result<(), Error> { self.0 .write_str(string) - .map_err(|_| Error::from(E::StdFmtWriteAdapter)) + .map_err(|_| err!("an error occurred when formatting an argument")) } } @@ -434,7 +433,7 @@ trait WriteExt: Write { #[inline] fn write_int( &mut self, - formatter: &IntegerFormatter, + formatter: &DecimalFormatter, n: impl Into, ) -> Result<(), Error> { self.write_decimal(&formatter.format_signed(n.into())) @@ -445,7 +444,7 @@ trait WriteExt: Write { #[inline] fn write_uint( &mut self, - formatter: &IntegerFormatter, + formatter: &DecimalFormatter, n: impl Into, ) -> Result<(), Error> { self.write_decimal(&formatter.format_unsigned(n.into())) @@ -464,7 +463,7 @@ trait WriteExt: Write { /// Write the given decimal number to this buffer. #[inline] - fn write_decimal(&mut self, decimal: &Integer) -> Result<(), Error> { + fn write_decimal(&mut self, decimal: &Decimal) -> Result<(), Error> { self.write_str(decimal.as_str()) } diff --git a/src/fmt/offset.rs b/src/fmt/offset.rs index f918098..f56e48a 100644 --- a/src/fmt/offset.rs +++ b/src/fmt/offset.rs @@ -102,7 +102,7 @@ from [Temporal's hybrid grammar]. // support a span of time of about 52 hours or so.) use crate::{ - error::{fmt::offset::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, fmt::{ temporal::{PiecesNumericOffset, PiecesOffset}, util::{parse_temporal_fraction, FractionalFormatter}, @@ -110,7 +110,7 @@ use crate::{ }, tz::Offset, util::{ - parse, + escape, parse, rangeint::{ri8, RFrom}, t::{self, C}, }, @@ -237,7 +237,13 @@ impl Numeric { if part_nanoseconds >= C(500_000_000) { seconds = seconds .try_checked_add("offset-seconds", C(1)) - .context(E::PrecisionLoss)?; + .with_context(|| { + err!( + "due to precision loss, UTC offset '{}' is \ + rounded to a value that is out of bounds", + self, + ) + })?; } } Ok(Offset::from_seconds_ranged(seconds * self.sign)) @@ -248,7 +254,11 @@ impl Numeric { // `Offset` fails. impl core::fmt::Display for Numeric { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str(if self.sign == C(-1) { "-" } else { "+" })?; + if self.sign == C(-1) { + write!(f, "-")?; + } else { + write!(f, "+")?; + } write!(f, "{:02}", self.hours)?; if let Some(minutes) = self.minutes { write!(f, ":{:02}", minutes)?; @@ -258,8 +268,11 @@ impl core::fmt::Display for Numeric { } if let Some(nanos) = self.nanoseconds { static FMT: FractionalFormatter = FractionalFormatter::new(); - f.write_str(".")?; - f.write_str(FMT.format(i32::from(nanos).unsigned_abs()).as_str())?; + write!( + f, + ".{}", + FMT.format(i32::from(nanos).unsigned_abs()).as_str() + )?; } Ok(()) } @@ -400,14 +413,18 @@ impl Parser { mut input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(Error::from(E::EndOfInput)); + return Err(err!("expected UTC offset, but found end of input")); } if input[0] == b'Z' || input[0] == b'z' { if !self.zulu { - return Err(Error::from(E::UnexpectedLetterOffsetNoZulu( - input[0], - ))); + return Err(err!( + "found {z:?} in {original:?} where a numeric UTC offset \ + was expected (this context does not permit \ + the Zulu offset)", + z = escape::Byte(input[0]), + original = escape::Bytes(input), + )); } input = &input[1..]; let value = ParsedOffset { kind: ParsedOffsetKind::Zulu }; @@ -447,24 +464,40 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { + let original = escape::Bytes(input); + // Parse sign component. let Parsed { value: sign, input } = - self.parse_sign(input).context(E::InvalidSign)?; + self.parse_sign(input).with_context(|| { + err!("failed to parse sign in UTC numeric offset {original:?}") + })?; // Parse hours component. let Parsed { value: hours, input } = - self.parse_hours(input).context(E::InvalidHours)?; + self.parse_hours(input).with_context(|| { + err!( + "failed to parse hours in UTC numeric offset {original:?}" + ) + })?; let extended = match self.colon { Colon::Optional => input.starts_with(b":"), Colon::Required => { if !input.is_empty() && !input.starts_with(b":") { - return Err(Error::from(E::NoColonAfterHours)); + return Err(err!( + "parsed hour component of time zone offset from \ + {original:?}, but could not find required colon \ + separator", + )); } true } Colon::Absent => { if !input.is_empty() && input.starts_with(b":") { - return Err(Error::from(E::ColonAfterHours)); + return Err(err!( + "parsed hour component of time zone offset from \ + {original:?}, but found colon after hours which \ + is not allowed", + )); } false } @@ -480,22 +513,32 @@ impl Parser { }; // Parse optional separator after hours. - let Parsed { value: has_minutes, input } = self - .parse_separator(input, extended) - .context(E::SeparatorAfterHours)?; + let Parsed { value: has_minutes, input } = + self.parse_separator(input, extended).with_context(|| { + err!( + "failed to parse separator after hours in \ + UTC numeric offset {original:?}" + ) + })?; if !has_minutes { - return if self.require_minute - || (self.subminute && self.require_second) - { - Err(Error::from(E::MissingMinuteAfterHour)) - } else { - Ok(Parsed { value: numeric, input }) - }; + if self.require_minute || (self.subminute && self.require_second) { + return Err(err!( + "parsed hour component of time zone offset from \ + {original:?}, but could not find required minute \ + component", + )); + } + return Ok(Parsed { value: numeric, input }); } // Parse minutes component. let Parsed { value: minutes, input } = - self.parse_minutes(input).context(E::InvalidMinutes)?; + self.parse_minutes(input).with_context(|| { + err!( + "failed to parse minutes in UTC numeric offset \ + {original:?}" + ) + })?; numeric.minutes = Some(minutes); // If subminute resolution is not supported, then we're done here. @@ -506,42 +549,65 @@ impl Parser { // more precision than is supported. So we return an error here. // If this winds up being problematic, we can make this error // configurable or remove it altogether (unfortunate). - return if input.get(0).map_or(false, |&b| b == b':') { - Err(Error::from(E::SubminutePrecisionNotEnabled)) - } else { - Ok(Parsed { value: numeric, input }) - }; + if input.get(0).map_or(false, |&b| b == b':') { + return Err(err!( + "subminute precision for UTC numeric offset {original:?} \ + is not enabled in this context (must provide only \ + integral minutes)", + )); + } + return Ok(Parsed { value: numeric, input }); } // Parse optional separator after minutes. - let Parsed { value: has_seconds, input } = self - .parse_separator(input, extended) - .context(E::SeparatorAfterMinutes)?; + let Parsed { value: has_seconds, input } = + self.parse_separator(input, extended).with_context(|| { + err!( + "failed to parse separator after minutes in \ + UTC numeric offset {original:?}" + ) + })?; if !has_seconds { - return if self.require_second { - Err(Error::from(E::MissingSecondAfterMinute)) - } else { - Ok(Parsed { value: numeric, input }) - }; + if self.require_second { + return Err(err!( + "parsed hour and minute components of time zone offset \ + from {original:?}, but could not find required second \ + component", + )); + } + return Ok(Parsed { value: numeric, input }); } // Parse seconds component. let Parsed { value: seconds, input } = - self.parse_seconds(input).context(E::InvalidSeconds)?; + self.parse_seconds(input).with_context(|| { + err!( + "failed to parse seconds in UTC numeric offset \ + {original:?}" + ) + })?; numeric.seconds = Some(seconds); // If subsecond resolution is not supported, then we're done here. if !self.subsecond { if input.get(0).map_or(false, |&b| b == b'.' || b == b',') { - return Err(Error::from(E::SubsecondPrecisionNotEnabled)); + return Err(err!( + "subsecond precision for UTC numeric offset {original:?} \ + is not enabled in this context (must provide only \ + integral minutes or seconds)", + )); } return Ok(Parsed { value: numeric, input }); } // Parse an optional fractional component. let Parsed { value: nanoseconds, input } = - parse_temporal_fraction(input) - .context(E::InvalidSecondsFractional)?; + parse_temporal_fraction(input).with_context(|| { + err!( + "failed to parse fractional nanoseconds in \ + UTC numeric offset {original:?}", + ) + })?; // OK because `parse_temporal_fraction` guarantees `0..=999_999_999`. numeric.nanoseconds = nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap()); @@ -553,13 +619,19 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?; + let sign = input.get(0).copied().ok_or_else(|| { + err!("expected UTC numeric offset, but found end of input") + })?; let sign = if sign == b'+' { t::Sign::N::<1>() } else if sign == b'-' { t::Sign::N::<-1>() } else { - return Err(Error::from(E::InvalidSignPlusOrMinus)); + return Err(err!( + "expected '+' or '-' sign at start of UTC numeric offset, \ + but found {found:?} instead", + found = escape::Byte(sign), + )); }; Ok(Parsed { value: sign, input: &input[1..] }) } @@ -569,16 +641,22 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - let (hours, input) = - parse::split(input, 2).ok_or(E::EndOfInputHour)?; - let hours = parse::i64(hours).context(E::ParseHours)?; + let (hours, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit hour after sign, but found end of input",) + })?; + let hours = parse::i64(hours).with_context(|| { + err!( + "failed to parse {hours:?} as hours (a two digit integer)", + hours = escape::Bytes(hours), + ) + })?; // Note that we support a slightly bigger range of offsets than // Temporal. Temporal seems to support only up to 23 hours, but // we go up to 25 hours. This is done to support POSIX time zone // strings, which also require 25 hours (plus the maximal minute/second // components). let hours = ParsedOffsetHours::try_new("hours", hours) - .context(E::RangeHours)?; + .context("offset hours are not valid")?; Ok(Parsed { value: hours, input }) } @@ -587,11 +665,20 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - let (minutes, input) = - parse::split(input, 2).ok_or(E::EndOfInputMinute)?; - let minutes = parse::i64(minutes).context(E::ParseMinutes)?; + let (minutes, input) = parse::split(input, 2).ok_or_else(|| { + err!( + "expected two digit minute after hours, \ + but found end of input", + ) + })?; + let minutes = parse::i64(minutes).with_context(|| { + err!( + "failed to parse {minutes:?} as minutes (a two digit integer)", + minutes = escape::Bytes(minutes), + ) + })?; let minutes = ParsedOffsetMinutes::try_new("minutes", minutes) - .context(E::RangeMinutes)?; + .context("minutes are not valid")?; Ok(Parsed { value: minutes, input }) } @@ -600,11 +687,20 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - let (seconds, input) = - parse::split(input, 2).ok_or(E::EndOfInputSecond)?; - let seconds = parse::i64(seconds).context(E::ParseSeconds)?; + let (seconds, input) = parse::split(input, 2).ok_or_else(|| { + err!( + "expected two digit second after hours, \ + but found end of input", + ) + })?; + let seconds = parse::i64(seconds).with_context(|| { + err!( + "failed to parse {seconds:?} as seconds (a two digit integer)", + seconds = escape::Bytes(seconds), + ) + })?; let seconds = ParsedOffsetSeconds::try_new("seconds", seconds) - .context(E::RangeSeconds)?; + .context("time zone offset seconds are not valid")?; Ok(Parsed { value: seconds, input }) } @@ -845,7 +941,7 @@ mod tests { fn err_numeric_empty() { insta::assert_snapshot!( Parser::new().parse_numeric(b"").unwrap_err(), - @"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input", + @r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###, ); } @@ -854,7 +950,7 @@ mod tests { fn err_numeric_notsign() { insta::assert_snapshot!( Parser::new().parse_numeric(b"*").unwrap_err(), - @"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset", + @r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###, ); } @@ -863,7 +959,7 @@ mod tests { fn err_numeric_hours_too_short() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+a").unwrap_err(), - @"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input", + @r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###, ); } @@ -872,7 +968,7 @@ mod tests { fn err_numeric_hours_invalid_digits() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+ab").unwrap_err(), - @"failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): invalid digit, expected 0-9 but got a", + @r###"failed to parse hours in UTC numeric offset "+ab": failed to parse "ab" as hours (a two digit integer): invalid digit, expected 0-9 but got a"###, ); } @@ -881,7 +977,7 @@ mod tests { fn err_numeric_hours_out_of_range() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-26").unwrap_err(), - @"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25", + @r###"failed to parse hours in UTC numeric offset "-26": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###, ); } @@ -890,7 +986,7 @@ mod tests { fn err_numeric_minutes_too_short() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:a").unwrap_err(), - @"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input", + @r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###, ); } @@ -899,7 +995,7 @@ mod tests { fn err_numeric_minutes_invalid_digits() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:ab").unwrap_err(), - @"failed to parse minutes in UTC numeric offset: failed to parse minutes (requires a two digit integer): invalid digit, expected 0-9 but got a", + @r###"failed to parse minutes in UTC numeric offset "+05:ab": failed to parse "ab" as minutes (a two digit integer): invalid digit, expected 0-9 but got a"###, ); } @@ -908,7 +1004,7 @@ mod tests { fn err_numeric_minutes_out_of_range() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:60").unwrap_err(), - @"failed to parse minutes in UTC numeric offset: minute in time zone offset is out of range: parameter 'minutes' with value 60 is not in the required range of 0..=59", + @r###"failed to parse minutes in UTC numeric offset "-05:60": minutes are not valid: parameter 'minutes' with value 60 is not in the required range of 0..=59"###, ); } @@ -917,7 +1013,7 @@ mod tests { fn err_numeric_seconds_too_short() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:30:a").unwrap_err(), - @"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input", + @r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###, ); } @@ -926,7 +1022,7 @@ mod tests { fn err_numeric_seconds_invalid_digits() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(), - @"failed to parse seconds in UTC numeric offset: failed to parse seconds (requires a two digit integer): invalid digit, expected 0-9 but got a", + @r###"failed to parse seconds in UTC numeric offset "+05:30:ab": failed to parse "ab" as seconds (a two digit integer): invalid digit, expected 0-9 but got a"###, ); } @@ -935,7 +1031,7 @@ mod tests { fn err_numeric_seconds_out_of_range() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:60").unwrap_err(), - @"failed to parse seconds in UTC numeric offset: second in time zone offset is out of range: parameter 'seconds' with value 60 is not in the required range of 0..=59", + @r###"failed to parse seconds in UTC numeric offset "-05:30:60": time zone offset seconds are not valid: parameter 'seconds' with value 60 is not in the required range of 0..=59"###, ); } @@ -945,31 +1041,31 @@ mod tests { fn err_numeric_fraction_non_empty() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(), - @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", + @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(), - @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", + @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); // Instead of end-of-string, add invalid digit. insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:44.a").unwrap_err(), - @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", + @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44.a": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(), - @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", + @r###"failed to parse fractional nanoseconds in UTC numeric offset "-05:30:44,a": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); // And also test basic format. insta::assert_snapshot!( Parser::new().parse_numeric(b"-053044.a").unwrap_err(), - @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", + @r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044.a": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); insta::assert_snapshot!( Parser::new().parse_numeric(b"-053044,a").unwrap_err(), - @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", + @r###"failed to parse fractional nanoseconds in UTC numeric offset "-053044,a": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); } @@ -980,7 +1076,7 @@ mod tests { fn err_numeric_subminute_disabled_but_desired() { insta::assert_snapshot!( Parser::new().subminute(false).parse_numeric(b"-05:59:32").unwrap_err(), - @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)", + @r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###, ); } @@ -990,11 +1086,11 @@ mod tests { fn err_zulu_disabled_but_desired() { insta::assert_snapshot!( Parser::new().zulu(false).parse(b"Z").unwrap_err(), - @"found `Z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)", + @r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###, ); insta::assert_snapshot!( Parser::new().zulu(false).parse(b"z").unwrap_err(), - @"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)", + @r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###, ); } @@ -1022,7 +1118,7 @@ mod tests { }; insta::assert_snapshot!( numeric.to_offset().unwrap_err(), - @"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599", + @"due to precision loss, UTC offset '+25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599", ); } @@ -1047,7 +1143,7 @@ mod tests { }; insta::assert_snapshot!( numeric.to_offset().unwrap_err(), - @"due to precision loss, offset is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599", + @"due to precision loss, UTC offset '-25:59:59.5' is rounded to a value that is out of bounds: parameter 'offset-seconds' with value 1 is not in the required range of -93599..=93599", ); } } diff --git a/src/fmt/rfc2822.rs b/src/fmt/rfc2822.rs index d199a81..7c986d9 100644 --- a/src/fmt/rfc2822.rs +++ b/src/fmt/rfc2822.rs @@ -43,11 +43,11 @@ general interchange format for new applications. use crate::{ civil::{Date, DateTime, Time, Weekday}, - error::{fmt::rfc2822::Error as E, ErrorContext}, - fmt::{util::IntegerFormatter, Parsed, Write, WriteExt}, + error::{err, ErrorContext}, + fmt::{util::DecimalFormatter, Parsed, Write, WriteExt}, tz::{Offset, TimeZone}, util::{ - parse, + escape, parse, rangeint::{ri8, RFrom}, t::{self, C}, }, @@ -313,7 +313,9 @@ impl DateTimeParser { let input = input.as_ref(); let zdt = self .parse_zoned_internal(input) - .context(E::FailedZoned)? + .context( + "failed to parse RFC 2822 datetime into Jiff zoned datetime", + )? .into_full()?; Ok(zdt) } @@ -349,7 +351,7 @@ impl DateTimeParser { let input = input.as_ref(); let ts = self .parse_timestamp_internal(input) - .context(E::FailedTimestamp)? + .context("failed to parse RFC 2822 datetime into Jiff timestamp")? .into_full()?; Ok(ts) } @@ -365,7 +367,9 @@ impl DateTimeParser { ) -> Result, Error> { let Parsed { value: (dt, offset), input } = self.parse_datetime_offset(input)?; - let ts = offset.to_timestamp(dt)?; + let ts = offset + .to_timestamp(dt) + .context("RFC 2822 datetime out of Jiff's range")?; let zdt = ts.to_zoned(TimeZone::fixed(offset)); Ok(Parsed { value: zdt, input }) } @@ -381,7 +385,9 @@ impl DateTimeParser { ) -> Result, Error> { let Parsed { value: (dt, offset), input } = self.parse_datetime_offset(input)?; - let ts = offset.to_timestamp(dt)?; + let ts = offset + .to_timestamp(dt) + .context("RFC 2822 datetime out of Jiff's range")?; Ok(Parsed { value: ts, input }) } @@ -419,11 +425,16 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(Error::from(E::Empty)); + return Err(err!( + "expected RFC 2822 datetime, but got empty string" + )); } let Parsed { input, .. } = self.skip_whitespace(input); if input.is_empty() { - return Err(Error::from(E::EmptyAfterWhitespace)); + return Err(err!( + "expected RFC 2822 datetime, but got empty string after \ + trimming whitespace", + )); } let Parsed { value: wd, input } = self.parse_weekday(input)?; let Parsed { value: day, input } = self.parse_day(input)?; @@ -440,19 +451,26 @@ impl DateTimeParser { self.skip_whitespace(input); let (second, input) = if !input.starts_with(b":") { if !whitespace_after_minute { - return Err(Error::from(E::WhitespaceAfterTime)); + return Err(err!( + "expected whitespace after parsing time: \ + expected at least one whitespace character \ + (space or tab), but found none", + )); } (t::Second::N::<0>(), input) } else { let Parsed { input, .. } = self.parse_time_separator(input)?; let Parsed { input, .. } = self.skip_whitespace(input); let Parsed { value: second, input } = self.parse_second(input)?; - let Parsed { input, .. } = self.parse_whitespace(input)?; + let Parsed { input, .. } = + self.parse_whitespace(input).with_context(|| { + err!("expected whitespace after parsing time") + })?; (second, input) }; let date = - Date::new_ranged(year, month, day).context(E::InvalidDate)?; + Date::new_ranged(year, month, day).context("invalid date")?; let time = Time::new_ranged( hour, minute, @@ -462,10 +480,13 @@ impl DateTimeParser { let dt = DateTime::from_parts(date, time); if let Some(wd) = wd { if !self.relaxed_weekday && wd != dt.weekday() { - return Err(Error::from(E::InconsistentWeekday { - parsed: wd, - from_date: dt.weekday(), - })); + return Err(err!( + "found parsed weekday of {parsed}, \ + but parsed datetime of {dt} has weekday \ + {has}", + parsed = weekday_abbrev(wd), + has = weekday_abbrev(dt.weekday()), + )); } } Ok(Parsed { value: dt, input }) @@ -496,13 +517,15 @@ impl DateTimeParser { if matches!(input[0], b'0'..=b'9') { return Ok(Parsed { value: None, input }); } - if let Ok(len) = u8::try_from(input.len()) { - if len < 4 { - return Err(Error::from(E::TooShortWeekday { - got_non_digit: input[0], - len, - })); - } + if input.len() < 4 { + return Err(err!( + "expected day at beginning of RFC 2822 datetime \ + since first non-whitespace byte, {first:?}, \ + is not a digit, but given string is too short \ + (length is {length})", + first = escape::Byte(input[0]), + length = input.len(), + )); } let b1 = input[0]; let b2 = input[1]; @@ -520,19 +543,31 @@ impl DateTimeParser { b"fri" => Weekday::Friday, b"sat" => Weekday::Saturday, _ => { - return Err(Error::from(E::InvalidWeekday { - got_non_digit: input[0], - })); + return Err(err!( + "expected day at beginning of RFC 2822 datetime \ + since first non-whitespace byte, {first:?}, \ + is not a digit, but did not recognize {got:?} \ + as a valid weekday abbreviation", + first = escape::Byte(input[0]), + got = escape::Bytes(&input[..3]), + )); } }; let Parsed { input, .. } = self.skip_whitespace(&input[3..]); let Some(should_be_comma) = input.get(0).copied() else { - return Err(Error::from(E::EndOfInputComma)); + return Err(err!( + "expected comma after parsed weekday `{weekday}` in \ + RFC 2822 datetime, but found end of string instead", + weekday = escape::Bytes(&[b1, b2, b3]), + )); }; if should_be_comma != b',' { - return Err(Error::from(E::UnexpectedByteComma { - byte: should_be_comma, - })); + return Err(err!( + "expected comma after parsed weekday `{weekday}` in \ + RFC 2822 datetime, but found `{got:?}` instead", + weekday = escape::Bytes(&[b1, b2, b3]), + got = escape::Byte(should_be_comma), + )); } let Parsed { input, .. } = self.skip_whitespace(&input[1..]); Ok(Parsed { value: Some(wd), input }) @@ -551,17 +586,21 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(Error::from(E::EndOfInputDay)); + return Err(err!("expected day, but found end of input")); } let mut digits = 1; if input.len() >= 2 && matches!(input[1], b'0'..=b'9') { digits = 2; } let (day, input) = input.split_at(digits); - let day = parse::i64(day).context(E::ParseDay)?; - let day = t::Day::try_new("day", day).context(E::ParseDay)?; + let day = parse::i64(day).with_context(|| { + err!("failed to parse {day:?} as day", day = escape::Bytes(day)) + })?; + let day = t::Day::try_new("day", day).context("day is not valid")?; let Parsed { input, .. } = - self.parse_whitespace(input).context(E::WhitespaceAfterDay)?; + self.parse_whitespace(input).with_context(|| { + err!("expected whitespace after parsing day {day}") + })?; Ok(Parsed { value: day, input }) } @@ -578,12 +617,16 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(Error::from(E::EndOfInputMonth)); + return Err(err!( + "expected abbreviated month name, but found end of input" + )); } - if let Ok(len) = u8::try_from(input.len()) { - if len < 3 { - return Err(Error::from(E::TooShortMonth { len })); - } + if input.len() < 3 { + return Err(err!( + "expected abbreviated month name, but remaining input \ + is too short (remaining bytes is {length})", + length = input.len(), + )); } let b1 = input[0].to_ascii_lowercase(); let b2 = input[1].to_ascii_lowercase(); @@ -601,14 +644,22 @@ impl DateTimeParser { b"oct" => 10, b"nov" => 11, b"dec" => 12, - _ => return Err(Error::from(E::InvalidMonth)), + _ => { + return Err(err!( + "expected abbreviated month name, \ + but did not recognize {got:?} \ + as a valid month", + got = escape::Bytes(&input[..3]), + )); + } }; // OK because we just assigned a numeric value ourselves // above, and all values are valid months. let month = t::Month::new(month).unwrap(); - let Parsed { input, .. } = self - .parse_whitespace(&input[3..]) - .context(E::WhitespaceAfterMonth)?; + let Parsed { input, .. } = + self.parse_whitespace(&input[3..]).with_context(|| { + err!("expected whitespace after parsing month name") + })?; Ok(Parsed { value: month, input }) } @@ -641,22 +692,31 @@ impl DateTimeParser { { digits += 1; } - if let Ok(len) = u8::try_from(digits) { - if len <= 1 { - return Err(Error::from(E::TooShortYear { len })); - } + if digits <= 1 { + return Err(err!( + "expected at least two ASCII digits for parsing \ + a year, but only found {digits}", + )); } let (year, input) = input.split_at(digits); - let year = parse::i64(year).context(E::ParseYear)?; + let year = parse::i64(year).with_context(|| { + err!( + "failed to parse {year:?} as year \ + (a two, three or four digit integer)", + year = escape::Bytes(year), + ) + })?; let year = match digits { 2 if year <= 49 => year + 2000, 2 | 3 => year + 1900, 4 => year, _ => unreachable!("digits={digits} must be 2, 3 or 4"), }; - let year = t::Year::try_new("year", year).context(E::InvalidYear)?; - let Parsed { input, .. } = - self.parse_whitespace(input).context(E::WhitespaceAfterYear)?; + let year = + t::Year::try_new("year", year).context("year is not valid")?; + let Parsed { input, .. } = self + .parse_whitespace(input) + .with_context(|| err!("expected whitespace after parsing year"))?; Ok(Parsed { value: year, input }) } @@ -670,9 +730,17 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (hour, input) = parse::split(input, 2).ok_or(E::EndOfInputHour)?; - let hour = parse::i64(hour).context(E::ParseHour)?; - let hour = t::Hour::try_new("hour", hour).context(E::InvalidHour)?; + let (hour, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit hour, but found end of input") + })?; + let hour = parse::i64(hour).with_context(|| { + err!( + "failed to parse {hour:?} as hour (a two digit integer)", + hour = escape::Bytes(hour), + ) + })?; + let hour = + t::Hour::try_new("hour", hour).context("hour is not valid")?; Ok(Parsed { value: hour, input }) } @@ -683,11 +751,17 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (minute, input) = - parse::split(input, 2).ok_or(E::EndOfInputMinute)?; - let minute = parse::i64(minute).context(E::ParseMinute)?; - let minute = - t::Minute::try_new("minute", minute).context(E::InvalidMinute)?; + let (minute, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit minute, but found end of input") + })?; + let minute = parse::i64(minute).with_context(|| { + err!( + "failed to parse {minute:?} as minute (a two digit integer)", + minute = escape::Bytes(minute), + ) + })?; + let minute = t::Minute::try_new("minute", minute) + .context("minute is not valid")?; Ok(Parsed { value: minute, input }) } @@ -698,14 +772,20 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (second, input) = - parse::split(input, 2).ok_or(E::EndOfInputSecond)?; - let mut second = parse::i64(second).context(E::ParseSecond)?; + let (second, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit second, but found end of input") + })?; + let mut second = parse::i64(second).with_context(|| { + err!( + "failed to parse {second:?} as second (a two digit integer)", + second = escape::Bytes(second), + ) + })?; if second == 60 { second = 59; } - let second = - t::Second::try_new("second", second).context(E::InvalidSecond)?; + let second = t::Second::try_new("second", second) + .context("second is not valid")?; Ok(Parsed { value: second, input }) } @@ -721,7 +801,13 @@ impl DateTimeParser { type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>; type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>; - let sign = input.get(0).copied().ok_or(E::EndOfInputOffset)?; + let sign = input.get(0).copied().ok_or_else(|| { + err!( + "expected sign for time zone offset, \ + (or a legacy time zone name abbreviation), \ + but found end of input", + ) + })?; let sign = if sign == b'+' { t::Sign::N::<1>() } else if sign == b'-' { @@ -730,16 +816,32 @@ impl DateTimeParser { return self.parse_offset_obsolete(input); }; let input = &input[1..]; - let (hhmm, input) = parse::split(input, 4).ok_or(E::TooShortOffset)?; + let (hhmm, input) = parse::split(input, 4).ok_or_else(|| { + err!( + "expected at least 4 digits for time zone offset \ + after sign, but found only {len} bytes remaining", + len = input.len(), + ) + })?; - let hh = parse::i64(&hhmm[0..2]).context(E::ParseOffsetHour)?; + let hh = parse::i64(&hhmm[0..2]).with_context(|| { + err!( + "failed to parse hours from time zone offset {hhmm}", + hhmm = escape::Bytes(hhmm) + ) + })?; let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh) - .context(E::InvalidOffsetHour)?; + .context("time zone offset hours are not valid")?; let hh = t::SpanZoneOffset::rfrom(hh); - let mm = parse::i64(&hhmm[2..4]).context(E::ParseOffsetMinute)?; + let mm = parse::i64(&hhmm[2..4]).with_context(|| { + err!( + "failed to parse minutes from time zone offset {hhmm}", + hhmm = escape::Bytes(hhmm) + ) + })?; let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm) - .context(E::InvalidOffsetMinute)?; + .context("time zone offset minutes are not valid")?; let mm = t::SpanZoneOffset::rfrom(mm); let seconds = hh * C(3_600) + mm * C(60); @@ -763,7 +865,11 @@ impl DateTimeParser { len += 1; } if len == 0 { - return Err(Error::from(E::WhitespaceAfterTimeForObsoleteOffset)); + return Err(err!( + "expected obsolete RFC 2822 time zone abbreviation, \ + but found no remaining non-whitespace characters \ + after time", + )); } let offset = match &letters[..len] { b"ut" | b"gmt" | b"z" => Offset::UTC, @@ -811,7 +917,11 @@ impl DateTimeParser { Offset::UTC } else { // But anything else we throw our hands up I guess. - return Err(Error::from(E::InvalidObsoleteOffset)); + return Err(err!( + "expected obsolete RFC 2822 time zone abbreviation, \ + but found {found:?}", + found = escape::Bytes(&input[..len]), + )); } } }; @@ -826,12 +936,15 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(Error::from(E::EndOfInputTimeSeparator)); + return Err(err!( + "expected time separator of ':', but found end of input", + )); } if input[0] != b':' { - return Err(Error::from(E::UnexpectedByteTimeSeparator { - byte: input[0], - })); + return Err(err!( + "expected time separator of ':', but found {got}", + got = escape::Byte(input[0]), + )); } Ok(Parsed { value: (), input: &input[1..] }) } @@ -846,7 +959,10 @@ impl DateTimeParser { let Parsed { input, value: had_whitespace } = self.skip_whitespace(input); if !had_whitespace { - return Err(Error::from(E::WhitespaceAfterTime)); + return Err(err!( + "expected at least one whitespace character (space or tab), \ + but found none", + )); } Ok(Parsed { value: (), input }) } @@ -896,20 +1012,26 @@ impl DateTimeParser { // I believe this error case is actually impossible, since as // soon as we hit 0, we break out. If there is more "comment," // then it will flag an error as unparsed input. - depth = depth - .checked_sub(1) - .ok_or(E::CommentClosingParenWithoutOpen)?; + depth = depth.checked_sub(1).ok_or_else(|| { + err!( + "found closing parenthesis in comment with \ + no matching opening parenthesis" + ) + })?; if depth == 0 { break; } } else if byte == b'(' { - depth = depth - .checked_add(1) - .ok_or(E::CommentTooManyNestedParens)?; + depth = depth.checked_add(1).ok_or_else(|| { + err!("found too many nested parenthesis in comment") + })?; } } if depth > 0 { - return Err(Error::from(E::CommentOpeningParenWithoutClose)); + return Err(err!( + "found opening parenthesis in comment with \ + no matching closing parenthesis" + )); } let Parsed { input, .. } = self.skip_whitespace(input); Ok(Parsed { value: (), input }) @@ -1292,16 +1414,19 @@ impl DateTimePrinter { offset: Option, mut wtr: W, ) -> Result<(), Error> { - static FMT_DAY: IntegerFormatter = IntegerFormatter::new(); - static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4); - static FMT_TIME_UNIT: IntegerFormatter = - IntegerFormatter::new().padding(2); + static FMT_DAY: DecimalFormatter = DecimalFormatter::new(); + static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4); + static FMT_TIME_UNIT: DecimalFormatter = + DecimalFormatter::new().padding(2); if dt.year() < 0 { // RFC 2822 actually says the year must be at least 1900, but // other implementations (like Chrono) allow any positive 4-digit // year. - return Err(Error::from(E::NegativeYear)); + return Err(err!( + "datetime {dt} has negative year, \ + which cannot be formatted with RFC 2822", + )); } wtr.write_str(weekday_abbrev(dt.weekday()))?; @@ -1349,17 +1474,20 @@ impl DateTimePrinter { timestamp: &Timestamp, mut wtr: W, ) -> Result<(), Error> { - static FMT_DAY: IntegerFormatter = IntegerFormatter::new().padding(2); - static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4); - static FMT_TIME_UNIT: IntegerFormatter = - IntegerFormatter::new().padding(2); + static FMT_DAY: DecimalFormatter = DecimalFormatter::new().padding(2); + static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4); + static FMT_TIME_UNIT: DecimalFormatter = + DecimalFormatter::new().padding(2); let dt = TimeZone::UTC.to_datetime(*timestamp); if dt.year() < 0 { // RFC 2822 actually says the year must be at least 1900, but // other implementations (like Chrono) allow any positive 4-digit // year. - return Err(Error::from(E::NegativeYear)); + return Err(err!( + "datetime {dt} has negative year, \ + which cannot be formatted with RFC 2822", + )); } wtr.write_str(weekday_abbrev(dt.weekday()))?; @@ -1615,7 +1743,7 @@ mod tests { insta::assert_snapshot!( p("Thu, 10 Jan 2024 05:34:45 -0500"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of `Thursday`, but parsed datetime has weekday `Wednesday`", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of Thu, but parsed datetime of 2024-01-10T05:34:45 has weekday Wed", ); insta::assert_snapshot!( p("Wed, 29 Feb 2023 05:34:45 -0500"), @@ -1627,11 +1755,11 @@ mod tests { ); insta::assert_snapshot!( p("Tue, 32 Jun 2024 05:34:45 -0500"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: failed to parse day: parameter 'day' with value 32 is not in the required range of 1..=31", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: day is not valid: parameter 'day' with value 32 is not in the required range of 1..=31", ); insta::assert_snapshot!( p("Sun, 30 Jun 2024 24:00:00 -0500"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: invalid hour: parameter 'hour' with value 24 is not in the required range of 0..=23", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23", ); // No whitespace after time insta::assert_snapshot!( @@ -1652,43 +1780,43 @@ mod tests { ); insta::assert_snapshot!( p(" "), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming leading whitespace", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming whitespace", ); insta::assert_snapshot!( p("Wat"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)", + @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###, ); insta::assert_snapshot!( p("Wed"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but given string is too short (length is 3)", + @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but given string is too short (length is 3)"###, ); insta::assert_snapshot!( p("Wed "), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday in RFC 2822 datetime, but found end of input instead", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected comma after parsed weekday `Wed` in RFC 2822 datetime, but found end of string instead", ); insta::assert_snapshot!( p("Wed ,"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input", ); insta::assert_snapshot!( p("Wed , "), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input", ); insta::assert_snapshot!( p("Wat, "), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, `W`, is not a digit, but did not recognize a valid weekday abbreviation", + @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day at beginning of RFC 2822 datetime since first non-whitespace byte, "W", is not a digit, but did not recognize "Wat" as a valid weekday abbreviation"###, ); insta::assert_snapshot!( p("Wed, "), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input", ); insta::assert_snapshot!( p("Wed, 1"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 1: expected at least one whitespace character (space or tab), but found none", ); insta::assert_snapshot!( p("Wed, 10"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing day 10: expected at least one whitespace character (space or tab), but found none", ); insta::assert_snapshot!( p("Wed, 10 J"), @@ -1696,11 +1824,11 @@ mod tests { ); insta::assert_snapshot!( p("Wed, 10 Wat"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize a valid abbreviated month name", + @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###, ); insta::assert_snapshot!( p("Wed, 10 Jan"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing abbreviated month name: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing month name: expected at least one whitespace character (space or tab), but found none", ); insta::assert_snapshot!( p("Wed, 10 Jan 2"), @@ -1708,15 +1836,15 @@ mod tests { ); insta::assert_snapshot!( p("Wed, 10 Jan 2024"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected whitespace after parsing year: expected at least one whitespace character (space or tab), but found none", ); insta::assert_snapshot!( p("Wed, 10 Jan 2024 05"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found end of input", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found end of input", ); insta::assert_snapshot!( p("Wed, 10 Jan 2024 053"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of `:`, but found `3`", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected time separator of ':', but found 3", ); insta::assert_snapshot!( p("Wed, 10 Jan 2024 05:34"), @@ -1732,7 +1860,7 @@ mod tests { ); insta::assert_snapshot!( p("Wed, 10 Jan 2024 05:34:45 J"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but did not recognize a valid abbreviation", + @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "J""###, ); } @@ -1912,7 +2040,7 @@ mod tests { .at(5, 34, 45, 0) .in_tz("America/New_York") .unwrap(); - insta::assert_snapshot!(p(&zdt), @"datetime has negative year, which cannot be formatted with RFC 2822"); + insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822"); } #[test] @@ -1934,6 +2062,6 @@ mod tests { .in_tz("America/New_York") .unwrap() .timestamp(); - insta::assert_snapshot!(p(ts), @"datetime has negative year, which cannot be formatted with RFC 2822"); + insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822"); } } diff --git a/src/fmt/rfc9557.rs b/src/fmt/rfc9557.rs index 0c24148..259e92c 100644 --- a/src/fmt/rfc9557.rs +++ b/src/fmt/rfc9557.rs @@ -95,13 +95,13 @@ including by returning an error if it isn't supported. // UTCOffsetMinutePrecision use crate::{ - error::{fmt::rfc9557::Error as E, Error}, + error::{err, Error}, fmt::{ offset::{self, ParsedOffset}, temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind}, Parsed, }, - util::parse, + util::{escape, parse}, }; /// The result of parsing RFC 9557 annotations. @@ -112,6 +112,11 @@ use crate::{ /// only validated at a syntax level. #[derive(Debug)] pub(crate) struct ParsedAnnotations<'i> { + /// The original input that all of the annotations were parsed from. + /// + /// N.B. This is currently unused, but potentially useful, so we leave it. + #[allow(dead_code)] + input: escape::Bytes<'i>, /// An optional time zone annotation that was extracted from the input. time_zone: Option>, // While we parse/validate them, we don't support any other annotations @@ -122,7 +127,7 @@ pub(crate) struct ParsedAnnotations<'i> { impl<'i> ParsedAnnotations<'i> { /// Return an empty parsed annotations. pub(crate) fn none() -> ParsedAnnotations<'static> { - ParsedAnnotations { time_zone: None } + ParsedAnnotations { input: escape::Bytes(&[]), time_zone: None } } /// Turns this parsed time zone into a structured time zone annotation, @@ -207,6 +212,8 @@ impl Parser { &self, input: &'i [u8], ) -> Result>, Error> { + let mkslice = parse::slicer(input); + let Parsed { value: time_zone, mut input } = self.parse_time_zone_annotation(input)?; loop { @@ -222,7 +229,10 @@ impl Parser { input = unconsumed; } - let value = ParsedAnnotations { time_zone }; + let value = ParsedAnnotations { + input: escape::Bytes(mkslice(input)), + time_zone, + }; Ok(Parsed { value, input }) } @@ -231,18 +241,14 @@ impl Parser { mut input: &'i [u8], ) -> Result>>, Error> { let unconsumed = input; - let Some((&first, tail)) = input.split_first() else { - return Ok(Parsed { value: None, input: unconsumed }); - }; - if first != b'[' { + if input.is_empty() || input[0] != b'[' { return Ok(Parsed { value: None, input: unconsumed }); } - input = tail; + input = &input[1..]; - let mut critical = false; - if let Some(tail) = input.strip_prefix(b"!") { - critical = true; - input = tail; + let critical = input.starts_with(b"!"); + if critical { + input = &input[1..]; } // If we're starting with a `+` or `-`, then we know we MUST have a @@ -278,8 +284,8 @@ impl Parser { // a generic key/value annotation. return Ok(Parsed { value: None, input: unconsumed }); } - while let Some(tail) = input.strip_prefix(b"/") { - input = tail; + while input.starts_with(b"/") { + input = &input[1..]; let Parsed { input: unconsumed, .. } = self.parse_tz_annotation_iana_name(input)?; input = unconsumed; @@ -300,21 +306,17 @@ impl Parser { &self, mut input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Ok(Parsed { value: false, input }); - }; - if first != b'[' { + if input.is_empty() || input[0] != b'[' { return Ok(Parsed { value: false, input }); } - input = tail; + input = &input[1..]; - let mut critical = false; - if let Some(tail) = input.strip_prefix(b"!") { - critical = true; - input = tail; + let critical = input.starts_with(b"!"); + if critical { + input = &input[1..]; } - let Parsed { input, .. } = self.parse_annotation_key(input)?; + let Parsed { value: key, input } = self.parse_annotation_key(input)?; let Parsed { input, .. } = self.parse_annotation_separator(input)?; let Parsed { input, .. } = self.parse_annotation_values(input)?; let Parsed { input, .. } = self.parse_annotation_close(input)?; @@ -324,7 +326,11 @@ impl Parser { // critical flag isn't set, we're "permissive" and just validate that // the syntax is correct (as we've already done at this point). if critical { - return Err(Error::from(E::UnsupportedAnnotationCritical)); + return Err(err!( + "found unsupported RFC 9557 annotation with key {key:?} \ + with the critical flag ('!') set", + key = escape::Bytes(key), + )); } Ok(Parsed { value: true, input }) @@ -375,8 +381,8 @@ impl Parser { input: &'i [u8], ) -> Result, Error> { let Parsed { mut input, .. } = self.parse_annotation_value(input)?; - while let Some(tail) = input.strip_prefix(b"-") { - input = tail; + while input.starts_with(b"-") { + input = &input[1..]; let Parsed { input: unconsumed, .. } = self.parse_annotation_value(input)?; input = unconsumed; @@ -407,137 +413,173 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Err(Error::from(E::EndOfInputAnnotation)); - }; - if !matches!(first, b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') { - return Err(Error::from(E::UnexpectedByteAnnotation { - byte: first, - })); + if input.is_empty() { + return Err(err!( + "expected the start of an RFC 9557 annotation or IANA \ + time zone component name, but found end of input instead", + )); } - Ok(Parsed { value: (), input: tail }) + if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') { + return Err(err!( + "expected ASCII alphabetic byte (or underscore or period) \ + at the start of an RFC 9557 annotation or time zone \ + component name, but found {:?} instead", + escape::Byte(input[0]), + )); + } + Ok(Parsed { value: (), input: &input[1..] }) } fn parse_tz_annotation_char<'i>( &self, input: &'i [u8], ) -> Parsed<'i, bool> { - let Some((&first, tail)) = input.split_first() else { - return Parsed { value: false, input }; + let is_tz_annotation_char = |byte| { + matches!( + byte, + b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', + ) }; - - if !matches!( - first, - b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', - ) { + if input.is_empty() || !is_tz_annotation_char(input[0]) { return Parsed { value: false, input }; } - Parsed { value: true, input: tail } + Parsed { value: true, input: &input[1..] } } fn parse_annotation_key_leading_char<'i>( &self, input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Err(Error::from(E::EndOfInputAnnotationKey)); - }; - if !matches!(first, b'_' | b'a'..=b'z') { - return Err(Error::from(E::UnexpectedByteAnnotationKey { - byte: first, - })); + if input.is_empty() { + return Err(err!( + "expected the start of an RFC 9557 annotation key, \ + but found end of input instead", + )); } - Ok(Parsed { value: (), input: tail }) + if !matches!(input[0], b'_' | b'a'..=b'z') { + return Err(err!( + "expected lowercase alphabetic byte (or underscore) \ + at the start of an RFC 9557 annotation key, \ + but found {:?} instead", + escape::Byte(input[0]), + )); + } + Ok(Parsed { value: (), input: &input[1..] }) } fn parse_annotation_key_char<'i>( &self, input: &'i [u8], ) -> Parsed<'i, bool> { - let Some((&first, tail)) = input.split_first() else { - return Parsed { value: false, input }; - }; - if !matches!(first, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z') { + let is_annotation_key_char = + |byte| matches!(byte, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z'); + if input.is_empty() || !is_annotation_key_char(input[0]) { return Parsed { value: false, input }; } - Parsed { value: true, input: tail } + Parsed { value: true, input: &input[1..] } } fn parse_annotation_value_leading_char<'i>( &self, input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Err(Error::from(E::EndOfInputAnnotationValue)); - }; - if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') { - return Err(Error::from(E::UnexpectedByteAnnotationValue { - byte: first, - })); + if input.is_empty() { + return Err(err!( + "expected the start of an RFC 9557 annotation value, \ + but found end of input instead", + )); } - Ok(Parsed { value: (), input: tail }) + if !matches!(input[0], b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') { + return Err(err!( + "expected alphanumeric ASCII byte \ + at the start of an RFC 9557 annotation value, \ + but found {:?} instead", + escape::Byte(input[0]), + )); + } + Ok(Parsed { value: (), input: &input[1..] }) } fn parse_annotation_value_char<'i>( &self, input: &'i [u8], ) -> Parsed<'i, bool> { - let Some((&first, tail)) = input.split_first() else { - return Parsed { value: false, input }; - }; - if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') { + let is_annotation_value_char = + |byte| matches!(byte, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z'); + if input.is_empty() || !is_annotation_value_char(input[0]) { return Parsed { value: false, input }; } - Parsed { value: true, input: tail } + Parsed { value: true, input: &input[1..] } } fn parse_annotation_separator<'i>( &self, input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Err(Error::from(E::EndOfInputAnnotationSeparator)); - }; - if first != b'=' { + if input.is_empty() { + return Err(err!( + "expected an '=' after parsing an RFC 9557 annotation key, \ + but found end of input instead", + )); + } + if input[0] != b'=' { // If we see a /, then it's likely the user was trying to insert a // time zone annotation in the wrong place. - return Err(Error::from(if first == b'/' { - E::UnexpectedSlashAnnotationSeparator + return Err(if input[0] == b'/' { + err!( + "expected an '=' after parsing an RFC 9557 annotation \ + key, but found / instead (time zone annotations must \ + come first)", + ) } else { - E::UnexpectedByteAnnotationSeparator { byte: first } - })); + err!( + "expected an '=' after parsing an RFC 9557 annotation \ + key, but found {:?} instead", + escape::Byte(input[0]), + ) + }); } - Ok(Parsed { value: (), input: tail }) + Ok(Parsed { value: (), input: &input[1..] }) } fn parse_annotation_close<'i>( &self, input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Err(Error::from(E::EndOfInputAnnotationClose)); - }; - if first != b']' { - return Err(Error::from(E::UnexpectedByteAnnotationClose { - byte: first, - })); + if input.is_empty() { + return Err(err!( + "expected an ']' after parsing an RFC 9557 annotation key \ + and value, but found end of input instead", + )); } - Ok(Parsed { value: (), input: tail }) + if input[0] != b']' { + return Err(err!( + "expected an ']' after parsing an RFC 9557 annotation key \ + and value, but found {:?} instead", + escape::Byte(input[0]), + )); + } + Ok(Parsed { value: (), input: &input[1..] }) } fn parse_tz_annotation_close<'i>( &self, input: &'i [u8], ) -> Result, Error> { - let Some((&first, tail)) = input.split_first() else { - return Err(Error::from(E::EndOfInputTzAnnotationClose)); - }; - if first != b']' { - return Err(Error::from(E::UnexpectedByteTzAnnotationClose { - byte: first, - })); + if input.is_empty() { + return Err(err!( + "expected an ']' after parsing an RFC 9557 time zone \ + annotation, but found end of input instead", + )); } - Ok(Parsed { value: (), input: tail }) + if input[0] != b']' { + return Err(err!( + "expected an ']' after parsing an RFC 9557 time zone \ + annotation, but found {:?} instead", + escape::Byte(input[0]), + )); + } + Ok(Parsed { value: (), input: &input[1..] }) } } @@ -623,22 +665,24 @@ mod tests { fn ok_empty() { let p = |input| Parser::new().parse(input).unwrap(); - insta::assert_debug_snapshot!(p(b""), @r#" + insta::assert_debug_snapshot!(p(b""), @r###" Parsed { value: ParsedAnnotations { + input: "", time_zone: None, }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"blah"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"blah"), @r###" Parsed { value: ParsedAnnotations { + input: "", time_zone: None, }, input: "blah", } - "#); + "###); } #[test] @@ -647,36 +691,39 @@ mod tests { insta::assert_debug_snapshot!( p(b"[u-ca=chinese]"), - @r#" + @r###" Parsed { value: ParsedAnnotations { + input: "[u-ca=chinese]", time_zone: None, }, input: "", } - "#, + "###, ); insta::assert_debug_snapshot!( p(b"[u-ca=chinese-japanese]"), - @r#" + @r###" Parsed { value: ParsedAnnotations { + input: "[u-ca=chinese-japanese]", time_zone: None, }, input: "", } - "#, + "###, ); insta::assert_debug_snapshot!( p(b"[u-ca=chinese-japanese-russian]"), - @r#" + @r###" Parsed { value: ParsedAnnotations { + input: "[u-ca=chinese-japanese-russian]", time_zone: None, }, input: "", } - "#, + "###, ); } @@ -684,9 +731,10 @@ mod tests { fn ok_iana() { let p = |input| Parser::new().parse(input).unwrap(); - insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r#" + insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###" Parsed { value: ParsedAnnotations { + input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -696,10 +744,11 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###" Parsed { value: ParsedAnnotations { + input: "[!America/New_York]", time_zone: Some( Named { critical: true, @@ -709,10 +758,11 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"[UTC]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"[UTC]"), @r###" Parsed { value: ParsedAnnotations { + input: "[UTC]", time_zone: Some( Named { critical: false, @@ -722,10 +772,11 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r###" Parsed { value: ParsedAnnotations { + input: "[.._foo_../.0+-]", time_zone: Some( Named { critical: false, @@ -735,16 +786,17 @@ mod tests { }, input: "", } - "#); + "###); } #[test] fn ok_offset() { let p = |input| Parser::new().parse(input).unwrap(); - insta::assert_debug_snapshot!(p(b"[-00]"), @r#" + insta::assert_debug_snapshot!(p(b"[-00]"), @r###" Parsed { value: ParsedAnnotations { + input: "[-00]", time_zone: Some( Offset { critical: false, @@ -758,10 +810,11 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"[+00]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"[+00]"), @r###" Parsed { value: ParsedAnnotations { + input: "[+00]", time_zone: Some( Offset { critical: false, @@ -775,10 +828,11 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"[-05]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"[-05]"), @r###" Parsed { value: ParsedAnnotations { + input: "[-05]", time_zone: Some( Offset { critical: false, @@ -792,10 +846,11 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r###" Parsed { value: ParsedAnnotations { + input: "[!+05:12]", time_zone: Some( Offset { critical: true, @@ -809,7 +864,7 @@ mod tests { }, input: "", } - "#); + "###); } #[test] @@ -818,9 +873,10 @@ mod tests { insta::assert_debug_snapshot!( p(b"[America/New_York][u-ca=chinese-japanese-russian]"), - @r#" + @r###" Parsed { value: ParsedAnnotations { + input: "[America/New_York][u-ca=chinese-japanese-russian]", time_zone: Some( Named { critical: false, @@ -830,7 +886,7 @@ mod tests { }, input: "", } - "#, + "###, ); } @@ -838,11 +894,11 @@ mod tests { fn err_iana() { insta::assert_snapshot!( Parser::new().parse(b"[0/Foo]").unwrap_err(), - @"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead", + @r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###, ); insta::assert_snapshot!( Parser::new().parse(b"[Foo/0Bar]").unwrap_err(), - @"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `0` instead", + @r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###, ); } @@ -850,23 +906,23 @@ mod tests { fn err_offset() { insta::assert_snapshot!( Parser::new().parse(b"[+").unwrap_err(), - @"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input", + @r###"failed to parse hours in UTC numeric offset "+": expected two digit hour after sign, but found end of input"###, ); insta::assert_snapshot!( Parser::new().parse(b"[+26]").unwrap_err(), - @"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25", + @r###"failed to parse hours in UTC numeric offset "+26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###, ); insta::assert_snapshot!( Parser::new().parse(b"[-26]").unwrap_err(), - @"failed to parse hours in UTC numeric offset: hour in time zone offset is out of range: parameter 'hours' with value 26 is not in the required range of 0..=25", + @r###"failed to parse hours in UTC numeric offset "-26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###, ); insta::assert_snapshot!( Parser::new().parse(b"[+05:12:34]").unwrap_err(), - @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)", + @r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###, ); insta::assert_snapshot!( Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(), - @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)", + @r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###, ); } @@ -874,7 +930,7 @@ mod tests { fn err_critical_unsupported() { insta::assert_snapshot!( Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(), - @"found unsupported RFC 9557 annotation with the critical flag (`!`) set", + @r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###, ); } @@ -886,7 +942,7 @@ mod tests { ); insta::assert_snapshot!( Parser::new().parse(b"[&").unwrap_err(), - @"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `&` instead", + @r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "&" instead"###, ); insta::assert_snapshot!( Parser::new().parse(b"[Foo][").unwrap_err(), @@ -894,7 +950,7 @@ mod tests { ); insta::assert_snapshot!( Parser::new().parse(b"[Foo][&").unwrap_err(), - @"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found `&` instead", + @r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###, ); } @@ -902,27 +958,27 @@ mod tests { fn err_separator() { insta::assert_snapshot!( Parser::new().parse(b"[abc").unwrap_err(), - @"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead", + @"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead", ); insta::assert_snapshot!( Parser::new().parse(b"[_abc").unwrap_err(), - @"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead", + @"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead", ); insta::assert_snapshot!( Parser::new().parse(b"[abc^").unwrap_err(), - @"expected an `]` after parsing an RFC 9557 time zone annotation, but found `^` instead", + @r###"expected an ']' after parsing an RFC 9557 time zone annotation, but found "^" instead"###, ); insta::assert_snapshot!( Parser::new().parse(b"[Foo][abc").unwrap_err(), - @"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead", + @"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead", ); insta::assert_snapshot!( Parser::new().parse(b"[Foo][_abc").unwrap_err(), - @"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead", + @"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead", ); insta::assert_snapshot!( Parser::new().parse(b"[Foo][abc^").unwrap_err(), - @"expected an `=` after parsing an RFC 9557 annotation key, but found `^` instead", + @r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###, ); } @@ -938,11 +994,11 @@ mod tests { ); insta::assert_snapshot!( Parser::new().parse(b"[abc=^").unwrap_err(), - @"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `^` instead", + @r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "^" instead"###, ); insta::assert_snapshot!( Parser::new().parse(b"[abc=]").unwrap_err(), - @"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `]` instead", + @r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###, ); } @@ -950,11 +1006,11 @@ mod tests { fn err_close() { insta::assert_snapshot!( Parser::new().parse(b"[abc=123").unwrap_err(), - @"expected an `]` after parsing an RFC 9557 annotation key and value, but found end of input instead", + @"expected an ']' after parsing an RFC 9557 annotation key and value, but found end of input instead", ); insta::assert_snapshot!( Parser::new().parse(b"[abc=123*").unwrap_err(), - @"expected an `]` after parsing an RFC 9557 annotation key and value, but found `*` instead", + @r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###, ); } @@ -990,7 +1046,7 @@ mod tests { let p = |input| Parser::new().parse(input).unwrap_err(); insta::assert_snapshot!( p(b"[america/new_york][america/new_york]"), - @"expected an `=` after parsing an RFC 9557 annotation key, but found `/` instead (time zone annotations must come first)", + @"expected an '=' after parsing an RFC 9557 annotation key, but found / instead (time zone annotations must come first)", ); } } diff --git a/src/fmt/serde.rs b/src/fmt/serde.rs index f8fe871..dc0078f 100644 --- a/src/fmt/serde.rs +++ b/src/fmt/serde.rs @@ -1778,24 +1778,28 @@ pub mod unsigned_duration { fn parse_iso_or_friendly( bytes: &[u8], ) -> Result { - let Some((&byte, tail)) = bytes.split_first() else { - return Err(crate::Error::from( - crate::error::fmt::Error::HybridDurationEmpty, + if bytes.is_empty() { + return Err(crate::error::err!( + "an empty string is not a valid `std::time::Duration`, \ + expected either a ISO 8601 or Jiff's 'friendly' \ + format", )); - }; - let mut first = byte; + } + let mut first = bytes[0]; // N.B. Unsigned durations don't support negative durations (of // course), but we still check for it here so that we can defer to // the dedicated parsers. They will provide their own error messages. if first == b'+' || first == b'-' { - let Some(&byte) = tail.first() else { - return Err(crate::Error::from( - crate::error::fmt::Error::HybridDurationPrefix { - sign: first, - }, + if bytes.len() == 1 { + return Err(crate::error::err!( + "found nothing after sign `{sign}`, \ + which is not a valid `std::time::Duration`, \ + expected either a ISO 8601 or Jiff's 'friendly' \ + format", + sign = crate::util::escape::Byte(first), )); - }; - first = byte; + } + first = bytes[1]; } let dur = if first == b'P' || first == b'p' { crate::fmt::temporal::DEFAULT_SPAN_PARSER diff --git a/src/fmt/strtime/format.rs b/src/fmt/strtime/format.rs index 74cb1a8..5d5d9a5 100644 --- a/src/fmt/strtime/format.rs +++ b/src/fmt/strtime/format.rs @@ -1,19 +1,16 @@ use crate::{ - error::{ - fmt::strtime::{Error as E, FormatError as FE}, - ErrorContext, - }, + error::{err, ErrorContext}, fmt::{ strtime::{ month_name_abbrev, month_name_full, weekday_name_abbrev, weekday_name_full, BrokenDownTime, Config, Custom, Extension, Flag, }, - util::{FractionalFormatter, IntegerFormatter}, + util::{DecimalFormatter, FractionalFormatter}, Write, WriteExt, }, tz::Offset, - util::utf8, + util::{escape, utf8}, Error, }; @@ -42,7 +39,10 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { self.wtr.write_str("%")?; break; } - return Err(E::UnexpectedEndAfterPercent.into()); + return Err(err!( + "invalid format string, expected byte after '%', \ + but found end of format string", + )); } let orig = self.fmt; if let Err(err) = self.format_one() { @@ -61,92 +61,100 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { } fn format_one(&mut self) -> Result<(), Error> { - let failc = - |directive, colons| E::DirectiveFailure { directive, colons }; - let fail = |directive| failc(directive, 0); - // Parse extensions like padding/case options and padding width. let ext = self.parse_extension()?; match self.f() { - b'%' => self.wtr.write_str("%").context(fail(b'%')), - b'A' => self.fmt_weekday_full(&ext).context(fail(b'A')), - b'a' => self.fmt_weekday_abbrev(&ext).context(fail(b'a')), - b'B' => self.fmt_month_full(&ext).context(fail(b'B')), - b'b' => self.fmt_month_abbrev(&ext).context(fail(b'b')), - b'C' => self.fmt_century(&ext).context(fail(b'C')), - b'c' => self.fmt_datetime(&ext).context(fail(b'c')), - b'D' => self.fmt_american_date(&ext).context(fail(b'D')), - b'd' => self.fmt_day_zero(&ext).context(fail(b'd')), - b'e' => self.fmt_day_space(&ext).context(fail(b'e')), - b'F' => self.fmt_iso_date(&ext).context(fail(b'F')), - b'f' => self.fmt_fractional(&ext).context(fail(b'f')), - b'G' => self.fmt_iso_week_year(&ext).context(fail(b'G')), - b'g' => self.fmt_iso_week_year2(&ext).context(fail(b'g')), - b'H' => self.fmt_hour24_zero(&ext).context(fail(b'H')), - b'h' => self.fmt_month_abbrev(&ext).context(fail(b'b')), - b'I' => self.fmt_hour12_zero(&ext).context(fail(b'H')), - b'j' => self.fmt_day_of_year(&ext).context(fail(b'j')), - b'k' => self.fmt_hour24_space(&ext).context(fail(b'k')), - b'l' => self.fmt_hour12_space(&ext).context(fail(b'l')), - b'M' => self.fmt_minute(&ext).context(fail(b'M')), - b'm' => self.fmt_month(&ext).context(fail(b'm')), - b'N' => self.fmt_nanoseconds(&ext).context(fail(b'N')), - b'n' => self.fmt_literal("\n").context(fail(b'n')), - b'P' => self.fmt_ampm_lower(&ext).context(fail(b'P')), - b'p' => self.fmt_ampm_upper(&ext).context(fail(b'p')), + b'%' => self.wtr.write_str("%").context("%% failed")?, + b'A' => self.fmt_weekday_full(&ext).context("%A failed")?, + b'a' => self.fmt_weekday_abbrev(&ext).context("%a failed")?, + b'B' => self.fmt_month_full(&ext).context("%B failed")?, + b'b' => self.fmt_month_abbrev(&ext).context("%b failed")?, + b'C' => self.fmt_century(&ext).context("%C failed")?, + b'c' => self.fmt_datetime(&ext).context("%c failed")?, + b'D' => self.fmt_american_date(&ext).context("%D failed")?, + b'd' => self.fmt_day_zero(&ext).context("%d failed")?, + b'e' => self.fmt_day_space(&ext).context("%e failed")?, + b'F' => self.fmt_iso_date(&ext).context("%F failed")?, + b'f' => self.fmt_fractional(&ext).context("%f failed")?, + b'G' => self.fmt_iso_week_year(&ext).context("%G failed")?, + b'g' => self.fmt_iso_week_year2(&ext).context("%g failed")?, + b'H' => self.fmt_hour24_zero(&ext).context("%H failed")?, + b'h' => self.fmt_month_abbrev(&ext).context("%b failed")?, + b'I' => self.fmt_hour12_zero(&ext).context("%H failed")?, + b'j' => self.fmt_day_of_year(&ext).context("%j failed")?, + b'k' => self.fmt_hour24_space(&ext).context("%k failed")?, + b'l' => self.fmt_hour12_space(&ext).context("%l failed")?, + b'M' => self.fmt_minute(&ext).context("%M failed")?, + b'm' => self.fmt_month(&ext).context("%m failed")?, + b'N' => self.fmt_nanoseconds(&ext).context("%N failed")?, + b'n' => self.fmt_literal("\n").context("%n failed")?, + b'P' => self.fmt_ampm_lower(&ext).context("%P failed")?, + b'p' => self.fmt_ampm_upper(&ext).context("%p failed")?, b'Q' => match ext.colons { - 0 => self.fmt_iana_nocolon().context(fail(b'Q')), - 1 => self.fmt_iana_colon().context(failc(b'Q', 1)), - _ => return Err(E::ColonCount { directive: b'Q' }.into()), + 0 => self.fmt_iana_nocolon().context("%Q failed")?, + 1 => self.fmt_iana_colon().context("%:Q failed")?, + _ => { + return Err(err!( + "invalid number of `:` in `%Q` directive" + )) + } }, - b'q' => self.fmt_quarter(&ext).context(fail(b'q')), - b'R' => self.fmt_clock_nosecs(&ext).context(fail(b'R')), - b'r' => self.fmt_12hour_time(&ext).context(fail(b'r')), - b'S' => self.fmt_second(&ext).context(fail(b'S')), - b's' => self.fmt_timestamp(&ext).context(fail(b's')), - b'T' => self.fmt_clock_secs(&ext).context(fail(b'T')), - b't' => self.fmt_literal("\t").context(fail(b't')), - b'U' => self.fmt_week_sun(&ext).context(fail(b'U')), - b'u' => self.fmt_weekday_mon(&ext).context(fail(b'u')), - b'V' => self.fmt_week_iso(&ext).context(fail(b'V')), - b'W' => self.fmt_week_mon(&ext).context(fail(b'W')), - b'w' => self.fmt_weekday_sun(&ext).context(fail(b'w')), - b'X' => self.fmt_time(&ext).context(fail(b'X')), - b'x' => self.fmt_date(&ext).context(fail(b'x')), - b'Y' => self.fmt_year(&ext).context(fail(b'Y')), - b'y' => self.fmt_year2(&ext).context(fail(b'y')), - b'Z' => self.fmt_tzabbrev(&ext).context(fail(b'Z')), + b'q' => self.fmt_quarter(&ext).context("%q failed")?, + b'R' => self.fmt_clock_nosecs(&ext).context("%R failed")?, + b'r' => self.fmt_12hour_time(&ext).context("%r failed")?, + b'S' => self.fmt_second(&ext).context("%S failed")?, + b's' => self.fmt_timestamp(&ext).context("%s failed")?, + b'T' => self.fmt_clock_secs(&ext).context("%T failed")?, + b't' => self.fmt_literal("\t").context("%t failed")?, + b'U' => self.fmt_week_sun(&ext).context("%U failed")?, + b'u' => self.fmt_weekday_mon(&ext).context("%u failed")?, + b'V' => self.fmt_week_iso(&ext).context("%V failed")?, + b'W' => self.fmt_week_mon(&ext).context("%W failed")?, + b'w' => self.fmt_weekday_sun(&ext).context("%w failed")?, + b'X' => self.fmt_time(&ext).context("%X failed")?, + b'x' => self.fmt_date(&ext).context("%x failed")?, + b'Y' => self.fmt_year(&ext).context("%Y failed")?, + b'y' => self.fmt_year2(&ext).context("%y failed")?, + b'Z' => self.fmt_tzabbrev(&ext).context("%Z failed")?, b'z' => match ext.colons { - 0 => self.fmt_offset_nocolon().context(fail(b'z')), - 1 => self.fmt_offset_colon().context(failc(b'z', 1)), - 2 => self.fmt_offset_colon2().context(failc(b'z', 2)), - 3 => self.fmt_offset_colon3().context(failc(b'z', 3)), - _ => return Err(E::ColonCount { directive: b'z' }.into()), + 0 => self.fmt_offset_nocolon().context("%z failed")?, + 1 => self.fmt_offset_colon().context("%:z failed")?, + 2 => self.fmt_offset_colon2().context("%::z failed")?, + 3 => self.fmt_offset_colon3().context("%:::z failed")?, + _ => { + return Err(err!( + "invalid number of `:` in `%z` directive" + )) + } }, b'.' => { if !self.bump_fmt() { - return Err(E::UnexpectedEndAfterDot.into()); + return Err(err!( + "invalid format string, expected directive after '%.'", + )); } // Parse precision settings after the `.`, effectively // overriding any digits that came before it. let ext = Extension { width: self.parse_width()?, ..ext }; match self.f() { - b'f' => self - .fmt_dot_fractional(&ext) - .context(E::DirectiveFailureDot { directive: b'f' }), + b'f' => { + self.fmt_dot_fractional(&ext).context("%.f failed")? + } unk => { - return Err(Error::from( - E::UnknownDirectiveAfterDot { directive: unk }, + return Err(err!( + "found unrecognized directive %{unk} following %.", + unk = escape::Byte(unk), )); } } } unk => { - return Err(Error::from(E::UnknownDirective { - directive: unk, - })) + return Err(err!( + "found unrecognized specifier directive %{unk}", + unk = escape::Byte(unk), + )); } - }?; + } self.bump_fmt(); Ok(()) } @@ -192,17 +200,21 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// some remaining bytes to parse. #[cold] #[inline(never)] - fn utf8_decode_and_bump(&mut self) -> Result { + fn utf8_decode_and_bump(&mut self) -> Result { match utf8::decode(self.fmt).expect("non-empty fmt") { Ok(ch) => { self.fmt = &self.fmt[ch.len_utf8()..]; return Ok(ch); } - Err(err) if self.config.lenient => { - self.fmt = &self.fmt[err.len()..]; + Err(errant_bytes) if self.config.lenient => { + self.fmt = &self.fmt[errant_bytes.len()..]; return Ok(char::REPLACEMENT_CHARACTER); } - Err(_) => Err(FE::InvalidUtf8), + Err(errant_bytes) => Err(err!( + "found invalid UTF-8 byte {errant_bytes:?} in format \ + string (format strings must be valid UTF-8)", + errant_bytes = escape::Bytes(errant_bytes), + )), } } @@ -258,7 +270,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %P fn fmt_ampm_lower(&mut self, ext: &Extension) -> Result<(), Error> { - let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); + let hour = self + .tm + .hour_ranged() + .ok_or_else(|| err!("requires time to format AM/PM"))? + .get(); ext.write_str( Case::AsIs, if hour < 12 { "am" } else { "pm" }, @@ -268,7 +284,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %p fn fmt_ampm_upper(&mut self, ext: &Extension) -> Result<(), Error> { - let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); + let hour = self + .tm + .hour_ranged() + .ok_or_else(|| err!("requires time to format AM/PM"))? + .get(); // Manually specialize this case to avoid hitting `write_str_cold`. let s = if matches!(ext.flag, Some(Flag::Swapcase)) { if hour < 12 { @@ -319,11 +339,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let day = self .tm .day - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.day_ranged()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged())) + .ok_or_else(|| err!("requires date to format day"))? .get(); ext.write_int(b'0', Some(2), day, self.wtr) } @@ -333,18 +350,19 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let day = self .tm .day - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.day_ranged()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged())) + .ok_or_else(|| err!("requires date to format day"))? .get(); ext.write_int(b' ', Some(2), day, self.wtr) } /// %I fn fmt_hour12_zero(&mut self, ext: &Extension) -> Result<(), Error> { - let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); + let mut hour = self + .tm + .hour_ranged() + .ok_or_else(|| err!("requires time to format hour"))? + .get(); if hour == 0 { hour = 12; } else if hour > 12 { @@ -355,13 +373,21 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %H fn fmt_hour24_zero(&mut self, ext: &Extension) -> Result<(), Error> { - let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); + let hour = self + .tm + .hour_ranged() + .ok_or_else(|| err!("requires time to format hour"))? + .get(); ext.write_int(b'0', Some(2), hour, self.wtr) } /// %l fn fmt_hour12_space(&mut self, ext: &Extension) -> Result<(), Error> { - let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); + let mut hour = self + .tm + .hour_ranged() + .ok_or_else(|| err!("requires time to format hour"))? + .get(); if hour == 0 { hour = 12; } else if hour > 12 { @@ -372,7 +398,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %k fn fmt_hour24_space(&mut self, ext: &Extension) -> Result<(), Error> { - let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); + let hour = self + .tm + .hour_ranged() + .ok_or_else(|| err!("requires time to format hour"))? + .get(); ext.write_int(b' ', Some(2), hour, self.wtr) } @@ -388,7 +418,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %M fn fmt_minute(&mut self, ext: &Extension) -> Result<(), Error> { - let minute = self.tm.minute.ok_or(FE::RequiresTime)?.get(); + let minute = self + .tm + .minute + .ok_or_else(|| err!("requires time to format minute"))? + .get(); ext.write_int(b'0', Some(2), minute, self.wtr) } @@ -397,11 +431,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.month_ranged()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) + .ok_or_else(|| err!("requires date to format month"))? .get(); ext.write_int(b'0', Some(2), month, self.wtr) } @@ -411,11 +442,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.month_ranged()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) + .ok_or_else(|| err!("requires date to format month"))?; ext.write_str(Case::AsIs, month_name_full(month), self.wtr) } @@ -424,18 +452,20 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.month_ranged()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) + .ok_or_else(|| err!("requires date to format month"))?; ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr) } /// %Q fn fmt_iana_nocolon(&mut self) -> Result<(), Error> { let Some(iana) = self.tm.iana_time_zone() else { - let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?; + let offset = self.tm.offset.ok_or_else(|| { + err!( + "requires IANA time zone identifier or time \ + zone offset, but none were present" + ) + })?; return write_offset(offset, false, true, false, &mut self.wtr); }; self.wtr.write_str(iana)?; @@ -445,7 +475,12 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %:Q fn fmt_iana_colon(&mut self) -> Result<(), Error> { let Some(iana) = self.tm.iana_time_zone() else { - let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?; + let offset = self.tm.offset.ok_or_else(|| { + err!( + "requires IANA time zone identifier or time \ + zone offset, but none were present" + ) + })?; return write_offset(offset, true, true, false, &mut self.wtr); }; self.wtr.write_str(iana)?; @@ -454,44 +489,62 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %z fn fmt_offset_nocolon(&mut self) -> Result<(), Error> { - let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; + let offset = self.tm.offset.ok_or_else(|| { + err!("requires offset to format time zone offset") + })?; write_offset(offset, false, true, false, self.wtr) } /// %:z fn fmt_offset_colon(&mut self) -> Result<(), Error> { - let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; + let offset = self.tm.offset.ok_or_else(|| { + err!("requires offset to format time zone offset") + })?; write_offset(offset, true, true, false, self.wtr) } /// %::z fn fmt_offset_colon2(&mut self) -> Result<(), Error> { - let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; + let offset = self.tm.offset.ok_or_else(|| { + err!("requires offset to format time zone offset") + })?; write_offset(offset, true, true, true, self.wtr) } /// %:::z fn fmt_offset_colon3(&mut self) -> Result<(), Error> { - let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; + let offset = self.tm.offset.ok_or_else(|| { + err!("requires offset to format time zone offset") + })?; write_offset(offset, true, false, false, self.wtr) } /// %S fn fmt_second(&mut self, ext: &Extension) -> Result<(), Error> { - let second = self.tm.second.ok_or(FE::RequiresTime)?.get(); + let second = self + .tm + .second + .ok_or_else(|| err!("requires time to format second"))? + .get(); ext.write_int(b'0', Some(2), second, self.wtr) } /// %s fn fmt_timestamp(&mut self, ext: &Extension) -> Result<(), Error> { - let timestamp = - self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?; + let timestamp = self.tm.to_timestamp().map_err(|_| { + err!( + "requires instant (a date, time and offset) \ + to format Unix timestamp", + ) + })?; ext.write_int(b' ', None, timestamp.as_second(), self.wtr) } /// %f fn fmt_fractional(&mut self, ext: &Extension) -> Result<(), Error> { - let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?; + let subsec = self.tm.subsec.ok_or_else(|| { + err!("requires time to format subsecond nanoseconds") + })?; let subsec = i32::from(subsec).unsigned_abs(); // For %f, we always want to emit at least one digit. The only way we // wouldn't is if our fractional component is zero. One exception to @@ -500,7 +553,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { // but this seems very odd. And an empty string cannot be parsed by // `%f`. if ext.width == Some(0) { - return Err(Error::from(FE::ZeroPrecisionFloat)); + return Err(err!("zero precision with %f is not allowed")); } if subsec == 0 && ext.width.is_none() { self.wtr.write_str("0")?; @@ -524,9 +577,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %N fn fmt_nanoseconds(&mut self, ext: &Extension) -> Result<(), Error> { - let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?; + let subsec = self.tm.subsec.ok_or_else(|| { + err!("requires time to format subsecond nanoseconds") + })?; if ext.width == Some(0) { - return Err(Error::from(FE::ZeroPrecisionNano)); + return Err(err!("zero precision with %N is not allowed")); } let subsec = i32::from(subsec).unsigned_abs(); // Since `%N` is actually an alias for `%9f`, when the precision @@ -541,8 +596,14 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { /// %Z fn fmt_tzabbrev(&mut self, ext: &Extension) -> Result<(), Error> { - let tz = self.tm.tz.as_ref().ok_or(FE::RequiresTimeZone)?; - let ts = self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?; + let tz = + self.tm.tz.as_ref().ok_or_else(|| { + err!("requires time zone in broken down time") + })?; + let ts = self + .tm + .to_timestamp() + .context("requires timestamp in broken down time")?; let oinfo = tz.to_offset_info(ts); ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr) } @@ -552,11 +613,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.weekday()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .ok_or_else(|| err!("requires date to format weekday"))?; ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr) } @@ -565,11 +623,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.weekday()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .ok_or_else(|| err!("requires date to format weekday"))?; ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr) } @@ -578,11 +633,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.weekday()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .ok_or_else(|| err!("requires date to format weekday number"))?; ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr) } @@ -591,11 +643,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.weekday()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .ok_or_else(|| err!("requires date to format weekday number"))?; ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr) } @@ -609,19 +658,17 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { .tm .day_of_year .map(|day| day.get()) - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.day_of_year()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year())) + .ok_or_else(|| { + err!("requires date to format Sunday-based week number") + })?; let weekday = self .tm .weekday - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.weekday()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .ok_or_else(|| { + err!("requires date to format Sunday-based week number") + })? .to_sunday_zero_offset(); // Example: 2025-01-05 is the first Sunday in 2025, and thus the start // of week 1. This means that 2025-01-04 (Saturday) is in week 0. @@ -637,16 +684,12 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weeknum = self .tm .iso_week - .or_else( - #[inline(never)] - || { - self.tm - .to_date() - .ok() - .map(|d| d.iso_week_date().week_ranged()) - }, - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| { + self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged()) + }) + .ok_or_else(|| { + err!("requires date to format ISO 8601 week number") + })?; ext.write_int(b'0', Some(2), weeknum, self.wtr) } @@ -660,19 +703,17 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { .tm .day_of_year .map(|day| day.get()) - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.day_of_year()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year())) + .ok_or_else(|| { + err!("requires date to format Monday-based week number") + })?; let weekday = self .tm .weekday - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.weekday()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .ok_or_else(|| { + err!("requires date to format Monday-based week number") + })? .to_sunday_zero_offset(); // Example: 2025-01-06 is the first Monday in 2025, and thus the start // of week 1. This means that 2025-01-05 (Sunday) is in week 0. @@ -688,11 +729,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .year - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.year_ranged()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) + .ok_or_else(|| err!("requires date to format year"))? .get(); ext.write_int(b'0', Some(4), year, self.wtr) } @@ -702,11 +740,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .year - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.year_ranged()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) + .ok_or_else(|| err!("requires date to format year (2-digit)"))? .get(); let year = year % 100; ext.write_int(b'0', Some(2), year, self.wtr) @@ -717,11 +752,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .year - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.year_ranged()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) + .ok_or_else(|| err!("requires date to format century (2-digit)"))? .get(); let century = year / 100; ext.write_int(b' ', None, century, self.wtr) @@ -732,16 +764,12 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .iso_week_year - .or_else( - #[inline(never)] - || { - self.tm - .to_date() - .ok() - .map(|d| d.iso_week_date().year_ranged()) - }, - ) - .ok_or(FE::RequiresDate)? + .or_else(|| { + self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged()) + }) + .ok_or_else(|| { + err!("requires date to format ISO 8601 week-based year") + })? .get(); ext.write_int(b'0', Some(4), year, self.wtr) } @@ -751,16 +779,15 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .iso_week_year - .or_else( - #[inline(never)] - || { - self.tm - .to_date() - .ok() - .map(|d| d.iso_week_date().year_ranged()) - }, - ) - .ok_or(FE::RequiresDate)? + .or_else(|| { + self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged()) + }) + .ok_or_else(|| { + err!( + "requires date to format \ + ISO 8601 week-based year (2-digit)" + ) + })? .get(); let year = year % 100; ext.write_int(b'0', Some(2), year, self.wtr) @@ -771,11 +798,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.month_ranged()), - ) - .ok_or(FE::RequiresDate)? + .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) + .ok_or_else(|| err!("requires date to format quarter"))? .get(); let quarter = match month { 1..=3 => 1, @@ -793,11 +817,8 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { .tm .day_of_year .map(|day| day.get()) - .or_else( - #[inline(never)] - || self.tm.to_date().ok().map(|d| d.day_of_year()), - ) - .ok_or(FE::RequiresDate)?; + .or_else(|| self.tm.to_date().ok().map(|d| d.day_of_year())) + .ok_or_else(|| err!("requires date to format day of year"))?; ext.write_int(b'0', Some(3), day, self.wtr) } @@ -850,7 +871,7 @@ fn write_offset( second: bool, wtr: &mut W, ) -> Result<(), Error> { - static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); + static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); let hours = offset.part_hours_ranged().abs().get(); let minutes = offset.part_minutes_ranged().abs().get(); @@ -946,7 +967,7 @@ impl Extension { self.width.or(pad_width) }; - let mut formatter = IntegerFormatter::new().padding_byte(pad_byte); + let mut formatter = DecimalFormatter::new().padding_byte(pad_byte); if let Some(width) = pad_width { formatter = formatter.padding(width); } @@ -1487,7 +1508,7 @@ mod tests { let dt = date(2025, 1, 20).at(13, 9, 0, 0); insta::assert_snapshot!( f("%s", dt), - @"strftime formatting failed: %s failed: requires instant (a timestamp or a date, time and offset)", + @"strftime formatting failed: %s failed: requires instant (a date, time and offset) to format Unix timestamp", ); } @@ -1500,7 +1521,7 @@ mod tests { ); insta::assert_snapshot!( format(b"abc %F \xFFxyz", d).unwrap_err(), - @"strftime formatting failed: invalid format string, it must be valid UTF-8", + @r#"strftime formatting failed: found invalid UTF-8 byte "\xff" in format string (format strings must be valid UTF-8)"#, ); } diff --git a/src/fmt/strtime/mod.rs b/src/fmt/strtime/mod.rs index 6a2d1f6..9455dd0 100644 --- a/src/fmt/strtime/mod.rs +++ b/src/fmt/strtime/mod.rs @@ -85,7 +85,7 @@ use jiff::{civil::time, fmt::strtime}; let t = time(23, 59, 59, 0); assert_eq!( strtime::format("%Y", t).unwrap_err().to_string(), - "strftime formatting failed: %Y failed: requires date to format", + "strftime formatting failed: %Y failed: requires date to format year", ); ``` @@ -275,7 +275,7 @@ The following things are currently unsupported: use crate::{ civil::{Date, DateTime, ISOWeekDate, Time, Weekday}, - error::{fmt::strtime::Error as E, ErrorContext}, + error::{err, ErrorContext}, fmt::{ strtime::{format::Formatter, parse::Parser}, Write, @@ -555,7 +555,7 @@ impl Config { /// assert_eq!( /// tm.to_string("%F %z").unwrap_err().to_string(), /// "strftime formatting failed: %z failed: \ - /// requires time zone offset", + /// requires offset to format time zone offset", /// ); /// /// // Now enable lenient mode: @@ -946,9 +946,13 @@ impl BrokenDownTime { fn parse_mono(fmt: &[u8], inp: &[u8]) -> Result { let mut pieces = BrokenDownTime::default(); let mut p = Parser { fmt, inp, tm: &mut pieces }; - p.parse().context(E::FailedStrptime)?; + p.parse().context("strptime parsing failed")?; if !p.inp.is_empty() { - return Err(Error::from(E::unconsumed(p.inp))); + return Err(err!( + "strptime expects to consume the entire input, but \ + {remaining:?} remains unparsed", + remaining = escape::Bytes(p.inp), + )); } Ok(pieces) } @@ -1051,7 +1055,7 @@ impl BrokenDownTime { let mkoffset = util::parse::offseter(inp); let mut pieces = BrokenDownTime::default(); let mut p = Parser { fmt, inp, tm: &mut pieces }; - p.parse().context(E::FailedStrptime)?; + p.parse().context("strptime parsing failed")?; let remainder = mkoffset(p.inp); Ok((pieces, remainder)) } @@ -1154,7 +1158,7 @@ impl BrokenDownTime { ) -> Result<(), Error> { let fmt = format.as_ref(); let mut formatter = Formatter { config, fmt, tm: self, wtr }; - formatter.format().context(E::FailedStrftime)?; + formatter.format().context("strftime formatting failed")?; Ok(()) } @@ -1333,11 +1337,10 @@ impl BrokenDownTime { /// )?.to_zoned(); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "datetime could not resolve to a timestamp since `reject` \ - /// conflict resolution was chosen, and because \ - /// datetime has offset `-05`, \ - /// but the time zone `US/Eastern` for the given datetime \ - /// unambiguously has offset `-04`", + /// "datetime 2024-07-14T21:14:00 could not resolve to a \ + /// timestamp since 'reject' conflict resolution was chosen, \ + /// and because datetime has offset -05, but the time zone \ + /// US/Eastern for the given datetime unambiguously has offset -04", /// ); /// /// # Ok::<(), Box>(()) @@ -1432,18 +1435,26 @@ impl BrokenDownTime { if let Some(ts) = self.timestamp { return Ok(ts.to_zoned(TimeZone::unknown())); } - Err(Error::from(E::ZonedOffsetOrTz)) + Err(err!( + "either offset (from %z) or IANA time zone identifier \ + (from %Q) is required for parsing zoned datetime", + )) } (Some(offset), None) => { let ts = match self.timestamp { Some(ts) => ts, None => { - let dt = self - .to_datetime() - .context(E::RequiredDateTimeForZoned)?; - let ts = offset - .to_timestamp(dt) - .context(E::RangeTimestamp)?; + let dt = self.to_datetime().context( + "datetime required to parse zoned datetime", + )?; + let ts = + offset.to_timestamp(dt).with_context(|| { + err!( + "parsed datetime {dt} and offset {offset}, \ + but combining them into a zoned datetime \ + is outside Jiff's supported timestamp range", + ) + })?; ts } }; @@ -1454,9 +1465,9 @@ impl BrokenDownTime { match self.timestamp { Some(ts) => Ok(ts.to_zoned(tz)), None => { - let dt = self - .to_datetime() - .context(E::RequiredDateTimeForZoned)?; + let dt = self.to_datetime().context( + "datetime required to parse zoned datetime", + )?; Ok(tz.to_zoned(dt)?) } } @@ -1467,17 +1478,19 @@ impl BrokenDownTime { Some(ts) => { let zdt = ts.to_zoned(tz); if zdt.offset() != offset { - return Err(Error::from(E::MismatchOffset { - parsed: offset, - got: zdt.offset(), - })); + return Err(err!( + "parsed time zone offset `{offset}`, but \ + offset from timestamp `{ts}` for time zone \ + `{iana}` is `{got}`", + got = zdt.offset(), + )); } Ok(zdt) } None => { - let dt = self - .to_datetime() - .context(E::RequiredDateTimeForZoned)?; + let dt = self.to_datetime().context( + "datetime required to parse zoned datetime", + )?; let azdt = OffsetConflict::Reject.resolve(dt, offset, tz)?; // Guaranteed that if OffsetConflict::Reject doesn't @@ -1571,20 +1584,31 @@ impl BrokenDownTime { /// ``` #[inline] pub fn to_timestamp(&self) -> Result { - // Previously, I had used this as the "fast path" and - // put the conversion code below into a cold unlineable - // function. But this "fast path" is actually the unusual - // case. It's rare to parse a timestamp (as an integer - // number of seconds since the Unix epoch) directly. - // So the code below, while bigger, is the common case. - // So it probably makes sense to keep it inlined. if let Some(timestamp) = self.timestamp() { return Ok(timestamp); } - let dt = - self.to_datetime().context(E::RequiredDateTimeForTimestamp)?; - let offset = self.offset.ok_or(E::RequiredOffsetForTimestamp)?; - offset.to_timestamp(dt).context(E::RangeTimestamp) + let dt = self + .to_datetime() + .context("datetime required to parse timestamp")?; + let offset = + self.to_offset().context("offset required to parse timestamp")?; + offset.to_timestamp(dt).with_context(|| { + err!( + "parsed datetime {dt} and offset {offset}, \ + but combining them into a timestamp is outside \ + Jiff's supported timestamp range", + ) + }) + } + + #[inline] + fn to_offset(&self) -> Result { + let Some(offset) = self.offset else { + return Err(err!( + "parsing format did not include time zone offset directive", + )); + }; + Ok(offset) } /// Extracts a civil datetime from this broken down time. @@ -1614,8 +1638,10 @@ impl BrokenDownTime { /// ``` #[inline] pub fn to_datetime(&self) -> Result { - let date = self.to_date().context(E::RequiredDateForDateTime)?; - let time = self.to_time().context(E::RequiredTimeForDateTime)?; + let date = + self.to_date().context("date required to parse datetime")?; + let time = + self.to_time().context("time required to parse datetime")?; Ok(DateTime::from_parts(date, time)) } @@ -1634,12 +1660,8 @@ impl BrokenDownTime { /// # Errors /// /// This returns an error if there weren't enough components to construct - /// a civil date, or if the components don't form into a valid date. This - /// means there must be at least a year and a way to determine the day of - /// the year. - /// - /// This will also return an error when there is a weekday component - /// set to a value inconsistent with the date returned. + /// a civil date. This means there must be at least a year and a way to + /// determine the day of the year. /// /// It's okay if there are more units than are needed to construct a civil /// datetime. For example, if this broken down time contains a civil time, @@ -1659,62 +1681,42 @@ impl BrokenDownTime { /// ``` #[inline] pub fn to_date(&self) -> Result { - #[cold] - #[inline(never)] - fn to_date(tm: &BrokenDownTime) -> Result { - let Some(year) = tm.year else { - // The Gregorian year and ISO week year may be parsed - // separately. That is, they are two different fields. So if - // the Gregorian year is absent, we might still have an ISO - // 8601 week date. - if let Some(date) = tm.to_date_from_iso()? { - return Ok(date); - } - return Err(Error::from(E::RequiredYearForDate)); - }; - let mut date = tm.to_date_from_gregorian(year)?; - if date.is_none() { - date = tm.to_date_from_iso()?; + let Some(year) = self.year else { + // The Gregorian year and ISO week year may be parsed separately. + // That is, they are two different fields. So if the Gregorian year + // is absent, we might still have an ISO 8601 week date. + if let Some(date) = self.to_date_from_iso()? { + return Ok(date); } - if date.is_none() { - date = tm.to_date_from_day_of_year(year)?; - } - if date.is_none() { - date = tm.to_date_from_week_sun(year)?; - } - if date.is_none() { - date = tm.to_date_from_week_mon(year)?; - } - let Some(date) = date else { - return Err(Error::from(E::RequiredSomeDayForDate)); - }; - if let Some(weekday) = tm.weekday { - if weekday != date.weekday() { - return Err(Error::from(E::MismatchWeekday { - parsed: weekday, - got: date.weekday(), - })); - } - } - Ok(date) - } - - // The common case is a simple Gregorian date. - // We put the rest behind a non-inlineable function - // to avoid code bloat for very uncommon cases. - let (Some(year), Some(month), Some(day)) = - (self.year, self.month, self.day) - else { - return to_date(self); + return Err(err!("missing year, date cannot be created")); + }; + let mut date = self.to_date_from_gregorian(year)?; + if date.is_none() { + date = self.to_date_from_iso()?; + } + if date.is_none() { + date = self.to_date_from_day_of_year(year)?; + } + if date.is_none() { + date = self.to_date_from_week_sun(year)?; + } + if date.is_none() { + date = self.to_date_from_week_mon(year)?; + } + let Some(date) = date else { + return Err(err!( + "a month/day, day-of-year or week date must be \ + present to create a date, but none were found", + )); }; - let date = - Date::new_ranged(year, month, day).context(E::InvalidDate)?; if let Some(weekday) = self.weekday { if weekday != date.weekday() { - return Err(Error::from(E::MismatchWeekday { - parsed: weekday, - got: date.weekday(), - })); + return Err(err!( + "parsed weekday {weekday} does not match \ + weekday {got} from parsed date {date}", + weekday = weekday_name_full(weekday), + got = weekday_name_full(date.weekday()), + )); } } Ok(date) @@ -1728,7 +1730,7 @@ impl BrokenDownTime { let (Some(month), Some(day)) = (self.month, self.day) else { return Ok(None); }; - Ok(Some(Date::new_ranged(year, month, day).context(E::InvalidDate)?)) + Ok(Some(Date::new_ranged(year, month, day).context("invalid date")?)) } #[inline] @@ -1744,7 +1746,7 @@ impl BrokenDownTime { .with() .day_of_year(doy.get()) .build() - .context(E::InvalidDate)? + .context("invalid date")? })) } @@ -1755,8 +1757,8 @@ impl BrokenDownTime { else { return Ok(None); }; - let wd = - ISOWeekDate::new_ranged(y, w, d).context(E::InvalidISOWeekDate)?; + let wd = ISOWeekDate::new_ranged(y, w, d) + .context("invalid ISO 8601 week date")?; Ok(Some(wd.date())) } @@ -1771,20 +1773,28 @@ impl BrokenDownTime { let week = i16::from(week); let wday = i16::from(weekday.to_sunday_zero_offset()); let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto()) - .context(E::InvalidDate)?; + .context("invalid date")?; let first_sunday = first_of_year .nth_weekday_of_month(1, Weekday::Sunday) .map(|d| d.day_of_year()) - .context(E::InvalidDate)?; + .context("invalid date")?; let doy = if week == 0 { let days_before_first_sunday = 7 - wday; let doy = first_sunday .checked_sub(days_before_first_sunday) - .ok_or(E::InvalidWeekdaySunday { got: weekday })?; + .ok_or_else(|| { + err!( + "weekday `{weekday:?}` is not valid for \ + Sunday based week number `{week}` \ + in year `{year}`", + ) + })?; if doy == 0 { - return Err(Error::from(E::InvalidWeekdaySunday { - got: weekday, - })); + return Err(err!( + "weekday `{weekday:?}` is not valid for \ + Sunday based week number `{week}` \ + in year `{year}`", + )); } doy } else { @@ -1796,7 +1806,7 @@ impl BrokenDownTime { .with() .day_of_year(doy) .build() - .context(E::InvalidDate)?; + .context("invalid date")?; Ok(Some(date)) } @@ -1811,20 +1821,28 @@ impl BrokenDownTime { let week = i16::from(week); let wday = i16::from(weekday.to_monday_zero_offset()); let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto()) - .context(E::InvalidDate)?; + .context("invalid date")?; let first_monday = first_of_year .nth_weekday_of_month(1, Weekday::Monday) .map(|d| d.day_of_year()) - .context(E::InvalidDate)?; + .context("invalid date")?; let doy = if week == 0 { let days_before_first_monday = 7 - wday; let doy = first_monday .checked_sub(days_before_first_monday) - .ok_or(E::InvalidWeekdayMonday { got: weekday })?; + .ok_or_else(|| { + err!( + "weekday `{weekday:?}` is not valid for \ + Monday based week number `{week}` \ + in year `{year}`", + ) + })?; if doy == 0 { - return Err(Error::from(E::InvalidWeekdayMonday { - got: weekday, - })); + return Err(err!( + "weekday `{weekday:?}` is not valid for \ + Monday based week number `{week}` \ + in year `{year}`", + )); } doy } else { @@ -1836,7 +1854,7 @@ impl BrokenDownTime { .with() .day_of_year(doy) .build() - .context(E::InvalidDate)?; + .context("invalid date")?; Ok(Some(date)) } @@ -1918,28 +1936,52 @@ impl BrokenDownTime { pub fn to_time(&self) -> Result { let Some(hour) = self.hour_ranged() else { if self.minute.is_some() { - return Err(Error::from(E::MissingTimeHourForMinute)); + return Err(err!( + "parsing format did not include hour directive, \ + but did include minute directive (cannot have \ + smaller time units with bigger time units missing)", + )); } if self.second.is_some() { - return Err(Error::from(E::MissingTimeHourForSecond)); + return Err(err!( + "parsing format did not include hour directive, \ + but did include second directive (cannot have \ + smaller time units with bigger time units missing)", + )); } if self.subsec.is_some() { - return Err(Error::from(E::MissingTimeHourForFractional)); + return Err(err!( + "parsing format did not include hour directive, \ + but did include fractional second directive (cannot have \ + smaller time units with bigger time units missing)", + )); } return Ok(Time::midnight()); }; let Some(minute) = self.minute else { if self.second.is_some() { - return Err(Error::from(E::MissingTimeMinuteForSecond)); + return Err(err!( + "parsing format did not include minute directive, \ + but did include second directive (cannot have \ + smaller time units with bigger time units missing)", + )); } if self.subsec.is_some() { - return Err(Error::from(E::MissingTimeMinuteForFractional)); + return Err(err!( + "parsing format did not include minute directive, \ + but did include fractional second directive (cannot have \ + smaller time units with bigger time units missing)", + )); } return Ok(Time::new_ranged(hour, C(0), C(0), C(0))); }; let Some(second) = self.second else { if self.subsec.is_some() { - return Err(Error::from(E::MissingTimeSecondForFractional)); + return Err(err!( + "parsing format did not include second directive, \ + but did include fractional second directive (cannot have \ + smaller time units with bigger time units missing)", + )); } return Ok(Time::new_ranged(hour, minute, C(0), C(0))); }; @@ -2079,8 +2121,8 @@ impl BrokenDownTime { /// // An error only occurs when you try to extract a date: /// assert_eq!( /// tm.to_date().unwrap_err().to_string(), - /// "invalid date: number of days for `2023` is invalid, \ - /// must be in range `1..=365`", + /// "invalid date: day-of-year=366 is out of range \ + /// for year=2023, must be in range 1..=365", /// ); /// // But parsing a value that is always illegal will /// // result in an error: @@ -3437,7 +3479,7 @@ impl Extension { fn parse_flag<'i>( fmt: &'i [u8], ) -> Result<(Option, &'i [u8]), Error> { - let (&byte, tail) = fmt.split_first().unwrap(); + let byte = fmt[0]; let flag = match byte { b'_' => Flag::PadSpace, b'0' => Flag::PadZero, @@ -3446,12 +3488,15 @@ impl Extension { b'#' => Flag::Swapcase, _ => return Ok((None, fmt)), }; - if tail.is_empty() { - return Err(Error::from(E::ExpectedDirectiveAfterFlag { - flag: byte, - })); + let fmt = &fmt[1..]; + if fmt.is_empty() { + return Err(err!( + "expected to find specifier directive after flag \ + {byte:?}, but found end of format string", + byte = escape::Byte(byte), + )); } - Ok((Some(flag), tail)) + Ok((Some(flag), fmt)) } /// Parses an optional width that comes after a (possibly absent) flag and @@ -3475,10 +3520,16 @@ impl Extension { return Ok((None, fmt)); } let (digits, fmt) = util::parse::split(fmt, digits).unwrap(); - let width = util::parse::i64(digits).context(E::FailedWidth)?; - let width = u8::try_from(width).map_err(|_| E::RangeWidth)?; + let width = util::parse::i64(digits) + .context("failed to parse conversion specifier width")?; + let width = u8::try_from(width).map_err(|_| { + err!("{width} is too big, max is {max}", max = u8::MAX) + })?; if fmt.is_empty() { - return Err(Error::from(E::ExpectedDirectiveAfterWidth)); + return Err(err!( + "expected to find specifier directive after width \ + {width}, but found end of format string", + )); } Ok((Some(width), fmt)) } @@ -3498,7 +3549,10 @@ impl Extension { } let fmt = &fmt[usize::from(colons)..]; if colons > 0 && fmt.is_empty() { - return Err(Error::from(E::ExpectedDirectiveAfterColons)); + return Err(err!( + "expected to find specifier directive after {colons} colons, \ + but found end of format string", + )); } Ok((u8::try_from(colons).unwrap(), fmt)) } diff --git a/src/fmt/strtime/parse.rs b/src/fmt/strtime/parse.rs index a66dc0d..cecaeff 100644 --- a/src/fmt/strtime/parse.rs +++ b/src/fmt/strtime/parse.rs @@ -1,17 +1,15 @@ +use core::fmt::Write; + use crate::{ civil::Weekday, - error::{ - fmt::strtime::{Error as E, ParseError as PE}, - util::ParseIntError, - ErrorContext, - }, + error::{err, ErrorContext}, fmt::{ offset, strtime::{BrokenDownTime, Extension, Flag, Meridiem}, Parsed, }, util::{ - parse, + escape, parse, rangeint::{ri8, RFrom}, t::{self, C}, }, @@ -26,97 +24,109 @@ pub(super) struct Parser<'f, 'i, 't> { impl<'f, 'i, 't> Parser<'f, 'i, 't> { pub(super) fn parse(&mut self) -> Result<(), Error> { - let failc = - |directive, colons| E::DirectiveFailure { directive, colons }; - let fail = |directive| failc(directive, 0); - while !self.fmt.is_empty() { if self.f() != b'%' { self.parse_literal()?; continue; } if !self.bump_fmt() { - return Err(Error::from(E::UnexpectedEndAfterPercent)); + return Err(err!( + "invalid format string, expected byte after '%', \ + but found end of format string", + )); } // We don't check this for `%.` since that currently always // must lead to `%.f` which can actually parse the empty string! if self.inp.is_empty() && self.f() != b'.' { - return Err(Error::from(PE::ExpectedNonEmpty { - directive: self.f(), - })); + return Err(err!( + "expected non-empty input for directive %{directive}, \ + but found end of input", + directive = escape::Byte(self.f()), + )); } // Parse extensions like padding/case options and padding width. let ext = self.parse_extension()?; match self.f() { - b'%' => self.parse_percent().context(fail(b'%'))?, - b'A' => self.parse_weekday_full().context(fail(b'A'))?, - b'a' => self.parse_weekday_abbrev().context(fail(b'a'))?, - b'B' => self.parse_month_name_full().context(fail(b'B'))?, - b'b' => self.parse_month_name_abbrev().context(fail(b'b'))?, - b'C' => self.parse_century(ext).context(fail(b'C'))?, - b'D' => self.parse_american_date().context(fail(b'D'))?, - b'd' => self.parse_day(ext).context(fail(b'd'))?, - b'e' => self.parse_day(ext).context(fail(b'e'))?, - b'F' => self.parse_iso_date().context(fail(b'F'))?, - b'f' => self.parse_fractional(ext).context(fail(b'f'))?, - b'G' => self.parse_iso_week_year(ext).context(fail(b'G'))?, - b'g' => self.parse_iso_week_year2(ext).context(fail(b'g'))?, - b'H' => self.parse_hour24(ext).context(fail(b'H'))?, - b'h' => self.parse_month_name_abbrev().context(fail(b'h'))?, - b'I' => self.parse_hour12(ext).context(fail(b'I'))?, - b'j' => self.parse_day_of_year(ext).context(fail(b'j'))?, - b'k' => self.parse_hour24(ext).context(fail(b'k'))?, - b'l' => self.parse_hour12(ext).context(fail(b'l'))?, - b'M' => self.parse_minute(ext).context(fail(b'M'))?, - b'm' => self.parse_month(ext).context(fail(b'm'))?, - b'N' => self.parse_fractional(ext).context(fail(b'N'))?, - b'n' => self.parse_whitespace().context(fail(b'n'))?, - b'P' => self.parse_ampm().context(fail(b'P'))?, - b'p' => self.parse_ampm().context(fail(b'p'))?, + b'%' => self.parse_percent().context("%% failed")?, + b'A' => self.parse_weekday_full().context("%A failed")?, + b'a' => self.parse_weekday_abbrev().context("%a failed")?, + b'B' => self.parse_month_name_full().context("%B failed")?, + b'b' => self.parse_month_name_abbrev().context("%b failed")?, + b'C' => self.parse_century(ext).context("%C failed")?, + b'D' => self.parse_american_date().context("%D failed")?, + b'd' => self.parse_day(ext).context("%d failed")?, + b'e' => self.parse_day(ext).context("%e failed")?, + b'F' => self.parse_iso_date().context("%F failed")?, + b'f' => self.parse_fractional(ext).context("%f failed")?, + b'G' => self.parse_iso_week_year(ext).context("%G failed")?, + b'g' => self.parse_iso_week_year2(ext).context("%g failed")?, + b'H' => self.parse_hour24(ext).context("%H failed")?, + b'h' => self.parse_month_name_abbrev().context("%h failed")?, + b'I' => self.parse_hour12(ext).context("%I failed")?, + b'j' => self.parse_day_of_year(ext).context("%j failed")?, + b'k' => self.parse_hour24(ext).context("%k failed")?, + b'l' => self.parse_hour12(ext).context("%l failed")?, + b'M' => self.parse_minute(ext).context("%M failed")?, + b'm' => self.parse_month(ext).context("%m failed")?, + b'N' => self.parse_fractional(ext).context("%N failed")?, + b'n' => self.parse_whitespace().context("%n failed")?, + b'P' => self.parse_ampm().context("%P failed")?, + b'p' => self.parse_ampm().context("%p failed")?, b'Q' => match ext.colons { - 0 => self.parse_iana_nocolon().context(fail(b'Q'))?, - 1 => self.parse_iana_colon().context(failc(b'Q', 1))?, - _ => return Err(E::ColonCount { directive: b'Q' }.into()), + 0 => self.parse_iana_nocolon().context("%Q failed")?, + 1 => self.parse_iana_colon().context("%:Q failed")?, + _ => { + return Err(err!( + "invalid number of `:` in `%Q` directive" + )) + } }, - b'R' => self.parse_clock_nosecs().context(fail(b'R'))?, - b'S' => self.parse_second(ext).context(fail(b'S'))?, - b's' => self.parse_timestamp(ext).context(fail(b's'))?, - b'T' => self.parse_clock_secs().context(fail(b'T'))?, - b't' => self.parse_whitespace().context(fail(b't'))?, - b'U' => self.parse_week_sun(ext).context(fail(b'U'))?, - b'u' => self.parse_weekday_mon(ext).context(fail(b'u'))?, - b'V' => self.parse_week_iso(ext).context(fail(b'V'))?, - b'W' => self.parse_week_mon(ext).context(fail(b'W'))?, - b'w' => self.parse_weekday_sun(ext).context(fail(b'w'))?, - b'Y' => self.parse_year(ext).context(fail(b'Y'))?, - b'y' => self.parse_year2(ext).context(fail(b'y'))?, + b'R' => self.parse_clock_nosecs().context("%R failed")?, + b'S' => self.parse_second(ext).context("%S failed")?, + b's' => self.parse_timestamp(ext).context("%s failed")?, + b'T' => self.parse_clock_secs().context("%T failed")?, + b't' => self.parse_whitespace().context("%t failed")?, + b'U' => self.parse_week_sun(ext).context("%U failed")?, + b'u' => self.parse_weekday_mon(ext).context("%u failed")?, + b'V' => self.parse_week_iso(ext).context("%V failed")?, + b'W' => self.parse_week_mon(ext).context("%W failed")?, + b'w' => self.parse_weekday_sun(ext).context("%w failed")?, + b'Y' => self.parse_year(ext).context("%Y failed")?, + b'y' => self.parse_year2(ext).context("%y failed")?, b'z' => match ext.colons { - 0 => self.parse_offset_nocolon().context(fail(b'z'))?, - 1 => self.parse_offset_colon().context(failc(b'z', 1))?, - 2 => self.parse_offset_colon2().context(failc(b'z', 2))?, - 3 => self.parse_offset_colon3().context(failc(b'z', 3))?, - _ => return Err(E::ColonCount { directive: b'z' }.into()), + 0 => self.parse_offset_nocolon().context("%z failed")?, + 1 => self.parse_offset_colon().context("%:z failed")?, + 2 => self.parse_offset_colon2().context("%::z failed")?, + 3 => self.parse_offset_colon3().context("%:::z failed")?, + _ => { + return Err(err!( + "invalid number of `:` in `%z` directive" + )) + } }, b'c' => { - return Err(Error::from(PE::NotAllowedLocaleDateAndTime)) + return Err(err!("cannot parse locale date and time")); } b'r' => { - return Err(Error::from( - PE::NotAllowedLocaleTwelveHourClockTime, - )) + return Err(err!( + "cannot parse locale 12-hour clock time" + )); } b'X' => { - return Err(Error::from(PE::NotAllowedLocaleClockTime)) + return Err(err!("cannot parse locale clock time")); + } + b'x' => { + return Err(err!("cannot parse locale date")); } - b'x' => return Err(Error::from(PE::NotAllowedLocaleDate)), b'Z' => { - return Err(Error::from( - PE::NotAllowedTimeZoneAbbreviation, - )) + return Err(err!("cannot parse time zone abbreviations")); } b'.' => { if !self.bump_fmt() { - return Err(E::UnexpectedEndAfterDot.into()); + return Err(err!( + "invalid format string, expected directive \ + after '%.'", + )); } // Skip over any precision settings that might be here. // This is a specific special format supported by `%.f`. @@ -124,20 +134,23 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let ext = Extension { width, ..ext }; self.fmt = fmt; match self.f() { - b'f' => self.parse_dot_fractional(ext).context( - E::DirectiveFailureDot { directive: b'f' }, - )?, + b'f' => self + .parse_dot_fractional(ext) + .context("%.f failed")?, unk => { - return Err(Error::from( - E::UnknownDirectiveAfterDot { directive: unk }, + return Err(err!( + "found unrecognized directive %{unk} \ + following %.", + unk = escape::Byte(unk), )); } } } unk => { - return Err(Error::from(E::UnknownDirective { - directive: unk, - })); + return Err(err!( + "found unrecognized directive %{unk}", + unk = escape::Byte(unk), + )); } } } @@ -208,14 +221,18 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { while self.i().is_ascii_whitespace() && self.bump_input() {} } } else if self.inp.is_empty() { - return Err(Error::from(PE::ExpectedMatchLiteralEndOfInput { - expected: self.f(), - })); + return Err(err!( + "expected to match literal byte {byte:?} from \ + format string, but found end of input", + byte = escape::Byte(self.fmt[0]), + )); } else if self.f() != self.i() { - return Err(Error::from(PE::ExpectedMatchLiteralByte { - expected: self.fmt[0], - got: self.i(), - })); + return Err(err!( + "expected to match literal byte {expect:?} from \ + format string, but found byte {found:?} in input", + expect = escape::Byte(self.f()), + found = escape::Byte(self.i()), + )); } else { self.bump_input(); } @@ -237,10 +254,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { /// Parses a literal '%' from the input. fn parse_percent(&mut self) -> Result<(), Error> { if self.i() != b'%' { - return Err(Error::from(PE::ExpectedMatchLiteralByte { - expected: b'%', - got: self.i(), - })); + return Err(err!( + "expected '%' due to '%%' in format string, \ + but found {byte:?} in input", + byte = escape::Byte(self.inp[0]), + )); } self.bump_fmt(); self.bump_input(); @@ -269,7 +287,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { 0 => Meridiem::AM, 1 => Meridiem::PM, // OK because 0 <= index <= 1. - _ => unreachable!("unknown AM/PM index"), + index => unreachable!("unknown AM/PM index {index}"), }); self.bump_fmt(); Ok(()) @@ -299,10 +317,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_day(&mut self, ext: Extension) -> Result<(), Error> { let (day, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseDay)?; + .context("failed to parse day")?; self.inp = inp; - let day = t::Day::try_new("day", day).context(PE::ParseDay)?; + let day = + t::Day::try_new("day", day).context("day number is invalid")?; self.tm.day = Some(day); self.bump_fmt(); Ok(()) @@ -314,11 +333,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_day_of_year(&mut self, ext: Extension) -> Result<(), Error> { let (day, inp) = ext .parse_number(3, Flag::PadZero, self.inp) - .context(PE::ParseDayOfYear)?; + .context("failed to parse day of year")?; self.inp = inp; let day = t::DayOfYear::try_new("day-of-year", day) - .context(PE::ParseDayOfYear)?; + .context("day of year number is invalid")?; self.tm.day_of_year = Some(day); self.bump_fmt(); Ok(()) @@ -328,10 +347,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_hour24(&mut self, ext: Extension) -> Result<(), Error> { let (hour, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseHour)?; + .context("failed to parse hour")?; self.inp = inp; - let hour = t::Hour::try_new("hour", hour).context(PE::ParseHour)?; + let hour = t::Hour::try_new("hour", hour) + .context("hour number is invalid")?; self.tm.hour = Some(hour); self.bump_fmt(); Ok(()) @@ -343,10 +363,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (hour, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseHour)?; + .context("failed to parse hour")?; self.inp = inp; - let hour = Hour12::try_new("hour", hour).context(PE::ParseHour)?; + let hour = + Hour12::try_new("hour", hour).context("hour number is invalid")?; self.tm.hour = Some(t::Hour::rfrom(hour)); self.bump_fmt(); Ok(()) @@ -365,11 +386,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_minute(&mut self, ext: Extension) -> Result<(), Error> { let (minute, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseMinute)?; + .context("failed to parse minute")?; self.inp = inp; - let minute = - t::Minute::try_new("minute", minute).context(PE::ParseMinute)?; + let minute = t::Minute::try_new("minute", minute) + .context("minute number is invalid")?; self.tm.minute = Some(minute); self.bump_fmt(); Ok(()) @@ -380,10 +401,9 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_iana_nocolon(&mut self) -> Result<(), Error> { #[cfg(not(feature = "alloc"))] { - Err(Error::from(PE::NotAllowedAlloc { - directive: b'Q', - colons: 0, - })) + Err(err!( + "cannot parse `%Q` without Jiff's `alloc` feature enabled" + )) } #[cfg(feature = "alloc")] { @@ -405,10 +425,9 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_iana_colon(&mut self) -> Result<(), Error> { #[cfg(not(feature = "alloc"))] { - Err(Error::from(PE::NotAllowedAlloc { - directive: b'Q', - colons: 1, - })) + Err(err!( + "cannot parse `%:Q` without Jiff's `alloc` feature enabled" + )) } #[cfg(feature = "alloc")] { @@ -504,7 +523,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_second(&mut self, ext: Extension) -> Result<(), Error> { let (mut second, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseSecond)?; + .context("failed to parse second")?; self.inp = inp; // As with other parses in Jiff, and like Temporal, @@ -513,8 +532,8 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { if second == 60 { second = 59; } - let second = - t::Second::try_new("second", second).context(PE::ParseSecond)?; + let second = t::Second::try_new("second", second) + .context("second number is invalid")?; self.tm.second = Some(second); self.bump_fmt(); Ok(()) @@ -526,14 +545,23 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (timestamp, inp) = ext // 19 comes from `i64::MAX.to_string().len()`. .parse_number(19, Flag::PadSpace, inp) - .context(PE::ParseTimestamp)?; + .context("failed to parse Unix timestamp (in seconds)")?; // I believe this error case is actually impossible. Since `timestamp` // is guaranteed to be positive, and negating any positive `i64` will // always result in a valid `i64`. + let timestamp = timestamp.checked_mul(sign).ok_or_else(|| { + err!( + "parsed Unix timestamp `{timestamp}` with a \ + leading `-` sign, which causes overflow", + ) + })?; let timestamp = - timestamp.checked_mul(sign).ok_or(PE::ParseTimestamp)?; - let timestamp = - Timestamp::from_second(timestamp).context(PE::ParseTimestamp)?; + Timestamp::from_second(timestamp).with_context(|| { + err!( + "parsed Unix timestamp `{timestamp}`, \ + but out of range of valid Jiff `Timestamp`", + ) + })?; self.inp = inp; self.tm.timestamp = Some(timestamp); @@ -557,20 +585,29 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { } let digits = mkdigits(self.inp); if digits.is_empty() { - return Err(Error::from(PE::ExpectedFractionalDigit)); + return Err(err!( + "expected at least one fractional decimal digit, \ + but did not find any", + )); } // I believe this error can never happen, since we know we have no more // than 9 ASCII digits. Any sequence of 9 ASCII digits can be parsed // into an `i64`. - let nanoseconds = - parse::fraction(digits).context(PE::ParseFractionalSeconds)?; + let nanoseconds = parse::fraction(digits).map_err(|err| { + err!( + "failed to parse {digits:?} as fractional second component \ + (up to 9 digits, nanosecond precision): {err}", + digits = escape::Bytes(digits), + ) + })?; // I believe this is also impossible to fail, since the maximal // fractional nanosecond is 999_999_999, and which also corresponds // to the maximal expressible number with 9 ASCII digits. So every // possible expressible value here is in range. let nanoseconds = - t::SubsecNanosecond::try_new("nanoseconds", nanoseconds) - .context(PE::ParseFractionalSeconds)?; + t::SubsecNanosecond::try_new("nanoseconds", nanoseconds).map_err( + |err| err!("fractional nanoseconds are not valid: {err}"), + )?; self.tm.subsec = Some(nanoseconds); self.bump_fmt(); Ok(()) @@ -592,11 +629,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_month(&mut self, ext: Extension) -> Result<(), Error> { let (month, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseMonth)?; + .context("failed to parse month")?; self.inp = inp; - let month = - t::Month::try_new("month", month).context(PE::ParseMonth)?; + let month = t::Month::try_new("month", month) + .context("month number is invalid")?; self.tm.month = Some(month); self.bump_fmt(); Ok(()) @@ -631,8 +668,8 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { b"December", ]; - let (index, inp) = - parse_choice(self.inp, CHOICES).context(PE::UnknownMonthName)?; + let (index, inp) = parse_choice(self.inp, CHOICES) + .context("unrecognized month name")?; self.inp = inp; // Both are OK because 0 <= index <= 11. @@ -668,7 +705,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { ]; let (index, inp) = parse_choice(self.inp, CHOICES) - .context(PE::UnknownWeekdayAbbreviation)?; + .context("unrecognized weekday abbreviation")?; self.inp = inp; // Both are OK because 0 <= index <= 6. @@ -684,13 +721,14 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> { let (weekday, inp) = ext .parse_number(1, Flag::NoPad, self.inp) - .context(PE::ParseWeekdayNumber)?; + .context("failed to parse weekday number")?; self.inp = inp; - let weekday = - i8::try_from(weekday).map_err(|_| PE::ParseWeekdayNumber)?; + let weekday = i8::try_from(weekday).map_err(|_| { + err!("parsed weekday number `{weekday}` is invalid") + })?; let weekday = Weekday::from_monday_one_offset(weekday) - .context(PE::ParseWeekdayNumber)?; + .context("weekday number is invalid")?; self.tm.weekday = Some(weekday); self.bump_fmt(); Ok(()) @@ -700,13 +738,14 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> { let (weekday, inp) = ext .parse_number(1, Flag::NoPad, self.inp) - .context(PE::ParseWeekdayNumber)?; + .context("failed to parse weekday number")?; self.inp = inp; - let weekday = - i8::try_from(weekday).map_err(|_| PE::ParseWeekdayNumber)?; + let weekday = i8::try_from(weekday).map_err(|_| { + err!("parsed weekday number `{weekday}` is invalid") + })?; let weekday = Weekday::from_sunday_zero_offset(weekday) - .context(PE::ParseWeekdayNumber)?; + .context("weekday number is invalid")?; self.tm.weekday = Some(weekday); self.bump_fmt(); Ok(()) @@ -717,11 +756,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_week_sun(&mut self, ext: Extension) -> Result<(), Error> { let (week, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseSundayWeekNumber)?; + .context("failed to parse Sunday-based week number")?; self.inp = inp; let week = t::WeekNum::try_new("week", week) - .context(PE::ParseSundayWeekNumber)?; + .context("Sunday-based week number is invalid")?; self.tm.week_sun = Some(week); self.bump_fmt(); Ok(()) @@ -731,11 +770,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_week_iso(&mut self, ext: Extension) -> Result<(), Error> { let (week, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseIsoWeekNumber)?; + .context("failed to parse ISO 8601 week number")?; self.inp = inp; let week = t::ISOWeek::try_new("week", week) - .context(PE::ParseIsoWeekNumber)?; + .context("ISO 8601 week number is invalid")?; self.tm.iso_week = Some(week); self.bump_fmt(); Ok(()) @@ -746,11 +785,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_week_mon(&mut self, ext: Extension) -> Result<(), Error> { let (week, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseMondayWeekNumber)?; + .context("failed to parse Monday-based week number")?; self.inp = inp; let week = t::WeekNum::try_new("week", week) - .context(PE::ParseMondayWeekNumber)?; + .context("Monday-based week number is invalid")?; self.tm.week_mon = Some(week); self.bump_fmt(); Ok(()) @@ -759,14 +798,16 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { /// Parses `%Y`, which we permit to be any year, including a negative year. fn parse_year(&mut self, ext: Extension) -> Result<(), Error> { let (sign, inp) = parse_optional_sign(self.inp); - let (year, inp) = - ext.parse_number(4, Flag::PadZero, inp).context(PE::ParseYear)?; + let (year, inp) = ext + .parse_number(4, Flag::PadZero, inp) + .context("failed to parse year")?; self.inp = inp; // OK because sign=={1,-1} and year can't be bigger than 4 digits // so overflow isn't possible. let year = sign.checked_mul(year).unwrap(); - let year = t::Year::try_new("year", year).context(PE::ParseYear)?; + let year = t::Year::try_new("year", year) + .context("year number is invalid")?; self.tm.year = Some(year); self.bump_fmt(); Ok(()) @@ -780,11 +821,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (year, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseYearTwoDigit)?; + .context("failed to parse 2-digit year")?; self.inp = inp; let year = Year2Digit::try_new("year (2 digits)", year) - .context(PE::ParseYearTwoDigit)?; + .context("year number is invalid")?; let mut year = t::Year::rfrom(year); if year <= C(68) { year += C(2000); @@ -800,12 +841,15 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { /// century. fn parse_century(&mut self, ext: Extension) -> Result<(), Error> { let (sign, inp) = parse_optional_sign(self.inp); - let (century, inp) = - ext.parse_number(2, Flag::NoPad, inp).context(PE::ParseCentury)?; + let (century, inp) = ext + .parse_number(2, Flag::NoPad, inp) + .context("failed to parse century")?; self.inp = inp; if !(0 <= century && century <= 99) { - return Err(Error::range("century", century, 0, 99)); + return Err(err!( + "century `{century}` is too big, must be in range 0-99", + )); } // OK because sign=={1,-1} and century can't be bigger than 2 digits @@ -815,7 +859,8 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { // 100 will never overflow. let year = century.checked_mul(100).unwrap(); // I believe the error condition here is impossible. - let year = t::Year::try_new("year", year).context(PE::ParseCentury)?; + let year = t::Year::try_new("year", year) + .context("year number (from century) is invalid")?; self.tm.year = Some(year); self.bump_fmt(); Ok(()) @@ -826,14 +871,14 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (sign, inp) = parse_optional_sign(self.inp); let (year, inp) = ext .parse_number(4, Flag::PadZero, inp) - .context(PE::ParseIsoWeekYear)?; + .context("failed to parse ISO 8601 week-based year")?; self.inp = inp; // OK because sign=={1,-1} and year can't be bigger than 4 digits // so overflow isn't possible. let year = sign.checked_mul(year).unwrap(); - let year = - t::ISOYear::try_new("year", year).context(PE::ParseIsoWeekYear)?; + let year = t::ISOYear::try_new("year", year) + .context("ISO 8601 week-based year number is invalid")?; self.tm.iso_week_year = Some(year); self.bump_fmt(); Ok(()) @@ -847,11 +892,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (year, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context(PE::ParseIsoWeekYearTwoDigit)?; + .context("failed to parse 2-digit ISO 8601 week-based year")?; self.inp = inp; let year = Year2Digit::try_new("year (2 digits)", year) - .context(PE::ParseIsoWeekYearTwoDigit)?; + .context("ISO 8601 week-based year number is invalid")?; let mut year = t::ISOYear::rfrom(year); if year <= C(68) { year += C(2000); @@ -919,10 +964,15 @@ impl Extension { n = n .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or(ParseIntError::TooBig)?; + .ok_or_else(|| { + err!( + "number '{}' too big to parse into 64-bit integer", + escape::Bytes(&inp[..digits]), + ) + })?; } if digits == 0 { - return Err(Error::from(ParseIntError::NoDigitsFound)); + return Err(err!("invalid number, no digits found")); } Ok((n, &inp[digits..])) } @@ -953,7 +1003,7 @@ fn parse_optional_sign<'i>(input: &'i [u8]) -> (i64, &'i [u8]) { /// The error includes the possible allowed choices. fn parse_choice<'i>( input: &'i [u8], - choices: &'static [&'static [u8]], + choices: &[&'static [u8]], ) -> Result<(usize, &'i [u8]), Error> { for (i, choice) in choices.into_iter().enumerate() { if input.len() < choice.len() { @@ -964,7 +1014,27 @@ fn parse_choice<'i>( return Ok((i, input)); } } - Err(Error::from(PE::ExpectedChoice { available: choices })) + #[cfg(feature = "alloc")] + { + let mut err = alloc::format!( + "failed to find expected choice at beginning of {input:?}, \ + available choices are: ", + input = escape::Bytes(input), + ); + for (i, choice) in choices.iter().enumerate() { + if i > 0 { + write!(err, ", ").unwrap(); + } + write!(err, "{}", escape::Bytes(choice)).unwrap(); + } + Err(Error::adhoc(err)) + } + #[cfg(not(feature = "alloc"))] + { + Err(err!( + "failed to find expected value from a set of allowed choices" + )) + } } /// Like `parse_choice`, but specialized for AM/PM. @@ -974,14 +1044,25 @@ fn parse_choice<'i>( #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_ampm<'i>(input: &'i [u8]) -> Result<(usize, &'i [u8]), Error> { if input.len() < 2 { - return Err(Error::from(PE::ExpectedAmPmTooShort)); + return Err(err!( + "expected to find AM or PM, \ + but the remaining input, {input:?}, is too short \ + to contain one", + input = escape::Bytes(input), + )); } let (x, input) = input.split_at(2); let candidate = &[x[0].to_ascii_lowercase(), x[1].to_ascii_lowercase()]; let index = match candidate { b"am" => 0, b"pm" => 1, - _ => return Err(Error::from(PE::ExpectedAmPm)), + _ => { + return Err(err!( + "expected to find AM or PM, but found \ + {candidate:?} instead", + candidate = escape::Bytes(x), + )) + } }; Ok((index, input)) } @@ -995,7 +1076,12 @@ fn parse_weekday_abbrev<'i>( input: &'i [u8], ) -> Result<(usize, &'i [u8]), Error> { if input.len() < 3 { - return Err(Error::from(PE::ExpectedWeekdayAbbreviationTooShort)); + return Err(err!( + "expected to find a weekday abbreviation, \ + but the remaining input, {input:?}, is too short \ + to contain one", + input = escape::Bytes(input), + )); } let (x, input) = input.split_at(3); let candidate = &[ @@ -1011,7 +1097,13 @@ fn parse_weekday_abbrev<'i>( b"thu" => 4, b"fri" => 5, b"sat" => 6, - _ => return Err(Error::from(PE::ExpectedWeekdayAbbreviation)), + _ => { + return Err(err!( + "expected to find weekday abbreviation, but found \ + {candidate:?} instead", + candidate = escape::Bytes(x), + )) + } }; Ok((index, input)) } @@ -1025,7 +1117,12 @@ fn parse_month_name_abbrev<'i>( input: &'i [u8], ) -> Result<(usize, &'i [u8]), Error> { if input.len() < 3 { - return Err(Error::from(PE::ExpectedMonthAbbreviationTooShort)); + return Err(err!( + "expected to find a month name abbreviation, \ + but the remaining input, {input:?}, is too short \ + to contain one", + input = escape::Bytes(input), + )); } let (x, input) = input.split_at(3); let candidate = &[ @@ -1046,7 +1143,13 @@ fn parse_month_name_abbrev<'i>( b"oct" => 9, b"nov" => 10, b"dec" => 11, - _ => return Err(Error::from(PE::ExpectedMonthAbbreviation)), + _ => { + return Err(err!( + "expected to find month name abbreviation, but found \ + {candidate:?} instead", + candidate = escape::Bytes(x), + )) + } }; Ok((index, input)) } @@ -1055,8 +1158,8 @@ fn parse_month_name_abbrev<'i>( fn parse_iana<'i>(input: &'i [u8]) -> Result<(&'i str, &'i [u8]), Error> { let mkiana = parse::slicer(input); let (_, mut input) = parse_iana_component(input)?; - while let Some(tail) = input.strip_prefix(b"/") { - input = tail; + while input.starts_with(b"/") { + input = &input[1..]; let (_, unconsumed) = parse_iana_component(input)?; input = unconsumed; } @@ -1077,10 +1180,17 @@ fn parse_iana_component<'i>( ) -> Result<(&'i [u8], &'i [u8]), Error> { let mkname = parse::slicer(input); if input.is_empty() { - return Err(Error::from(PE::ExpectedIanaTzEndOfInput)); + return Err(err!( + "expected the start of an IANA time zone identifier \ + name or component, but found end of input instead", + )); } if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') { - return Err(Error::from(PE::ExpectedIanaTz)); + return Err(err!( + "expected the start of an IANA time zone identifier \ + name or component, but found {:?} instead", + escape::Byte(input[0]), + )); } input = &input[1..]; @@ -1090,12 +1200,8 @@ fn parse_iana_component<'i>( b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', ) }; - loop { - let Some((&first, tail)) = input.split_first() else { break }; - if !is_iana_char(first) { - break; - } - input = tail; + while !input.is_empty() && is_iana_char(input[0]) { + input = &input[1..]; } Ok((mkname(input), input)) } @@ -1595,7 +1701,7 @@ mod tests { let p = |fmt: &str, input: &str| { BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) .unwrap() - .offset + .to_offset() .unwrap() }; @@ -1710,116 +1816,116 @@ mod tests { insta::assert_snapshot!( p("%M", ""), - @"strptime parsing failed: expected non-empty input for directive `%M`, but found end of input", + @"strptime parsing failed: expected non-empty input for directive %M, but found end of input", ); insta::assert_snapshot!( p("%M", "a"), - @"strptime parsing failed: %M failed: failed to parse minute number: invalid number, no digits found", + @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found", ); insta::assert_snapshot!( p("%M%S", "15"), - @"strptime parsing failed: expected non-empty input for directive `%S`, but found end of input", + @"strptime parsing failed: expected non-empty input for directive %S, but found end of input", ); insta::assert_snapshot!( p("%M%a", "Sun"), - @"strptime parsing failed: %M failed: failed to parse minute number: invalid number, no digits found", + @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found", ); insta::assert_snapshot!( p("%y", "999"), - @"strptime expects to consume the entire input, but `9` remains unparsed", + @r###"strptime expects to consume the entire input, but "9" remains unparsed"###, ); insta::assert_snapshot!( p("%Y", "-10000"), - @"strptime expects to consume the entire input, but `0` remains unparsed", + @r###"strptime expects to consume the entire input, but "0" remains unparsed"###, ); insta::assert_snapshot!( p("%Y", "10000"), - @"strptime expects to consume the entire input, but `0` remains unparsed", + @r###"strptime expects to consume the entire input, but "0" remains unparsed"###, ); insta::assert_snapshot!( p("%A %m/%d/%y", "Mon 7/14/24"), - @"strptime parsing failed: %A failed: unrecognized weekday abbreviation: failed to find expected value, available choices are: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday", + @r#"strptime parsing failed: %A failed: unrecognized weekday abbreviation: failed to find expected choice at beginning of "Mon 7/14/24", available choices are: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday"#, ); insta::assert_snapshot!( p("%b", "Bad"), - @"strptime parsing failed: %b failed: expected to find month name abbreviation", + @r###"strptime parsing failed: %b failed: expected to find month name abbreviation, but found "Bad" instead"###, ); insta::assert_snapshot!( p("%h", "July"), - @"strptime expects to consume the entire input, but `y` remains unparsed", + @r###"strptime expects to consume the entire input, but "y" remains unparsed"###, ); insta::assert_snapshot!( p("%B", "Jul"), - @"strptime parsing failed: %B failed: unrecognized month name: failed to find expected value, available choices are: January, February, March, April, May, June, July, August, September, October, November, December", + @r###"strptime parsing failed: %B failed: unrecognized month name: failed to find expected choice at beginning of "Jul", available choices are: January, February, March, April, May, June, July, August, September, October, November, December"###, ); insta::assert_snapshot!( p("%H", "24"), - @"strptime parsing failed: %H failed: failed to parse hour number: parameter 'hour' with value 24 is not in the required range of 0..=23", + @"strptime parsing failed: %H failed: hour number is invalid: parameter 'hour' with value 24 is not in the required range of 0..=23", ); insta::assert_snapshot!( p("%M", "60"), - @"strptime parsing failed: %M failed: failed to parse minute number: parameter 'minute' with value 60 is not in the required range of 0..=59", + @"strptime parsing failed: %M failed: minute number is invalid: parameter 'minute' with value 60 is not in the required range of 0..=59", ); insta::assert_snapshot!( p("%S", "61"), - @"strptime parsing failed: %S failed: failed to parse second number: parameter 'second' with value 61 is not in the required range of 0..=59", + @"strptime parsing failed: %S failed: second number is invalid: parameter 'second' with value 61 is not in the required range of 0..=59", ); insta::assert_snapshot!( p("%I", "0"), - @"strptime parsing failed: %I failed: failed to parse hour number: parameter 'hour' with value 0 is not in the required range of 1..=12", + @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 0 is not in the required range of 1..=12", ); insta::assert_snapshot!( p("%I", "13"), - @"strptime parsing failed: %I failed: failed to parse hour number: parameter 'hour' with value 13 is not in the required range of 1..=12", + @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 13 is not in the required range of 1..=12", ); insta::assert_snapshot!( p("%p", "aa"), - @"strptime parsing failed: %p failed: expected to find `AM` or `PM`", + @r###"strptime parsing failed: %p failed: expected to find AM or PM, but found "aa" instead"###, ); insta::assert_snapshot!( p("%_", " "), - @"strptime parsing failed: expected to find specifier directive after flag `_`, but found end of format string", + @r###"strptime parsing failed: expected to find specifier directive after flag "_", but found end of format string"###, ); insta::assert_snapshot!( p("%-", " "), - @"strptime parsing failed: expected to find specifier directive after flag `-`, but found end of format string", + @r###"strptime parsing failed: expected to find specifier directive after flag "-", but found end of format string"###, ); insta::assert_snapshot!( p("%0", " "), - @"strptime parsing failed: expected to find specifier directive after flag `0`, but found end of format string", + @r###"strptime parsing failed: expected to find specifier directive after flag "0", but found end of format string"###, ); insta::assert_snapshot!( p("%^", " "), - @"strptime parsing failed: expected to find specifier directive after flag `^`, but found end of format string", + @r###"strptime parsing failed: expected to find specifier directive after flag "^", but found end of format string"###, ); insta::assert_snapshot!( p("%#", " "), - @"strptime parsing failed: expected to find specifier directive after flag `#`, but found end of format string", + @r###"strptime parsing failed: expected to find specifier directive after flag "#", but found end of format string"###, ); insta::assert_snapshot!( p("%_1", " "), - @"strptime parsing failed: expected to find specifier directive after parsed width, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after width 1, but found end of format string", ); insta::assert_snapshot!( p("%_23", " "), - @"strptime parsing failed: expected to find specifier directive after parsed width, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after width 23, but found end of format string", ); insta::assert_snapshot!( p("%:", " "), - @"strptime parsing failed: expected to find specifier directive after colons, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after 1 colons, but found end of format string", ); insta::assert_snapshot!( p("%::", " "), - @"strptime parsing failed: expected to find specifier directive after colons, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after 2 colons, but found end of format string", ); insta::assert_snapshot!( p("%:::", " "), - @"strptime parsing failed: expected to find specifier directive after colons, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after 3 colons, but found end of format string", ); insta::assert_snapshot!( @@ -1832,15 +1938,15 @@ mod tests { ); insta::assert_snapshot!( p("%H:%M:%S%.f", "15:59:01.1234567891"), - @"strptime expects to consume the entire input, but `1` remains unparsed", + @r###"strptime expects to consume the entire input, but "1" remains unparsed"###, ); insta::assert_snapshot!( p("%H:%M:%S.%f", "15:59:01."), - @"strptime parsing failed: expected non-empty input for directive `%f`, but found end of input", + @"strptime parsing failed: expected non-empty input for directive %f, but found end of input", ); insta::assert_snapshot!( p("%H:%M:%S.%f", "15:59:01"), - @"strptime parsing failed: expected to match literal byte `.` from format string, but found end of input", + @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###, ); insta::assert_snapshot!( p("%H:%M:%S.%f", "15:59:01.a"), @@ -1848,11 +1954,11 @@ mod tests { ); insta::assert_snapshot!( p("%H:%M:%S.%N", "15:59:01."), - @"strptime parsing failed: expected non-empty input for directive `%N`, but found end of input", + @"strptime parsing failed: expected non-empty input for directive %N, but found end of input", ); insta::assert_snapshot!( p("%H:%M:%S.%N", "15:59:01"), - @"strptime parsing failed: expected to match literal byte `.` from format string, but found end of input", + @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###, ); insta::assert_snapshot!( p("%H:%M:%S.%N", "15:59:01.a"), @@ -1861,61 +1967,61 @@ mod tests { insta::assert_snapshot!( p("%Q", "+America/New_York"), - @"strptime parsing failed: %Q failed: failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): invalid digit, expected 0-9 but got A", + @r#"strptime parsing failed: %Q failed: failed to parse hours in UTC numeric offset "+America/New_York": failed to parse "Am" as hours (a two digit integer): invalid digit, expected 0-9 but got A"#, ); insta::assert_snapshot!( p("%Q", "-America/New_York"), - @"strptime parsing failed: %Q failed: failed to parse hours in UTC numeric offset: failed to parse hours (requires a two digit integer): invalid digit, expected 0-9 but got A", + @r#"strptime parsing failed: %Q failed: failed to parse hours in UTC numeric offset "-America/New_York": failed to parse "Am" as hours (a two digit integer): invalid digit, expected 0-9 but got A"#, ); insta::assert_snapshot!( p("%:Q", "+0400"), - @"strptime parsing failed: %:Q failed: parsed hour component of time zone offset, but could not find required colon separator", + @r#"strptime parsing failed: %:Q failed: parsed hour component of time zone offset from "+0400", but could not find required colon separator"#, ); insta::assert_snapshot!( p("%Q", "+04:00"), - @"strptime parsing failed: %Q failed: parsed hour component of time zone offset, but found colon after hours which is not allowed", + @r#"strptime parsing failed: %Q failed: parsed hour component of time zone offset from "+04:00", but found colon after hours which is not allowed"#, ); insta::assert_snapshot!( p("%Q", "America/"), - @"strptime parsing failed: %Q failed: expected to find the start of an IANA time zone identifier name or component, but found end of input instead", + @"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found end of input instead", ); insta::assert_snapshot!( p("%Q", "America/+"), - @"strptime parsing failed: %Q failed: expected to find the start of an IANA time zone identifier name or component", + @r###"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found "+" instead"###, ); insta::assert_snapshot!( p("%s", "-377705023202"), - @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): parameter 'second' with value -377705023202 is not in the required range of -377705023201..=253402207200", + @"strptime parsing failed: %s failed: parsed Unix timestamp `-377705023202`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value -377705023202 is not in the required range of -377705023201..=253402207200", ); insta::assert_snapshot!( p("%s", "253402207201"), - @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): parameter 'second' with value 253402207201 is not in the required range of -377705023201..=253402207200", + @"strptime parsing failed: %s failed: parsed Unix timestamp `253402207201`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value 253402207201 is not in the required range of -377705023201..=253402207200", ); insta::assert_snapshot!( p("%s", "-9999999999999999999"), - @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number too big to parse into 64-bit integer", + @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer", ); insta::assert_snapshot!( p("%s", "9999999999999999999"), - @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number too big to parse into 64-bit integer", + @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer", ); insta::assert_snapshot!( p("%u", "0"), - @"strptime parsing failed: %u failed: failed to parse weekday number: parameter 'weekday' with value 0 is not in the required range of 1..=7", + @"strptime parsing failed: %u failed: weekday number is invalid: parameter 'weekday' with value 0 is not in the required range of 1..=7", ); insta::assert_snapshot!( p("%w", "7"), - @"strptime parsing failed: %w failed: failed to parse weekday number: parameter 'weekday' with value 7 is not in the required range of 0..=6", + @"strptime parsing failed: %w failed: weekday number is invalid: parameter 'weekday' with value 7 is not in the required range of 0..=6", ); insta::assert_snapshot!( p("%u", "128"), - @"strptime expects to consume the entire input, but `28` remains unparsed", + @r###"strptime expects to consume the entire input, but "28" remains unparsed"###, ); insta::assert_snapshot!( p("%w", "128"), - @"strptime expects to consume the entire input, but `28` remains unparsed", + @r###"strptime expects to consume the entire input, but "28" remains unparsed"###, ); } @@ -1935,11 +2041,11 @@ mod tests { ); insta::assert_snapshot!( p("%m", "7"), - @"year required to parse date", + @"missing year, date cannot be created", ); insta::assert_snapshot!( p("%d", "25"), - @"year required to parse date", + @"missing year, date cannot be created", ); insta::assert_snapshot!( p("%Y-%m", "2024-7"), @@ -1951,7 +2057,7 @@ mod tests { ); insta::assert_snapshot!( p("%m-%d", "7-25"), - @"year required to parse date", + @"missing year, date cannot be created", ); insta::assert_snapshot!( @@ -1964,20 +2070,20 @@ mod tests { ); insta::assert_snapshot!( p("%a %m/%d/%y", "Mon 7/14/24"), - @"parsed weekday `Monday` does not match weekday `Sunday` from parsed date", + @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14", ); insta::assert_snapshot!( p("%A %m/%d/%y", "Monday 7/14/24"), - @"parsed weekday `Monday` does not match weekday `Sunday` from parsed date", + @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14", ); insta::assert_snapshot!( p("%Y-%U-%u", "2025-00-2"), - @"weekday `Tuesday` is not valid for Sunday based week number", + @"weekday `Tuesday` is not valid for Sunday based week number `0` in year `2025`", ); insta::assert_snapshot!( p("%Y-%W-%u", "2025-00-2"), - @"weekday `Tuesday` is not valid for Monday based week number", + @"weekday `Tuesday` is not valid for Monday based week number `0` in year `2025`", ); } @@ -2019,57 +2125,57 @@ mod tests { insta::assert_snapshot!( p("%z", "+05:30"), - @"strptime parsing failed: %z failed: parsed hour component of time zone offset, but found colon after hours which is not allowed", + @r#"strptime parsing failed: %z failed: parsed hour component of time zone offset from "+05:30", but found colon after hours which is not allowed"#, ); insta::assert_snapshot!( p("%:z", "+0530"), - @"strptime parsing failed: %:z failed: parsed hour component of time zone offset, but could not find required colon separator", + @r#"strptime parsing failed: %:z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#, ); insta::assert_snapshot!( p("%::z", "+0530"), - @"strptime parsing failed: %::z failed: parsed hour component of time zone offset, but could not find required colon separator", + @r#"strptime parsing failed: %::z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#, ); insta::assert_snapshot!( p("%:::z", "+0530"), - @"strptime parsing failed: %:::z failed: parsed hour component of time zone offset, but could not find required colon separator", + @r#"strptime parsing failed: %:::z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#, ); insta::assert_snapshot!( p("%z", "+05"), - @"strptime parsing failed: %z failed: parsed hour component of time zone offset, but could not find required minute component", + @r#"strptime parsing failed: %z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#, ); insta::assert_snapshot!( p("%:z", "+05"), - @"strptime parsing failed: %:z failed: parsed hour component of time zone offset, but could not find required minute component", + @r#"strptime parsing failed: %:z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#, ); insta::assert_snapshot!( p("%::z", "+05"), - @"strptime parsing failed: %::z failed: parsed hour component of time zone offset, but could not find required minute component", + @r#"strptime parsing failed: %::z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#, ); insta::assert_snapshot!( p("%::z", "+05:30"), - @"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset, but could not find required second component", + @r#"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset from "+05:30", but could not find required second component"#, ); insta::assert_snapshot!( p("%:::z", "+5"), - @"strptime parsing failed: %:::z failed: failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input", + @r#"strptime parsing failed: %:::z failed: failed to parse hours in UTC numeric offset "+5": expected two digit hour after sign, but found end of input"#, ); insta::assert_snapshot!( p("%z", "+0530:15"), - @"strptime expects to consume the entire input, but `:15` remains unparsed", + @r#"strptime expects to consume the entire input, but ":15" remains unparsed"#, ); insta::assert_snapshot!( p("%:z", "+05:3015"), - @"strptime expects to consume the entire input, but `15` remains unparsed", + @r#"strptime expects to consume the entire input, but "15" remains unparsed"#, ); insta::assert_snapshot!( p("%::z", "+05:3015"), - @"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset, but could not find required second component", + @r#"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset from "+05:3015", but could not find required second component"#, ); insta::assert_snapshot!( p("%:::z", "+05:3015"), - @"strptime expects to consume the entire input, but `15` remains unparsed", + @r#"strptime expects to consume the entire input, but "15" remains unparsed"#, ); } @@ -2086,7 +2192,7 @@ mod tests { insta::assert_snapshot!( p("%^50C%", "2000000000000000000#0077)()"), - @"strptime parsing failed: %C failed: parameter 'century' with value 2000000000000000000 is not in the required range of 0..=99", + @"strptime parsing failed: %C failed: century `2000000000000000000` is too big, must be in range 0-99", ); } } diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index 0e0cf43..15178e5 100644 --- a/src/fmt/temporal/mod.rs +++ b/src/fmt/temporal/mod.rs @@ -171,7 +171,7 @@ There is some more [background on Temporal's format] available. */ use crate::{ - civil::{self, ISOWeekDate}, + civil, error::Error, fmt::Write, span::Span, @@ -320,12 +320,13 @@ impl DateTimeParser { /// ); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "datetime could not resolve to timestamp since `reject` \ - /// conflict resolution was chosen, and because datetime \ - /// has offset `-05`, but the time zone `America/Indiana/Vevay` \ - /// for the given datetime falls in a gap (between offsets \ - /// `-05` and `-04`), and all offsets for a gap are \ - /// regarded as invalid", + /// "parsing \"2006-04-02T02:30-05[America/Indiana/Vevay]\" failed: \ + /// datetime 2006-04-02T02:30:00 could not resolve to timestamp \ + /// since 'reject' conflict resolution was chosen, and because \ + /// datetime has offset -05, but the time zone America/Indiana/Vevay \ + /// for the given datetime falls in a gap \ + /// (between offsets -05 and -04), \ + /// and all offsets for a gap are regarded as invalid", /// ); /// ``` /// @@ -409,10 +410,11 @@ impl DateTimeParser { /// ); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "datetime could not resolve to a timestamp since `reject` \ - /// conflict resolution was chosen, and because datetime has \ - /// offset `+00`, but the time zone `America/New_York` \ - /// for the given datetime unambiguously has offset `-04`", + /// "parsing \"2025-06-20T17:30+00[America/New_York]\" failed: \ + /// datetime 2025-06-20T17:30:00 could not resolve to a timestamp \ + /// since 'reject' conflict resolution was chosen, and because \ + /// datetime has offset +00, but the time zone America/New_York \ + /// for the given datetime unambiguously has offset -04", /// ); /// ``` /// @@ -1025,8 +1027,9 @@ impl DateTimeParser { /// // Normally this operation will fail. /// assert_eq!( /// PARSER.parse_zoned(timestamp).unwrap_err().to_string(), - /// "failed to find time zone annotation in square brackets, \ - /// which is required for parsing a zoned datetime", + /// "failed to find time zone in square brackets in \ + /// \"2025-01-02T15:13-05\", which is required for \ + /// parsing a zoned instant", /// ); /// /// // But you can work-around this with `Pieces`, which gives you direct @@ -1070,8 +1073,8 @@ impl DateTimeParser { /// /// assert_eq!( /// PARSER.parse_date("2024-03-10T00:00:00Z").unwrap_err().to_string(), - /// "cannot parse civil date/time from string with a Zulu offset, \ - /// parse as a `jiff::Timestamp` first and convert to a civil date/time instead", + /// "cannot parse civil date from string with a Zulu offset, \ + /// parse as a `Timestamp` and convert to a civil date instead", /// ); /// /// # Ok::<(), Box>(()) @@ -1107,22 +1110,6 @@ impl DateTimeParser { let pieces = parsed.to_pieces()?; Ok(pieces) } - - /// Parses an ISO 8601 week date. - /// - /// This isn't exported because it's not clear that it's worth it. - /// Moreover, this isn't part of the Temporal spec, so it's a little odd - /// to have it here. If this really needs to be exported, we probably need - /// a new module that wraps and re-uses this module's internal parser to - /// avoid too much code duplication. - pub(crate) fn parse_iso_week_date>( - &self, - input: I, - ) -> Result { - let input = input.as_ref(); - let wd = self.p.parse_iso_week_date(input)?.into_full()?; - Ok(wd) - } } /// A printer for Temporal datetimes. @@ -1977,25 +1964,6 @@ impl DateTimePrinter { ) -> Result<(), Error> { self.p.print_pieces(pieces, wtr) } - - /// Prints an ISO 8601 week date. - /// - /// This isn't exported because it's not clear that it's worth it. - /// Moreover, this isn't part of the Temporal spec, so it's a little odd - /// to have it here. But it's very convenient to have the ISO 8601 week - /// date parser in this module, and so we stick the printer here along - /// with it. - /// - /// Note that this printer will use `w` when `lowercase` is enabled. (It - /// isn't possible to enable this using the current Jiff public API. But - /// it's probably fine.) - pub(crate) fn print_iso_week_date( - &self, - iso_week_date: &ISOWeekDate, - wtr: W, - ) -> Result<(), Error> { - self.p.print_iso_week_date(iso_week_date, wtr) - } } /// A parser for Temporal durations. @@ -2487,7 +2455,7 @@ mod tests { ); insta::assert_snapshot!( DateTimeParser::new().parse_date("-000000-01-01").unwrap_err(), - @"failed to parse year in date: year zero must be written without a sign or a positive sign, but not a negative sign", + @r###"failed to parse year in date "-000000-01-01": year zero must be written without a sign or a positive sign, but not a negative sign"###, ); } diff --git a/src/fmt/temporal/parser.rs b/src/fmt/temporal/parser.rs index 6a6083d..9826f5e 100644 --- a/src/fmt/temporal/parser.rs +++ b/src/fmt/temporal/parser.rs @@ -1,6 +1,6 @@ use crate::{ - civil::{Date, DateTime, ISOWeekDate, Time, Weekday}, - error::{fmt::temporal::Error as E, Error, ErrorContext}, + civil::{Date, DateTime, Time}, + error::{err, Error, ErrorContext}, fmt::{ offset::{self, ParsedOffset}, rfc9557::{self, ParsedAnnotations}, @@ -67,7 +67,7 @@ impl<'i> ParsedDateTime<'i> { } #[cfg_attr(feature = "perf-inline", inline(always))] - fn to_ambiguous_zoned( + pub(super) fn to_ambiguous_zoned( &self, db: &TimeZoneDatabase, offset_conflict: OffsetConflict, @@ -76,10 +76,14 @@ impl<'i> ParsedDateTime<'i> { let dt = DateTime::from_parts(self.date.date, time); // We always require a time zone when parsing a zoned instant. - let tz_annotation = self - .annotations - .to_time_zone_annotation()? - .ok_or(E::MissingTimeZoneAnnotation)?; + let tz_annotation = + self.annotations.to_time_zone_annotation()?.ok_or_else(|| { + err!( + "failed to find time zone in square brackets \ + in {:?}, which is required for parsing a zoned instant", + self.input, + ) + })?; let tz = tz_annotation.to_time_zone_with(db)?; // If there's no offset, then our only choice, regardless of conflict @@ -98,7 +102,11 @@ impl<'i> ParsedDateTime<'i> { // likely result in `OffsetConflict::Reject` raising an error. // (Unless the actual correct offset at the time is `+00:00` for // the time zone parsed.) - return OffsetConflict::AlwaysOffset.resolve(dt, Offset::UTC, tz); + return OffsetConflict::AlwaysOffset + .resolve(dt, Offset::UTC, tz) + .with_context(|| { + err!("parsing {input:?} failed", input = self.input) + }); } let offset = parsed_offset.to_offset()?; let is_equal = |parsed: Offset, candidate: Offset| { @@ -129,30 +137,46 @@ impl<'i> ParsedDateTime<'i> { }; parsed == candidate }; - offset_conflict.resolve_with(dt, offset, tz, is_equal) + offset_conflict.resolve_with(dt, offset, tz, is_equal).with_context( + || err!("parsing {input:?} failed", input = self.input), + ) } #[cfg_attr(feature = "perf-inline", inline(always))] pub(super) fn to_timestamp(&self) -> Result { - let time = self - .time - .as_ref() - .map(|p| p.time) - .ok_or(E::MissingTimeInTimestamp)?; - let parsed_offset = - self.offset.as_ref().ok_or(E::MissingOffsetInTimestamp)?; + let time = self.time.as_ref().map(|p| p.time).ok_or_else(|| { + err!( + "failed to find time component in {:?}, \ + which is required for parsing a timestamp", + self.input, + ) + })?; + let parsed_offset = self.offset.as_ref().ok_or_else(|| { + err!( + "failed to find offset component in {:?}, \ + which is required for parsing a timestamp", + self.input, + ) + })?; let offset = parsed_offset.to_offset()?; let dt = DateTime::from_parts(self.date.date, time); - let timestamp = offset - .to_timestamp(dt) - .context(E::ConvertDateTimeToTimestamp { offset })?; + let timestamp = offset.to_timestamp(dt).with_context(|| { + err!( + "failed to convert civil datetime to timestamp \ + with offset {offset}", + ) + })?; Ok(timestamp) } #[cfg_attr(feature = "perf-inline", inline(always))] pub(super) fn to_datetime(&self) -> Result { if self.offset.as_ref().map_or(false, |o| o.is_zulu()) { - return Err(Error::from(E::CivilDateTimeZulu)); + return Err(err!( + "cannot parse civil date from string with a Zulu \ + offset, parse as a `Timestamp` and convert to a civil \ + datetime instead", + )); } Ok(DateTime::from_parts(self.date.date, self.time())) } @@ -160,7 +184,11 @@ impl<'i> ParsedDateTime<'i> { #[cfg_attr(feature = "perf-inline", inline(always))] pub(super) fn to_date(&self) -> Result { if self.offset.as_ref().map_or(false, |o| o.is_zulu()) { - return Err(Error::from(E::CivilDateTimeZulu)); + return Err(err!( + "cannot parse civil date from string with a Zulu \ + offset, parse as a `Timestamp` and convert to a civil \ + date instead", + )); } Ok(self.date.date) } @@ -244,11 +272,24 @@ impl<'i> ParsedTimeZone<'i> { ) -> Result { match self.kind { ParsedTimeZoneKind::Named(iana_name) => { - db.get(iana_name).context(E::FailedTzdbLookup) + let tz = db.get(iana_name).with_context(|| { + err!( + "parsed apparent IANA time zone identifier \ + {iana_name} from {input}, but the tzdb lookup \ + failed", + input = self.input, + ) + })?; + Ok(tz) } ParsedTimeZoneKind::Offset(poff) => { - let offset = - poff.to_offset().context(E::FailedOffsetNumeric)?; + let offset = poff.to_offset().with_context(|| { + err!( + "offset successfully parsed from {input}, \ + but failed to convert to numeric `Offset`", + input = self.input, + ) + })?; Ok(TimeZone::fixed(offset)) } #[cfg(feature = "alloc")] @@ -289,7 +330,7 @@ impl DateTimeParser { ) -> Result>, Error> { let mkslice = parse::slicer(input); let Parsed { value: date, input } = self.parse_date_spec(input)?; - let Some((&first, tail)) = input.split_first() else { + if input.is_empty() { let value = ParsedDateTime { input: escape::Bytes(mkslice(input)), date, @@ -298,11 +339,12 @@ impl DateTimeParser { annotations: ParsedAnnotations::none(), }; return Ok(Parsed { value, input }); - }; - let (time, offset, input) = if !matches!(first, b' ' | b'T' | b't') { + } + let (time, offset, input) = if !matches!(input[0], b' ' | b'T' | b't') + { (None, None, input) } else { - let input = tail; + let input = &input[1..]; // If there's a separator, then we must parse a time and we are // *allowed* to parse an offset. But without a separator, we don't // support offsets. Just annotations (which are parsed below). @@ -342,17 +384,20 @@ impl DateTimeParser { #[cfg_attr(feature = "perf-inline", inline(always))] pub(super) fn parse_temporal_time<'i>( &self, - input: &'i [u8], + mut input: &'i [u8], ) -> Result>, Error> { let mkslice = parse::slicer(input); - if let Some(input) = - input.strip_prefix(b"T").or_else(|| input.strip_prefix(b"t")) - { + if input.starts_with(b"T") || input.starts_with(b"t") { + input = &input[1..]; let Parsed { value: time, input } = self.parse_time_spec(input)?; let Parsed { value: offset, input } = self.parse_offset(input)?; if offset.map_or(false, |o| o.is_zulu()) { - return Err(Error::from(E::CivilDateTimeZulu)); + return Err(err!( + "cannot parse civil time from string with a Zulu \ + offset, parse as a `Timestamp` and convert to a civil \ + time instead", + )); } let Parsed { input, .. } = self.parse_annotations(input)?; return Ok(Parsed { value: time, input }); @@ -369,10 +414,18 @@ impl DateTimeParser { if let Ok(parsed) = self.parse_temporal_datetime(input) { let Parsed { value: dt, input } = parsed; if dt.offset.map_or(false, |o| o.is_zulu()) { - return Err(Error::from(E::CivilDateTimeZulu)); + return Err(err!( + "cannot parse plain time from full datetime string with a \ + Zulu offset, parse as a `Timestamp` and convert to a \ + plain time instead", + )); } let Some(time) = dt.time else { - return Err(Error::from(E::MissingTimeInDate)); + return Err(err!( + "successfully parsed date from {parsed:?}, but \ + no time component was found", + parsed = dt.input, + )); }; return Ok(Parsed { value: time, input }); } @@ -383,7 +436,11 @@ impl DateTimeParser { let Parsed { value: time, input } = self.parse_time_spec(input)?; let Parsed { value: offset, input } = self.parse_offset(input)?; if offset.map_or(false, |o| o.is_zulu()) { - return Err(Error::from(E::CivilDateTimeZulu)); + return Err(err!( + "cannot parse plain time from string with a Zulu \ + offset, parse as a `Timestamp` and convert to a plain \ + time instead", + )); } // The possible ambiguities occur with the time AND the // optional offset, so try to parse what we have so far as @@ -395,10 +452,18 @@ impl DateTimeParser { if !time.extended { let possibly_ambiguous = mkslice(input); if self.parse_month_day(possibly_ambiguous).is_ok() { - return Err(Error::from(E::AmbiguousTimeMonthDay)); + return Err(err!( + "parsed time from {parsed:?} is ambiguous \ + with a month-day date", + parsed = escape::Bytes(possibly_ambiguous), + )); } if self.parse_year_month(possibly_ambiguous).is_ok() { - return Err(Error::from(E::AmbiguousTimeYearMonth)); + return Err(err!( + "parsed time from {parsed:?} is ambiguous \ + with a year-month date", + parsed = escape::Bytes(possibly_ambiguous), + )); } } // OK... carry on. @@ -411,7 +476,9 @@ impl DateTimeParser { &self, mut input: &'i [u8], ) -> Result>, Error> { - let &first = input.first().ok_or(E::EmptyTimeZone)?; + let Some(first) = input.first().copied() else { + return Err(err!("an empty string is not a valid time zone")); + }; let original = escape::Bytes(input); if matches!(first, b'+' | b'-') { static P: offset::Parser = offset::Parser::new() @@ -428,8 +495,13 @@ impl DateTimeParser { // be an IANA time zone identifier. We do this in a couple // different cases below, hence the helper function. let mknamed = |consumed, remaining| { - let tzid = core::str::from_utf8(consumed) - .map_err(|_| E::InvalidTimeZoneUtf8)?; + let Ok(tzid) = core::str::from_utf8(consumed) else { + return Err(err!( + "found plausible IANA time zone identifier \ + {input:?}, but it is not valid UTF-8", + input = escape::Bytes(consumed), + )); + }; let kind = ParsedTimeZoneKind::Named(tzid); let value = ParsedTimeZone { input: original, kind }; Ok(Parsed { value, input: remaining }) @@ -449,12 +521,12 @@ impl DateTimeParser { let mkconsumed = parse::slicer(input); let mut saw_number = false; loop { - let Some((&byte, tail)) = input.split_first() else { break }; + let Some(byte) = input.first().copied() else { break }; if byte.is_ascii_whitespace() { break; } saw_number = saw_number || byte.is_ascii_digit(); - input = tail; + input = &input[1..]; } let consumed = mkconsumed(input); if !saw_number { @@ -462,7 +534,10 @@ impl DateTimeParser { } #[cfg(not(feature = "alloc"))] { - Err(Error::from(E::AllocPosixTimeZone)) + Err(err!( + "cannot parsed time zones other than fixed offsets \ + without the `alloc` crate feature enabled", + )) } #[cfg(feature = "alloc")] { @@ -484,50 +559,6 @@ impl DateTimeParser { } } - /// Parses an ISO 8601 week date. - /// - /// Note that this isn't part of the Temporal ISO 8601 spec. We put it here - /// because it shares a fair bit of code with parsing regular ISO 8601 - /// dates. - #[cfg_attr(feature = "perf-inline", inline(always))] - pub(super) fn parse_iso_week_date<'i>( - &self, - input: &'i [u8], - ) -> Result, Error> { - // Parse year component. - let Parsed { value: year, input } = - self.parse_year(input).context(E::FailedYearInDate)?; - let extended = input.starts_with(b"-"); - - // Parse optional separator. - let Parsed { input, .. } = self - .parse_date_separator(input, extended) - .context(E::FailedSeparatorAfterYear)?; - - // Parse 'W' prefix before week num. - let Parsed { input, .. } = self - .parse_week_prefix(input) - .context(E::FailedWeekNumberPrefixInDate)?; - - // Parse week num component. - let Parsed { value: week, input } = - self.parse_week_num(input).context(E::FailedWeekNumberInDate)?; - - // Parse optional separator. - let Parsed { input, .. } = self - .parse_date_separator(input, extended) - .context(E::FailedSeparatorAfterWeekNumber)?; - - // Parse day component. - let Parsed { value: weekday, input } = - self.parse_weekday(input).context(E::FailedWeekdayInDate)?; - - let iso_week_date = ISOWeekDate::new_ranged(year, week, weekday) - .context(E::InvalidWeekDate)?; - - Ok(Parsed { value: iso_week_date, input: input }) - } - // Date ::: // DateYear - DateMonth - DateDay // DateYear DateMonth DateDay @@ -537,32 +568,40 @@ impl DateTimeParser { input: &'i [u8], ) -> Result>, Error> { let mkslice = parse::slicer(input); + let original = escape::Bytes(input); // Parse year component. let Parsed { value: year, input } = - self.parse_year(input).context(E::FailedYearInDate)?; + self.parse_year(input).with_context(|| { + err!("failed to parse year in date {original:?}") + })?; let extended = input.starts_with(b"-"); // Parse optional separator. let Parsed { input, .. } = self .parse_date_separator(input, extended) - .context(E::FailedSeparatorAfterYear)?; + .context("failed to parse separator after year")?; // Parse month component. let Parsed { value: month, input } = - self.parse_month(input).context(E::FailedMonthInDate)?; + self.parse_month(input).with_context(|| { + err!("failed to parse month in date {original:?}") + })?; // Parse optional separator. let Parsed { input, .. } = self .parse_date_separator(input, extended) - .context(E::FailedSeparatorAfterMonth)?; + .context("failed to parse separator after month")?; // Parse day component. let Parsed { value: day, input } = - self.parse_day(input).context(E::FailedDayInDate)?; + self.parse_day(input).with_context(|| { + err!("failed to parse day in date {original:?}") + })?; - let date = - Date::new_ranged(year, month, day).context(E::InvalidDate)?; + let date = Date::new_ranged(year, month, day).with_context(|| { + err!("date parsed from {original:?} is not valid") + })?; let value = ParsedDate { input: escape::Bytes(mkslice(input)), date }; Ok(Parsed { value, input }) } @@ -579,10 +618,13 @@ impl DateTimeParser { input: &'i [u8], ) -> Result>, Error> { let mkslice = parse::slicer(input); + let original = escape::Bytes(input); // Parse hour component. let Parsed { value: hour, input } = - self.parse_hour(input).context(E::FailedHourInTime)?; + self.parse_hour(input).with_context(|| { + err!("failed to parse hour in time {original:?}") + })?; let extended = input.starts_with(b":"); // Parse optional minute component. @@ -603,7 +645,9 @@ impl DateTimeParser { return Ok(Parsed { value, input }); } let Parsed { value: minute, input } = - self.parse_minute(input).context(E::FailedMinuteInTime)?; + self.parse_minute(input).with_context(|| { + err!("failed to parse minute in time {original:?}") + })?; // Parse optional second component. let Parsed { value: has_second, input } = @@ -623,12 +667,18 @@ impl DateTimeParser { return Ok(Parsed { value, input }); } let Parsed { value: second, input } = - self.parse_second(input).context(E::FailedSecondInTime)?; + self.parse_second(input).with_context(|| { + err!("failed to parse second in time {original:?}") + })?; // Parse an optional fractional component. let Parsed { value: nanosecond, input } = - parse_temporal_fraction(input) - .context(E::FailedFractionalSecondInTime)?; + parse_temporal_fraction(input).with_context(|| { + err!( + "failed to parse fractional nanoseconds \ + in time {original:?}", + ) + })?; let time = Time::new_ranged( hour, @@ -665,26 +715,33 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { + let original = escape::Bytes(input); + // Parse month component. let Parsed { value: month, mut input } = - self.parse_month(input).context(E::FailedMonthInMonthDay)?; + self.parse_month(input).with_context(|| { + err!("failed to parse month in month-day {original:?}") + })?; // Skip over optional separator. - if let Some(tail) = input.strip_prefix(b"-") { - input = tail; + if input.starts_with(b"-") { + input = &input[1..]; } // Parse day component. let Parsed { value: day, input } = - self.parse_day(input).context(E::FailedDayInMonthDay)?; + self.parse_day(input).with_context(|| { + err!("failed to parse day in month-day {original:?}") + })?; // Check that the month-day is valid. Since Temporal's month-day // permits 02-29, we use a leap year. The error message here is // probably confusing, but these errors should never be exposed to the // user. let year = t::Year::N::<2024>(); - let _ = - Date::new_ranged(year, month, day).context(E::InvalidMonthDay)?; + let _ = Date::new_ranged(year, month, day).with_context(|| { + err!("month-day parsed from {original:?} is not valid") + })?; // We have a valid year-month. But we don't return it because we just // need to check validity. @@ -701,24 +758,31 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { + let original = escape::Bytes(input); + // Parse year component. let Parsed { value: year, mut input } = - self.parse_year(input).context(E::FailedYearInYearMonth)?; + self.parse_year(input).with_context(|| { + err!("failed to parse year in date {original:?}") + })?; // Skip over optional separator. - if let Some(tail) = input.strip_prefix(b"-") { - input = tail; + if input.starts_with(b"-") { + input = &input[1..]; } // Parse month component. let Parsed { value: month, input } = - self.parse_month(input).context(E::FailedMonthInYearMonth)?; + self.parse_month(input).with_context(|| { + err!("failed to parse month in month-day {original:?}") + })?; // Check that the year-month is valid. We just use a day of 1, since // every month in every year must have a day 1. let day = t::Day::N::<1>(); - let _ = - Date::new_ranged(year, month, day).context(E::InvalidYearMonth)?; + let _ = Date::new_ranged(year, month, day).with_context(|| { + err!("year-month parsed from {original:?} is not valid") + })?; // We have a valid year-month. But we don't return it because we just // need to check validity. @@ -740,33 +804,50 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { + // TODO: We could probably decrease the codegen for this function, + // or at least make it tighter, by putting the code for signed years + // behind an unlineable function. + let Parsed { value: sign, input } = self.parse_year_sign(input); if let Some(sign) = sign { - return self.parse_signed_year(input, sign); + let (year, input) = parse::split(input, 6).ok_or_else(|| { + err!( + "expected six digit year (because of a leading sign), \ + but found end of input", + ) + })?; + let year = parse::i64(year).with_context(|| { + err!( + "failed to parse {year:?} as year (a six digit integer)", + year = escape::Bytes(year), + ) + })?; + let year = + t::Year::try_new("year", year).context("year is not valid")?; + if year == C(0) && sign.is_negative() { + return Err(err!( + "year zero must be written without a sign or a \ + positive sign, but not a negative sign", + )); + } + Ok(Parsed { value: year * sign.as_ranged_integer(), input }) + } else { + let (year, input) = parse::split(input, 4).ok_or_else(|| { + err!( + "expected four digit year (or leading sign for \ + six digit year), but found end of input", + ) + })?; + let year = parse::i64(year).with_context(|| { + err!( + "failed to parse {year:?} as year (a four digit integer)", + year = escape::Bytes(year), + ) + })?; + let year = + t::Year::try_new("year", year).context("year is not valid")?; + Ok(Parsed { value: year, input }) } - - let (year, input) = - parse::split(input, 4).ok_or(E::ExpectedFourDigitYear)?; - let year = parse::i64(year).context(E::ParseYearFourDigit)?; - let year = t::Year::try_new("year", year).context(E::InvalidYear)?; - Ok(Parsed { value: year, input }) - } - - #[cold] - #[inline(never)] - fn parse_signed_year<'i>( - &self, - input: &'i [u8], - sign: Sign, - ) -> Result, Error> { - let (year, input) = - parse::split(input, 6).ok_or(E::ExpectedSixDigitYear)?; - let year = parse::i64(year).context(E::ParseYearSixDigit)?; - let year = t::Year::try_new("year", year).context(E::InvalidYear)?; - if year == C(0) && sign.is_negative() { - return Err(Error::from(E::InvalidYearZero)); - } - Ok(Parsed { value: year * sign.as_ranged_integer(), input }) } // DateMonth ::: @@ -779,11 +860,17 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (month, input) = - parse::split(input, 2).ok_or(E::ExpectedTwoDigitMonth)?; - let month = parse::i64(month).context(E::ParseMonthTwoDigit)?; + let (month, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit month, but found end of input") + })?; + let month = parse::i64(month).with_context(|| { + err!( + "failed to parse {month:?} as month (a two digit integer)", + month = escape::Bytes(month), + ) + })?; let month = - t::Month::try_new("month", month).context(E::InvalidMonth)?; + t::Month::try_new("month", month).context("month is not valid")?; Ok(Parsed { value: month, input }) } @@ -798,10 +885,16 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (day, input) = - parse::split(input, 2).ok_or(E::ExpectedTwoDigitDay)?; - let day = parse::i64(day).context(E::ParseDayTwoDigit)?; - let day = t::Day::try_new("day", day).context(E::InvalidDay)?; + let (day, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit day, but found end of input") + })?; + let day = parse::i64(day).with_context(|| { + err!( + "failed to parse {day:?} as day (a two digit integer)", + day = escape::Bytes(day), + ) + })?; + let day = t::Day::try_new("day", day).context("day is not valid")?; Ok(Parsed { value: day, input }) } @@ -820,10 +913,17 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (hour, input) = - parse::split(input, 2).ok_or(E::ExpectedTwoDigitHour)?; - let hour = parse::i64(hour).context(E::ParseHourTwoDigit)?; - let hour = t::Hour::try_new("hour", hour).context(E::InvalidHour)?; + let (hour, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit hour, but found end of input") + })?; + let hour = parse::i64(hour).with_context(|| { + err!( + "failed to parse {hour:?} as hour (a two digit integer)", + hour = escape::Bytes(hour), + ) + })?; + let hour = + t::Hour::try_new("hour", hour).context("hour is not valid")?; Ok(Parsed { value: hour, input }) } @@ -842,11 +942,17 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (minute, input) = - parse::split(input, 2).ok_or(E::ExpectedTwoDigitMinute)?; - let minute = parse::i64(minute).context(E::ParseMinuteTwoDigit)?; - let minute = - t::Minute::try_new("minute", minute).context(E::InvalidMinute)?; + let (minute, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit minute, but found end of input") + })?; + let minute = parse::i64(minute).with_context(|| { + err!( + "failed to parse {minute:?} as minute (a two digit integer)", + minute = escape::Bytes(minute), + ) + })?; + let minute = t::Minute::try_new("minute", minute) + .context("minute is not valid")?; Ok(Parsed { value: minute, input }) } @@ -866,16 +972,22 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (second, input) = - parse::split(input, 2).ok_or(E::ExpectedTwoDigitSecond)?; - let mut second = parse::i64(second).context(E::ParseSecondTwoDigit)?; + let (second, input) = parse::split(input, 2).ok_or_else(|| { + err!("expected two digit second, but found end of input",) + })?; + let mut second = parse::i64(second).with_context(|| { + err!( + "failed to parse {second:?} as second (a two digit integer)", + second = escape::Bytes(second), + ) + })?; // NOTE: I believe Temporal allows one to make this configurable. That // is, to reject it. But for now, we just always clamp a leap second. if second == 60 { second = 59; } - let second = - t::Second::try_new("second", second).context(E::InvalidSecond)?; + let second = t::Second::try_new("second", second) + .context("second is not valid")?; Ok(Parsed { value: second, input }) } @@ -895,7 +1007,7 @@ impl DateTimeParser { input: &'i [u8], ) -> Result>, Error> { const P: rfc9557::Parser = rfc9557::Parser::new(); - if input.first().map_or(true, |&b| b != b'[') { + if input.is_empty() || input[0] != b'[' { let value = ParsedAnnotations::none(); return Ok(Parsed { input, value }); } @@ -910,24 +1022,32 @@ impl DateTimeParser { #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_date_separator<'i>( &self, - input: &'i [u8], + mut input: &'i [u8], extended: bool, ) -> Result, Error> { if !extended { // If we see a '-' when not in extended mode, then we can report // a better error message than, e.g., "-3 isn't a valid day." if input.starts_with(b"-") { - return Err(Error::from(E::ExpectedNoSeparator)); + return Err(err!( + "expected no separator after month since none was \ + found after the year, but found a '-' separator", + )); } return Ok(Parsed { value: (), input }); } - let (&first, input) = - input.split_first().ok_or(E::ExpectedSeparatorFoundEndOfInput)?; - if first != b'-' { - return Err(Error::from(E::ExpectedSeparatorFoundByte { - byte: first, - })); + if input.is_empty() { + return Err(err!( + "expected '-' separator, but found end of input" + )); } + if input[0] != b'-' { + return Err(err!( + "expected '-' separator, but found {found:?} instead", + found = escape::Byte(input[0]), + )); + } + input = &input[1..]; Ok(Parsed { value: (), input }) } @@ -948,16 +1068,13 @@ impl DateTimeParser { extended: bool, ) -> Parsed<'i, bool> { if !extended { - let expected = parse::split(input, 2) - .map_or(false, |(prefix, _)| { - prefix.iter().all(u8::is_ascii_digit) - }); + let expected = + input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit); return Parsed { value: expected, input }; } - let mut is_separator = false; - if let Some(tail) = input.strip_prefix(b":") { - is_separator = true; - input = tail; + let is_separator = input.get(0).map_or(false, |&b| b == b':'); + if is_separator { + input = &input[1..]; } Parsed { value: is_separator, input } } @@ -977,9 +1094,9 @@ impl DateTimeParser { #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_year_sign<'i>( &self, - input: &'i [u8], + mut input: &'i [u8], ) -> Parsed<'i, Option> { - let Some((&sign, tail)) = input.split_first() else { + let Some(sign) = input.get(0).copied() else { return Parsed { value: None, input }; }; let sign = if sign == b'+' { @@ -989,55 +1106,8 @@ impl DateTimeParser { } else { return Parsed { value: None, input }; }; - Parsed { value: Some(sign), input: tail } - } - - /// Parses the `W` that is expected to appear before the week component in - /// an ISO 8601 week date. - #[cfg_attr(feature = "perf-inline", inline(always))] - fn parse_week_prefix<'i>( - &self, - input: &'i [u8], - ) -> Result, Error> { - let (&first, input) = - input.split_first().ok_or(E::ExpectedWeekPrefixFoundEndOfInput)?; - if !matches!(first, b'W' | b'w') { - return Err(Error::from(E::ExpectedWeekPrefixFoundByte { - byte: first, - })); - } - Ok(Parsed { value: (), input }) - } - - /// Parses the week number that follows a `W` in an ISO week date. - #[cfg_attr(feature = "perf-inline", inline(always))] - fn parse_week_num<'i>( - &self, - input: &'i [u8], - ) -> Result, Error> { - let (week_num, input) = - parse::split(input, 2).ok_or(E::ExpectedTwoDigitWeekNumber)?; - let week_num = - parse::i64(week_num).context(E::ParseWeekNumberTwoDigit)?; - let week_num = t::ISOWeek::try_new("week_num", week_num) - .context(E::InvalidWeekNumber)?; - Ok(Parsed { value: week_num, input }) - } - - /// Parses the weekday (1-indexed, starting with Monday) in an ISO 8601 - /// week date. - #[cfg_attr(feature = "perf-inline", inline(always))] - fn parse_weekday<'i>( - &self, - input: &'i [u8], - ) -> Result, Error> { - let (weekday, input) = - parse::split(input, 1).ok_or(E::ExpectedOneDigitWeekday)?; - let weekday = parse::i64(weekday).context(E::ParseWeekdayOneDigit)?; - let weekday = t::WeekdayOne::try_new("weekday", weekday) - .context(E::InvalidWeekday)?; - let weekday = Weekday::from_monday_one_offset_ranged(weekday); - Ok(Parsed { value: weekday, input }) + input = &input[1..]; + Parsed { value: Some(sign), input } } } @@ -1068,7 +1138,14 @@ impl SpanParser { let parsed = parsed.and_then(|_| builder.to_span())?; parsed.into_full() } - imp(self, input.as_ref()) + + let input = input.as_ref(); + imp(self, input).with_context(|| { + err!( + "failed to parse {input:?} as an ISO 8601 duration string", + input = escape::Bytes(input) + ) + }) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1083,7 +1160,14 @@ impl SpanParser { let parsed = parsed.and_then(|_| builder.to_signed_duration())?; parsed.into_full() } - imp(self, input.as_ref()) + + let input = input.as_ref(); + imp(self, input).with_context(|| { + err!( + "failed to parse {input:?} as an ISO 8601 duration string", + input = escape::Bytes(input) + ) + }) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1103,7 +1187,14 @@ impl SpanParser { let d = parsed.value; parsed.into_full_with(format_args!("{d:?}")) } - imp(self, input.as_ref()) + + let input = input.as_ref(); + imp(self, input).with_context(|| { + err!( + "failed to parse {input:?} as an ISO 8601 duration string", + input = escape::Bytes(input) + ) + }) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1112,6 +1203,7 @@ impl SpanParser { input: &'i [u8], builder: &mut DurationUnits, ) -> Result, Error> { + let original = escape::Bytes(input); let (sign, input) = if !input.first().map_or(false, |&b| matches!(b, b'+' | b'-')) { (Sign::Positive, input) @@ -1119,8 +1211,8 @@ impl SpanParser { let Parsed { value: sign, input } = self.parse_sign(input); (sign, input) }; - let Parsed { input, .. } = self.parse_duration_designator(input)?; + let Parsed { input, .. } = self.parse_date_units(input, builder)?; let Parsed { value: has_time, mut input } = self.parse_time_designator(input); @@ -1129,7 +1221,11 @@ impl SpanParser { input = parsed.input; if builder.get_min().map_or(true, |min| min > Unit::Hour) { - return Err(Error::from(E::ExpectedTimeUnits)); + return Err(err!( + "found a time designator (T or t) in an ISO 8601 \ + duration string in {original:?}, but did not find \ + any time units", + )); } } builder.set_sign(sign); @@ -1142,6 +1238,7 @@ impl SpanParser { input: &'i [u8], builder: &mut DurationUnits, ) -> Result, Error> { + let original = escape::Bytes(input); let (sign, input) = if !input.first().map_or(false, |&b| matches!(b, b'+' | b'-')) { (Sign::Positive, input) @@ -1149,17 +1246,25 @@ impl SpanParser { let Parsed { value: sign, input } = self.parse_sign(input); (sign, input) }; - let Parsed { input, .. } = self.parse_duration_designator(input)?; + let Parsed { value: has_time, input } = self.parse_time_designator(input); if !has_time { - return Err(Error::from(E::ExpectedTimeDesignator)); + return Err(err!( + "parsing ISO 8601 duration into a `SignedDuration` requires \ + that the duration contain a time component and no \ + components of days or greater", + )); } let Parsed { input, .. } = self.parse_time_units(input, builder)?; if builder.get_min().map_or(true, |min| min > Unit::Hour) { - return Err(Error::from(E::ExpectedTimeUnits)); + return Err(err!( + "found a time designator (T or t) in an ISO 8601 \ + duration string in {original:?}, but did not find \ + any time units", + )); } builder.set_sign(sign); Ok(Parsed { value: (), input }) @@ -1234,21 +1339,26 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - let (&first, input) = input - .split_first() - .ok_or(E::ExpectedDateDesignatorFoundEndOfInput)?; - let unit = match first { + if input.is_empty() { + return Err(err!( + "expected to find date unit designator suffix \ + (Y, M, W or D), but found end of input", + )); + } + let unit = match input[0] { b'Y' | b'y' => Unit::Year, b'M' | b'm' => Unit::Month, b'W' | b'w' => Unit::Week, b'D' | b'd' => Unit::Day, - _ => { - return Err(Error::from(E::ExpectedDateDesignatorFoundByte { - byte: first, - })); + unknown => { + return Err(err!( + "expected to find date unit designator suffix \ + (Y, M, W or D), but found {found:?} instead", + found = escape::Byte(unknown), + )); } }; - Ok(Parsed { value: unit, input }) + Ok(Parsed { value: unit, input: &input[1..] }) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1256,20 +1366,25 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - let (&first, input) = input - .split_first() - .ok_or(E::ExpectedTimeDesignatorFoundEndOfInput)?; - let unit = match first { + if input.is_empty() { + return Err(err!( + "expected to find time unit designator suffix \ + (H, M or S), but found end of input", + )); + } + let unit = match input[0] { b'H' | b'h' => Unit::Hour, b'M' | b'm' => Unit::Minute, b'S' | b's' => Unit::Second, - _ => { - return Err(Error::from(E::ExpectedTimeDesignatorFoundByte { - byte: first, - })); + unknown => { + return Err(err!( + "expected to find time unit designator suffix \ + (H, M or S), but found {found:?} instead", + found = escape::Byte(unknown), + )); } }; - Ok(Parsed { value: unit, input }) + Ok(Parsed { value: unit, input: &input[1..] }) } // DurationDesignator ::: one of @@ -1279,28 +1394,30 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - let (&first, input) = input - .split_first() - .ok_or(E::ExpectedDurationDesignatorFoundEndOfInput)?; - if !matches!(first, b'P' | b'p') { - return Err(Error::from(E::ExpectedDurationDesignatorFoundByte { - byte: first, - })); + if input.is_empty() { + return Err(err!( + "expected to find duration beginning with 'P' or 'p', \ + but found end of input", + )); } - Ok(Parsed { value: (), input }) + if !matches!(input[0], b'P' | b'p') { + return Err(err!( + "expected 'P' or 'p' prefix to begin duration, \ + but found {found:?} instead", + found = escape::Byte(input[0]), + )); + } + Ok(Parsed { value: (), input: &input[1..] }) } // TimeDesignator ::: one of // T t #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_time_designator<'i>(&self, input: &'i [u8]) -> Parsed<'i, bool> { - let Some((&first, tail)) = input.split_first() else { - return Parsed { value: false, input }; - }; - if !matches!(first, b'T' | b't') { + if input.is_empty() || !matches!(input[0], b'T' | b't') { return Parsed { value: false, input }; } - Parsed { value: true, input: tail } + Parsed { value: true, input: &input[1..] } } // TemporalSign ::: @@ -1312,13 +1429,17 @@ impl SpanParser { #[cold] #[inline(never)] fn parse_sign<'i>(&self, input: &'i [u8]) -> Parsed<'i, Sign> { - if let Some(tail) = input.strip_prefix(b"+") { - Parsed { value: Sign::Positive, input: tail } - } else if let Some(tail) = input.strip_prefix(b"-") { - Parsed { value: Sign::Negative, input: tail } + let Some(sign) = input.get(0).copied() else { + return Parsed { value: Sign::Positive, input }; + }; + let sign = if sign == b'+' { + Sign::Positive + } else if sign == b'-' { + Sign::Negative } else { - Parsed { value: Sign::Positive, input } - } + return Parsed { value: Sign::Positive, input }; + }; + Parsed { value: sign, input: &input[1..] } } } @@ -1360,63 +1481,63 @@ mod tests { insta::assert_snapshot!( p(b"P0d"), - @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", + @r#"failed to parse "P0d" as an ISO 8601 duration string: parsing ISO 8601 duration into a `SignedDuration` requires that the duration contain a time component and no components of days or greater"#, ); insta::assert_snapshot!( p(b"PT0d"), - @"expected to find time unit designator suffix (`H`, `M` or `S`), but found `d` instead", + @r#"failed to parse "PT0d" as an ISO 8601 duration string: expected to find time unit designator suffix (H, M or S), but found "d" instead"#, ); insta::assert_snapshot!( p(b"P0dT1s"), - @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", + @r#"failed to parse "P0dT1s" as an ISO 8601 duration string: parsing ISO 8601 duration into a `SignedDuration` requires that the duration contain a time component and no components of days or greater"#, ); insta::assert_snapshot!( p(b""), - @"expected to find duration beginning with `P` or `p`, but found end of input", + @r#"failed to parse "" as an ISO 8601 duration string: expected to find duration beginning with 'P' or 'p', but found end of input"#, ); insta::assert_snapshot!( p(b"P"), - @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", + @r#"failed to parse "P" as an ISO 8601 duration string: parsing ISO 8601 duration into a `SignedDuration` requires that the duration contain a time component and no components of days or greater"#, ); insta::assert_snapshot!( p(b"PT"), - @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", + @r#"failed to parse "PT" as an ISO 8601 duration string: found a time designator (T or t) in an ISO 8601 duration string in "PT", but did not find any time units"#, ); insta::assert_snapshot!( p(b"PTs"), - @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", + @r#"failed to parse "PTs" as an ISO 8601 duration string: found a time designator (T or t) in an ISO 8601 duration string in "PTs", but did not find any time units"#, ); insta::assert_snapshot!( p(b"PT1s1m"), - @"found value with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)", + @r#"failed to parse "PT1s1m" as an ISO 8601 duration string: found value 1 with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)"#, ); insta::assert_snapshot!( p(b"PT1s1h"), - @"found value with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)", + @r#"failed to parse "PT1s1h" as an ISO 8601 duration string: found value 1 with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)"#, ); insta::assert_snapshot!( p(b"PT1m1h"), - @"found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)", + @r#"failed to parse "PT1m1h" as an ISO 8601 duration string: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#, ); insta::assert_snapshot!( p(b"-PT9223372036854775809s"), - @"value for seconds is too big (or small) to fit into a signed 64-bit integer", + @r#"failed to parse "-PT9223372036854775809s" as an ISO 8601 duration string: `-9223372036854775809` seconds is too big (or small) to fit into a signed 64-bit integer"#, ); insta::assert_snapshot!( p(b"PT9223372036854775808s"), - @"value for seconds is too big (or small) to fit into a signed 64-bit integer", + @r#"failed to parse "PT9223372036854775808s" as an ISO 8601 duration string: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#, ); insta::assert_snapshot!( p(b"PT1m9223372036854775807s"), - @"accumulated duration overflowed when adding value to unit second", + @r#"failed to parse "PT1m9223372036854775807s" as an ISO 8601 duration string: accumulated `SignedDuration` of `1m` overflowed when adding 9223372036854775807 of unit second"#, ); insta::assert_snapshot!( p(b"PT2562047788015215.6h"), - @"accumulated duration overflowed when adding fractional value to unit hour", + @r#"failed to parse "PT2562047788015215.6h" as an ISO 8601 duration string: accumulated `SignedDuration` of `2562047788015215h` overflowed when adding 0.600000000 of unit hour"#, ); } @@ -1457,76 +1578,76 @@ mod tests { insta::assert_snapshot!( p(b"-PT1S"), - @"cannot parse negative duration into unsigned `std::time::Duration`", + @r#"failed to parse "-PT1S" as an ISO 8601 duration string: cannot parse negative duration into unsigned `std::time::Duration`"#, ); insta::assert_snapshot!( p(b"-PT0S"), - @"cannot parse negative duration into unsigned `std::time::Duration`", + @r#"failed to parse "-PT0S" as an ISO 8601 duration string: cannot parse negative duration into unsigned `std::time::Duration`"#, ); insta::assert_snapshot!( p(b"P0d"), - @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", + @r#"failed to parse "P0d" as an ISO 8601 duration string: parsing ISO 8601 duration into a `SignedDuration` requires that the duration contain a time component and no components of days or greater"#, ); insta::assert_snapshot!( p(b"PT0d"), - @"expected to find time unit designator suffix (`H`, `M` or `S`), but found `d` instead", + @r#"failed to parse "PT0d" as an ISO 8601 duration string: expected to find time unit designator suffix (H, M or S), but found "d" instead"#, ); insta::assert_snapshot!( p(b"P0dT1s"), - @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", + @r#"failed to parse "P0dT1s" as an ISO 8601 duration string: parsing ISO 8601 duration into a `SignedDuration` requires that the duration contain a time component and no components of days or greater"#, ); insta::assert_snapshot!( p(b""), - @"expected to find duration beginning with `P` or `p`, but found end of input", + @r#"failed to parse "" as an ISO 8601 duration string: expected to find duration beginning with 'P' or 'p', but found end of input"#, ); insta::assert_snapshot!( p(b"P"), - @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", + @r#"failed to parse "P" as an ISO 8601 duration string: parsing ISO 8601 duration into a `SignedDuration` requires that the duration contain a time component and no components of days or greater"#, ); insta::assert_snapshot!( p(b"PT"), - @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", + @r#"failed to parse "PT" as an ISO 8601 duration string: found a time designator (T or t) in an ISO 8601 duration string in "PT", but did not find any time units"#, ); insta::assert_snapshot!( p(b"PTs"), - @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", + @r#"failed to parse "PTs" as an ISO 8601 duration string: found a time designator (T or t) in an ISO 8601 duration string in "PTs", but did not find any time units"#, ); insta::assert_snapshot!( p(b"PT1s1m"), - @"found value with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)", + @r#"failed to parse "PT1s1m" as an ISO 8601 duration string: found value 1 with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)"#, ); insta::assert_snapshot!( p(b"PT1s1h"), - @"found value with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)", + @r#"failed to parse "PT1s1h" as an ISO 8601 duration string: found value 1 with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)"#, ); insta::assert_snapshot!( p(b"PT1m1h"), - @"found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)", + @r#"failed to parse "PT1m1h" as an ISO 8601 duration string: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#, ); insta::assert_snapshot!( p(b"-PT9223372036854775809S"), - @"cannot parse negative duration into unsigned `std::time::Duration`", + @r#"failed to parse "-PT9223372036854775809S" as an ISO 8601 duration string: cannot parse negative duration into unsigned `std::time::Duration`"#, ); insta::assert_snapshot!( p(b"PT18446744073709551616S"), - @"number too big to parse into 64-bit integer", + @r#"failed to parse "PT18446744073709551616S" as an ISO 8601 duration string: number `18446744073709551616` too big to parse into 64-bit integer"#, ); insta::assert_snapshot!( p(b"PT5124095576030431H16.999999999S"), - @"accumulated duration overflowed when adding value to unit second", + @r#"failed to parse "PT5124095576030431H16.999999999S" as an ISO 8601 duration string: accumulated `std::time::Duration` of `18446744073709551600s` overflowed when adding 16 of unit second"#, ); insta::assert_snapshot!( p(b"PT1M18446744073709551556S"), - @"accumulated duration overflowed when adding value to unit second", + @r#"failed to parse "PT1M18446744073709551556S" as an ISO 8601 duration string: accumulated `std::time::Duration` of `60s` overflowed when adding 18446744073709551556 of unit second"#, ); insta::assert_snapshot!( p(b"PT5124095576030431.5H"), - @"accumulated duration overflowed when adding fractional value to unit hour", + @r#"failed to parse "PT5124095576030431.5H" as an ISO 8601 duration string: accumulated `std::time::Duration` of `18446744073709551600s` overflowed when adding 0.500000000 of unit hour"#, ); } @@ -1589,7 +1710,7 @@ mod tests { DateTimeParser::new().parse_temporal_datetime(input).unwrap() }; - insta::assert_debug_snapshot!(p(b"2024-06-01"), @r#" + insta::assert_debug_snapshot!(p(b"2024-06-01"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01", @@ -1600,13 +1721,14 @@ mod tests { time: None, offset: None, annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01[America/New_York]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01[America/New_York]"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01[America/New_York]", @@ -1617,6 +1739,7 @@ mod tests { time: None, offset: None, annotations: ParsedAnnotations { + input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1627,8 +1750,8 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T01:02:03", @@ -1645,13 +1768,14 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T01:02:03-05", @@ -1674,13 +1798,14 @@ mod tests { }, ), annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05[America/New_York]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05[America/New_York]"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T01:02:03-05[America/New_York]", @@ -1703,6 +1828,7 @@ mod tests { }, ), annotations: ParsedAnnotations { + input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1713,8 +1839,8 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03Z[America/New_York]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03Z[America/New_York]"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T01:02:03Z[America/New_York]", @@ -1735,6 +1861,7 @@ mod tests { }, ), annotations: ParsedAnnotations { + input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1745,8 +1872,8 @@ mod tests { }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-01[America/New_York]"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-01[America/New_York]"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T01:02:03-01[America/New_York]", @@ -1769,6 +1896,7 @@ mod tests { }, ), annotations: ParsedAnnotations { + input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1779,7 +1907,7 @@ mod tests { }, input: "", } - "#); + "###); } #[test] @@ -1788,7 +1916,7 @@ mod tests { DateTimeParser::new().parse_temporal_datetime(input).unwrap() }; - insta::assert_debug_snapshot!(p(b"2024-06-01T01"), @r#" + insta::assert_debug_snapshot!(p(b"2024-06-01T01"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T01", @@ -1805,13 +1933,14 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01T0102"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01T0102"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T0102", @@ -1828,13 +1957,14 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01T01:02"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01T01:02"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01T01:02", @@ -1851,12 +1981,13 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); + "###); } #[test] @@ -1865,7 +1996,7 @@ mod tests { DateTimeParser::new().parse_temporal_datetime(input).unwrap() }; - insta::assert_debug_snapshot!(p(b"2024-06-01t01:02:03"), @r#" + insta::assert_debug_snapshot!(p(b"2024-06-01t01:02:03"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01t01:02:03", @@ -1882,13 +2013,14 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); - insta::assert_debug_snapshot!(p(b"2024-06-01 01:02:03"), @r#" + "###); + insta::assert_debug_snapshot!(p(b"2024-06-01 01:02:03"), @r###" Parsed { value: ParsedDateTime { input: "2024-06-01 01:02:03", @@ -1905,12 +2037,13 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { + input: "", time_zone: None, }, }, input: "", } - "#); + "###); } #[test] @@ -2057,11 +2190,11 @@ mod tests { insta::assert_snapshot!( p(b"010203"), - @"parsed time is ambiguous with a month-day date", + @r###"parsed time from "010203" is ambiguous with a month-day date"###, ); insta::assert_snapshot!( p(b"130112"), - @"parsed time is ambiguous with a year-month date", + @r###"parsed time from "130112" is ambiguous with a year-month date"###, ); } @@ -2073,21 +2206,21 @@ mod tests { insta::assert_snapshot!( p(b"2024-06-01[America/New_York]"), - @"successfully parsed date, but no time component was found", + @r###"successfully parsed date from "2024-06-01[America/New_York]", but no time component was found"###, ); // 2099 is not a valid time, but 2099-12-01 is a valid date, so this // carves a path where a full datetime parse is OK, but a basic // time-only parse is not. insta::assert_snapshot!( p(b"2099-12-01[America/New_York]"), - @"successfully parsed date, but no time component was found", + @r###"successfully parsed date from "2099-12-01[America/New_York]", but no time component was found"###, ); // Like above, but this time we use an invalid date. As a result, we // get an error reported not on the invalid date, but on how it is an // invalid time. (Because we're asking for a time here.) insta::assert_snapshot!( p(b"2099-13-01[America/New_York]"), - @"failed to parse minute in time: parsed minute is not valid: parameter 'minute' with value 99 is not in the required range of 0..=59", + @r###"failed to parse minute in time "2099-13-01[America/New_York]": minute is not valid: parameter 'minute' with value 99 is not in the required range of 0..=59"###, ); } @@ -2099,19 +2232,19 @@ mod tests { insta::assert_snapshot!( p(b"T00:00:00Z"), - @"cannot parse civil date/time from string with a Zulu offset, parse as a `jiff::Timestamp` first and convert to a civil date/time instead", + @"cannot parse civil time from string with a Zulu offset, parse as a `Timestamp` and convert to a civil time instead", ); insta::assert_snapshot!( p(b"00:00:00Z"), - @"cannot parse civil date/time from string with a Zulu offset, parse as a `jiff::Timestamp` first and convert to a civil date/time instead", + @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead", ); insta::assert_snapshot!( p(b"000000Z"), - @"cannot parse civil date/time from string with a Zulu offset, parse as a `jiff::Timestamp` first and convert to a civil date/time instead", + @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead", ); insta::assert_snapshot!( p(b"2099-12-01T00:00:00Z"), - @"cannot parse civil date/time from string with a Zulu offset, parse as a `jiff::Timestamp` first and convert to a civil date/time instead", + @"cannot parse plain time from full datetime string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead", ); } @@ -2170,7 +2303,7 @@ mod tests { fn err_date_empty() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"").unwrap_err(), - @"failed to parse year in date: expected four digit year (or leading sign for six digit year), but found end of input", + @r###"failed to parse year in date "": expected four digit year (or leading sign for six digit year), but found end of input"###, ); } @@ -2178,40 +2311,40 @@ mod tests { fn err_date_year() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"123").unwrap_err(), - @"failed to parse year in date: expected four digit year (or leading sign for six digit year), but found end of input", + @r###"failed to parse year in date "123": expected four digit year (or leading sign for six digit year), but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"123a").unwrap_err(), - @"failed to parse year in date: failed to parse four digit integer as year: invalid digit, expected 0-9 but got a", + @r###"failed to parse year in date "123a": failed to parse "123a" as year (a four digit integer): invalid digit, expected 0-9 but got a"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-9999").unwrap_err(), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", + @r###"failed to parse year in date "-9999": expected six digit year (because of a leading sign), but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"+9999").unwrap_err(), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", + @r###"failed to parse year in date "+9999": expected six digit year (because of a leading sign), but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-99999").unwrap_err(), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", + @r###"failed to parse year in date "-99999": expected six digit year (because of a leading sign), but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"+99999").unwrap_err(), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", + @r###"failed to parse year in date "+99999": expected six digit year (because of a leading sign), but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-99999a").unwrap_err(), - @"failed to parse year in date: failed to parse six digit integer as year: invalid digit, expected 0-9 but got a", + @r###"failed to parse year in date "-99999a": failed to parse "99999a" as year (a six digit integer): invalid digit, expected 0-9 but got a"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"+999999").unwrap_err(), - @"failed to parse year in date: parsed year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999", + @r###"failed to parse year in date "+999999": year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-010000").unwrap_err(), - @"failed to parse year in date: parsed year is not valid: parameter 'year' with value 10000 is not in the required range of -9999..=9999", + @r###"failed to parse year in date "-010000": year is not valid: parameter 'year' with value 10000 is not in the required range of -9999..=9999"###, ); } @@ -2219,19 +2352,19 @@ mod tests { fn err_date_month() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-").unwrap_err(), - @"failed to parse month in date: expected two digit month, but found end of input", + @r###"failed to parse month in date "2024-": expected two digit month, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024").unwrap_err(), - @"failed to parse month in date: expected two digit month, but found end of input", + @r###"failed to parse month in date "2024": expected two digit month, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-13-01").unwrap_err(), - @"failed to parse month in date: parsed month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12", + @r###"failed to parse month in date "2024-13-01": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"20241301").unwrap_err(), - @"failed to parse month in date: parsed month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12", + @r###"failed to parse month in date "20241301": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"###, ); } @@ -2239,27 +2372,27 @@ mod tests { fn err_date_day() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-12-").unwrap_err(), - @"failed to parse day in date: expected two digit day, but found end of input", + @r###"failed to parse day in date "2024-12-": expected two digit day, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"202412").unwrap_err(), - @"failed to parse day in date: expected two digit day, but found end of input", + @r###"failed to parse day in date "202412": expected two digit day, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-12-40").unwrap_err(), - @"failed to parse day in date: parsed day is not valid: parameter 'day' with value 40 is not in the required range of 1..=31", + @r###"failed to parse day in date "2024-12-40": day is not valid: parameter 'day' with value 40 is not in the required range of 1..=31"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-11-31").unwrap_err(), - @"parsed date is not valid: parameter 'day' with value 31 is not in the required range of 1..=30", + @r###"date parsed from "2024-11-31" is not valid: parameter 'day' with value 31 is not in the required range of 1..=30"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-02-30").unwrap_err(), - @"parsed date is not valid: parameter 'day' with value 30 is not in the required range of 1..=29", + @r###"date parsed from "2024-02-30" is not valid: parameter 'day' with value 30 is not in the required range of 1..=29"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2023-02-29").unwrap_err(), - @"parsed date is not valid: parameter 'day' with value 29 is not in the required range of 1..=28", + @r###"date parsed from "2023-02-29" is not valid: parameter 'day' with value 29 is not in the required range of 1..=28"###, ); } @@ -2267,11 +2400,11 @@ mod tests { fn err_date_separator() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-1231").unwrap_err(), - @"failed to parse separator after month: expected `-` separator, but found `3`", + @r###"failed to parse separator after month: expected '-' separator, but found "3" instead"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"202412-31").unwrap_err(), - @"failed to parse separator after month: expected no separator since none was found after the year, but found a `-` separator", + @"failed to parse separator after month: expected no separator after month since none was found after the year, but found a '-' separator", ); } @@ -2400,7 +2533,7 @@ mod tests { fn err_time_empty() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"").unwrap_err(), - @"failed to parse hour in time: expected two digit hour, but found end of input", + @r###"failed to parse hour in time "": expected two digit hour, but found end of input"###, ); } @@ -2408,15 +2541,15 @@ mod tests { fn err_time_hour() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"a").unwrap_err(), - @"failed to parse hour in time: expected two digit hour, but found end of input", + @r###"failed to parse hour in time "a": expected two digit hour, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"1a").unwrap_err(), - @"failed to parse hour in time: failed to parse two digit integer as hour: invalid digit, expected 0-9 but got a", + @r###"failed to parse hour in time "1a": failed to parse "1a" as hour (a two digit integer): invalid digit, expected 0-9 but got a"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"24").unwrap_err(), - @"failed to parse hour in time: parsed hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23", + @r###"failed to parse hour in time "24": hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23"###, ); } @@ -2424,19 +2557,19 @@ mod tests { fn err_time_minute() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:").unwrap_err(), - @"failed to parse minute in time: expected two digit minute, but found end of input", + @r###"failed to parse minute in time "01:": expected two digit minute, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:a").unwrap_err(), - @"failed to parse minute in time: expected two digit minute, but found end of input", + @r###"failed to parse minute in time "01:a": expected two digit minute, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:1a").unwrap_err(), - @"failed to parse minute in time: failed to parse two digit integer as minute: invalid digit, expected 0-9 but got a", + @r###"failed to parse minute in time "01:1a": failed to parse "1a" as minute (a two digit integer): invalid digit, expected 0-9 but got a"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:60").unwrap_err(), - @"failed to parse minute in time: parsed minute is not valid: parameter 'minute' with value 60 is not in the required range of 0..=59", + @r###"failed to parse minute in time "01:60": minute is not valid: parameter 'minute' with value 60 is not in the required range of 0..=59"###, ); } @@ -2444,19 +2577,19 @@ mod tests { fn err_time_second() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:").unwrap_err(), - @"failed to parse second in time: expected two digit second, but found end of input", + @r###"failed to parse second in time "01:02:": expected two digit second, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:a").unwrap_err(), - @"failed to parse second in time: expected two digit second, but found end of input", + @r###"failed to parse second in time "01:02:a": expected two digit second, but found end of input"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:1a").unwrap_err(), - @"failed to parse second in time: failed to parse two digit integer as second: invalid digit, expected 0-9 but got a", + @r###"failed to parse second in time "01:02:1a": failed to parse "1a" as second (a two digit integer): invalid digit, expected 0-9 but got a"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:61").unwrap_err(), - @"failed to parse second in time: parsed second is not valid: parameter 'second' with value 61 is not in the required range of 0..=59", + @r###"failed to parse second in time "01:02:61": second is not valid: parameter 'second' with value 61 is not in the required range of 0..=59"###, ); } @@ -2464,262 +2597,11 @@ mod tests { fn err_time_fractional() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:03.").unwrap_err(), - @"failed to parse fractional seconds in time: found decimal after seconds component, but did not find any digits after decimal", + @r###"failed to parse fractional nanoseconds in time "01:02:03.": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:03.a").unwrap_err(), - @"failed to parse fractional seconds in time: found decimal after seconds component, but did not find any digits after decimal", - ); - } - - #[test] - fn ok_iso_week_date_parse_basic() { - fn p(input: &str) -> Parsed<'_, ISOWeekDate> { - DateTimeParser::new() - .parse_iso_week_date(input.as_bytes()) - .unwrap() - } - - insta::assert_debug_snapshot!( p("2024-W01-5"), @r#" - Parsed { - value: ISOWeekDate { - year: 2024, - week: 1, - weekday: Friday, - }, - input: "", - } - "#); - insta::assert_debug_snapshot!( p("2024-W52-7"), @r#" - Parsed { - value: ISOWeekDate { - year: 2024, - week: 52, - weekday: Sunday, - }, - input: "", - } - "#); - insta::assert_debug_snapshot!( p("2004-W53-6"), @r#" - Parsed { - value: ISOWeekDate { - year: 2004, - week: 53, - weekday: Saturday, - }, - input: "", - } - "#); - insta::assert_debug_snapshot!( p("2009-W01-1"), @r#" - Parsed { - value: ISOWeekDate { - year: 2009, - week: 1, - weekday: Monday, - }, - input: "", - } - "#); - - insta::assert_debug_snapshot!( p("2024W015"), @r#" - Parsed { - value: ISOWeekDate { - year: 2024, - week: 1, - weekday: Friday, - }, - input: "", - } - "#); - insta::assert_debug_snapshot!( p("2024W527"), @r#" - Parsed { - value: ISOWeekDate { - year: 2024, - week: 52, - weekday: Sunday, - }, - input: "", - } - "#); - insta::assert_debug_snapshot!( p("2004W536"), @r#" - Parsed { - value: ISOWeekDate { - year: 2004, - week: 53, - weekday: Saturday, - }, - input: "", - } - "#); - insta::assert_debug_snapshot!( p("2009W011"), @r#" - Parsed { - value: ISOWeekDate { - year: 2009, - week: 1, - weekday: Monday, - }, - input: "", - } - "#); - - // Lowercase should be okay. This matches how - // we support `T` or `t`. - insta::assert_debug_snapshot!( p("2009w011"), @r#" - Parsed { - value: ISOWeekDate { - year: 2009, - week: 1, - weekday: Monday, - }, - input: "", - } - "#); - } - - #[test] - fn err_iso_week_date_year() { - let p = |input: &str| { - DateTimeParser::new() - .parse_iso_week_date(input.as_bytes()) - .unwrap_err() - }; - - insta::assert_snapshot!( - p("123"), - @"failed to parse year in date: expected four digit year (or leading sign for six digit year), but found end of input", - ); - insta::assert_snapshot!( - p("123a"), - @"failed to parse year in date: failed to parse four digit integer as year: invalid digit, expected 0-9 but got a", - ); - - insta::assert_snapshot!( - p("-9999"), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", - ); - insta::assert_snapshot!( - p("+9999"), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", - ); - insta::assert_snapshot!( - p("-99999"), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", - ); - insta::assert_snapshot!( - p("+99999"), - @"failed to parse year in date: expected six digit year (because of a leading sign), but found end of input", - ); - insta::assert_snapshot!( - p("-99999a"), - @"failed to parse year in date: failed to parse six digit integer as year: invalid digit, expected 0-9 but got a", - ); - insta::assert_snapshot!( - p("+999999"), - @"failed to parse year in date: parsed year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999", - ); - insta::assert_snapshot!( - p("-010000"), - @"failed to parse year in date: parsed year is not valid: parameter 'year' with value 10000 is not in the required range of -9999..=9999", - ); - } - - #[test] - fn err_iso_week_date_week_prefix() { - let p = |input: &str| { - DateTimeParser::new() - .parse_iso_week_date(input.as_bytes()) - .unwrap_err() - }; - - insta::assert_snapshot!( - p("2024-"), - @"failed to parse week number prefix in date: expected `W` or `w`, but found end of input", - ); - insta::assert_snapshot!( - p("2024"), - @"failed to parse week number prefix in date: expected `W` or `w`, but found end of input", - ); - } - - #[test] - fn err_iso_week_date_week_number() { - let p = |input: &str| { - DateTimeParser::new() - .parse_iso_week_date(input.as_bytes()) - .unwrap_err() - }; - - insta::assert_snapshot!( - p("2024-W"), - @"failed to parse week number in date: expected two digit week number, but found end of input", - ); - insta::assert_snapshot!( - p("2024-W1"), - @"failed to parse week number in date: expected two digit week number, but found end of input", - ); - insta::assert_snapshot!( - p("2024-W53-1"), - @"parsed week date is not valid: ISO week number is invalid for given year", - ); - insta::assert_snapshot!( - p("2030W531"), - @"parsed week date is not valid: ISO week number is invalid for given year", - ); - } - - #[test] - fn err_iso_week_date_parse_incomplete() { - let p = |input: &str| { - DateTimeParser::new() - .parse_iso_week_date(input.as_bytes()) - .unwrap_err() - }; - - insta::assert_snapshot!( - p("2024-W53-1"), - @"parsed week date is not valid: ISO week number is invalid for given year", - ); - insta::assert_snapshot!( - p("2025-W53-1"), - @"parsed week date is not valid: ISO week number is invalid for given year", - ); - } - - #[test] - fn err_iso_week_date_date_day() { - let p = |input: &str| { - DateTimeParser::new() - .parse_iso_week_date(input.as_bytes()) - .unwrap_err() - }; - insta::assert_snapshot!( - p("2024-W12-"), - @"failed to parse weekday in date: expected one digit weekday, but found end of input", - ); - insta::assert_snapshot!( - p("2024W12"), - @"failed to parse weekday in date: expected one digit weekday, but found end of input", - ); - insta::assert_snapshot!( - p("2024-W11-8"), - @"failed to parse weekday in date: parsed weekday is not valid: parameter 'weekday' with value 8 is not in the required range of 1..=7", - ); - } - - #[test] - fn err_iso_week_date_date_separator() { - let p = |input: &str| { - DateTimeParser::new() - .parse_iso_week_date(input.as_bytes()) - .unwrap_err() - }; - insta::assert_snapshot!( - p("2024-W521"), - @"failed to parse separator after week number: expected `-` separator, but found `1`", - ); - insta::assert_snapshot!( - p("2024W01-5"), - @"failed to parse separator after week number: expected no separator since none was found after the year, but found a `-` separator", + @r###"failed to parse fractional nanoseconds in time "01:02:03.a": found decimal after seconds component, but did not find any decimal digits after decimal"###, ); } } diff --git a/src/fmt/temporal/pieces.rs b/src/fmt/temporal/pieces.rs index ca311c8..156ff3f 100644 --- a/src/fmt/temporal/pieces.rs +++ b/src/fmt/temporal/pieces.rs @@ -146,8 +146,9 @@ use crate::{ /// /// assert_eq!( /// "2025-01-03T17:28-05".parse::().unwrap_err().to_string(), -/// "failed to find time zone annotation in square brackets, \ -/// which is required for parsing a zoned datetime", +/// "failed to find time zone in square brackets in \ +/// \"2025-01-03T17:28-05\", which is required for \ +/// parsing a zoned instant", /// ); /// ``` /// diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs index b01c332..8e80a7d 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -1,9 +1,9 @@ use crate::{ - civil::{Date, DateTime, ISOWeekDate, Time}, - error::{fmt::temporal::Error as E, Error}, + civil::{Date, DateTime, Time}, + error::{err, Error}, fmt::{ temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind}, - util::{FractionalFormatter, IntegerFormatter}, + util::{DecimalFormatter, FractionalFormatter}, Write, WriteExt, }, span::Span, @@ -108,11 +108,11 @@ impl DateTimePrinter { date: &Date, mut wtr: W, ) -> Result<(), Error> { - static FMT_YEAR_POSITIVE: IntegerFormatter = - IntegerFormatter::new().padding(4); - static FMT_YEAR_NEGATIVE: IntegerFormatter = - IntegerFormatter::new().padding(6); - static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); + static FMT_YEAR_POSITIVE: DecimalFormatter = + DecimalFormatter::new().padding(4); + static FMT_YEAR_NEGATIVE: DecimalFormatter = + DecimalFormatter::new().padding(6); + static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); if date.year() >= 0 { wtr.write_int(&FMT_YEAR_POSITIVE, date.year())?; @@ -132,7 +132,7 @@ impl DateTimePrinter { time: &Time, mut wtr: W, ) -> Result<(), Error> { - static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); + static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); wtr.write_int(&FMT_TWO, time.hour())?; @@ -197,7 +197,13 @@ impl DateTimePrinter { // // Anyway, if you're seeing this error and think there should be a // different behavior, please file an issue. - Err(Error::from(E::PrintTimeZoneFailure)) + Err(err!( + "time zones without IANA identifiers that aren't either \ + fixed offsets or a POSIX time zone can't be serialized \ + (this typically occurs when this is a system time zone \ + derived from `/etc/localtime` on Unix systems that \ + isn't symlinked to an entry in `/usr/share/zoneinfo`)", + )) } pub(super) fn print_pieces( @@ -249,34 +255,6 @@ impl DateTimePrinter { Ok(()) } - pub(super) fn print_iso_week_date( - &self, - iso_week_date: &ISOWeekDate, - mut wtr: W, - ) -> Result<(), Error> { - static FMT_YEAR_POSITIVE: IntegerFormatter = - IntegerFormatter::new().padding(4); - static FMT_YEAR_NEGATIVE: IntegerFormatter = - IntegerFormatter::new().padding(6); - static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); - static FMT_ONE: IntegerFormatter = IntegerFormatter::new().padding(1); - - if iso_week_date.year() >= 0 { - wtr.write_int(&FMT_YEAR_POSITIVE, iso_week_date.year())?; - } else { - wtr.write_int(&FMT_YEAR_NEGATIVE, iso_week_date.year())?; - } - wtr.write_str("-")?; - wtr.write_char(if self.lowercase { 'w' } else { 'W' })?; - wtr.write_int(&FMT_TWO, iso_week_date.week())?; - wtr.write_str("-")?; - wtr.write_int( - &FMT_ONE, - iso_week_date.weekday().to_monday_one_offset(), - )?; - Ok(()) - } - /// Formats the given "pieces" offset into the writer given. fn print_pieces_offset( &self, @@ -304,7 +282,7 @@ impl DateTimePrinter { offset: &Offset, mut wtr: W, ) -> Result<(), Error> { - static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); + static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); wtr.write_str(if offset.is_negative() { "-" } else { "+" })?; let mut hours = offset.part_hours_ranged().abs().get(); @@ -339,7 +317,7 @@ impl DateTimePrinter { offset: &Offset, mut wtr: W, ) -> Result<(), Error> { - static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); + static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); wtr.write_str(if offset.is_negative() { "-" } else { "+" })?; let hours = offset.part_hours_ranged().abs().get(); @@ -427,7 +405,7 @@ impl SpanPrinter { span: &Span, mut wtr: W, ) -> Result<(), Error> { - static FMT_INT: IntegerFormatter = IntegerFormatter::new(); + static FMT_INT: DecimalFormatter = DecimalFormatter::new(); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); if span.is_negative() { @@ -541,7 +519,7 @@ impl SpanPrinter { dur: &SignedDuration, mut wtr: W, ) -> Result<(), Error> { - static FMT_INT: IntegerFormatter = IntegerFormatter::new(); + static FMT_INT: DecimalFormatter = DecimalFormatter::new(); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); let mut non_zero_greater_than_second = false; @@ -590,7 +568,7 @@ impl SpanPrinter { dur: &core::time::Duration, mut wtr: W, ) -> Result<(), Error> { - static FMT_INT: IntegerFormatter = IntegerFormatter::new(); + static FMT_INT: DecimalFormatter = DecimalFormatter::new(); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); let mut non_zero_greater_than_second = false; @@ -642,10 +620,7 @@ impl SpanPrinter { mod tests { use alloc::string::String; - use crate::{ - civil::{date, Weekday}, - span::ToSpan, - }; + use crate::{civil::date, span::ToSpan}; use super::*; @@ -947,22 +922,4 @@ mod tests { @"PT5124095576030431H15.999999999S", ); } - - #[test] - fn print_iso_week_date() { - let p = |d: ISOWeekDate| -> String { - let mut buf = String::new(); - DateTimePrinter::new().print_iso_week_date(&d, &mut buf).unwrap(); - buf - }; - - insta::assert_snapshot!( - p(ISOWeekDate::new(2024, 52, Weekday::Monday).unwrap()), - @"2024-W52-1", - ); - insta::assert_snapshot!( - p(ISOWeekDate::new(2004, 1, Weekday::Sunday).unwrap()), - @"2004-W01-7", - ); - } } diff --git a/src/fmt/util.rs b/src/fmt/util.rs index 23662ea..3e1773a 100644 --- a/src/fmt/util.rs +++ b/src/fmt/util.rs @@ -1,7 +1,7 @@ use crate::{ - error::{fmt::util::Error as E, ErrorContext}, + error::{err, ErrorContext}, fmt::Parsed, - util::{c::Sign, parse, t}, + util::{c::Sign, escape, parse, t}, Error, SignedDuration, Span, Unit, }; @@ -14,31 +14,52 @@ use crate::{ /// faster. We roll our own which is a bit slower, but gets us enough of a win /// to be satisfied with and with (almost) pure safe code. /// -/// This only includes the sign when formatting a negative signed integer. +/// By default, this only includes the sign if it's negative. To always include +/// the sign, set `force_sign` to `true`. #[derive(Clone, Copy, Debug)] -pub(crate) struct IntegerFormatter { +pub(crate) struct DecimalFormatter { + force_sign: Option, minimum_digits: u8, padding_byte: u8, } -impl IntegerFormatter { - /// Creates a new integer formatter using the default configuration. - pub(crate) const fn new() -> IntegerFormatter { - IntegerFormatter { minimum_digits: 0, padding_byte: b'0' } +impl DecimalFormatter { + /// Creates a new decimal formatter using the default configuration. + pub(crate) const fn new() -> DecimalFormatter { + DecimalFormatter { + force_sign: None, + minimum_digits: 0, + padding_byte: b'0', + } } - /// Format the given value using this configuration as a signed integer + /// Format the given value using this configuration as a signed decimal /// ASCII number. #[cfg_attr(feature = "perf-inline", inline(always))] - pub(crate) const fn format_signed(&self, value: i64) -> Integer { - Integer::signed(self, value) + pub(crate) const fn format_signed(&self, value: i64) -> Decimal { + Decimal::signed(self, value) } - /// Format the given value using this configuration as an unsigned integer + /// Format the given value using this configuration as an unsigned decimal /// ASCII number. #[cfg_attr(feature = "perf-inline", inline(always))] - pub(crate) const fn format_unsigned(&self, value: u64) -> Integer { - Integer::unsigned(self, value) + pub(crate) const fn format_unsigned(&self, value: u64) -> Decimal { + Decimal::unsigned(self, value) + } + + /// Forces the sign to be rendered, even if it's positive. + /// + /// When `zero_is_positive` is true, then a zero value is formatted with a + /// positive sign. Otherwise, it is formatted with a negative sign. + /// + /// Regardless of this setting, a sign is never emitted when formatting an + /// unsigned integer. + #[cfg(test)] + pub(crate) const fn force_sign( + self, + zero_is_positive: bool, + ) -> DecimalFormatter { + DecimalFormatter { force_sign: Some(zero_is_positive), ..self } } /// The minimum number of digits/padding that this number should be @@ -48,113 +69,181 @@ impl IntegerFormatter { /// /// The minimum number of digits is capped at the maximum number of digits /// for an i64 value (19) or a u64 value (20). - pub(crate) const fn padding(self, mut digits: u8) -> IntegerFormatter { - if digits > Integer::MAX_LEN { - digits = Integer::MAX_LEN; + pub(crate) const fn padding(self, mut digits: u8) -> DecimalFormatter { + if digits > Decimal::MAX_I64_DIGITS { + digits = Decimal::MAX_I64_DIGITS; } - IntegerFormatter { minimum_digits: digits, ..self } + DecimalFormatter { minimum_digits: digits, ..self } } /// The padding byte to use when `padding` is set. /// /// The default is `0`. - pub(crate) const fn padding_byte(self, byte: u8) -> IntegerFormatter { - IntegerFormatter { padding_byte: byte, ..self } + pub(crate) const fn padding_byte(self, byte: u8) -> DecimalFormatter { + DecimalFormatter { padding_byte: byte, ..self } } - /// Returns the minimum number of digits for an integer value. - const fn get_minimum_digits(&self) -> u8 { - if self.minimum_digits <= Integer::MAX_LEN { + /// Returns the minimum number of digits for a signed value. + const fn get_signed_minimum_digits(&self) -> u8 { + if self.minimum_digits <= Decimal::MAX_I64_DIGITS { self.minimum_digits } else { - Integer::MAX_LEN + Decimal::MAX_I64_DIGITS + } + } + + /// Returns the minimum number of digits for an unsigned value. + const fn get_unsigned_minimum_digits(&self) -> u8 { + if self.minimum_digits <= Decimal::MAX_U64_DIGITS { + self.minimum_digits + } else { + Decimal::MAX_U64_DIGITS } } } -impl Default for IntegerFormatter { - fn default() -> IntegerFormatter { - IntegerFormatter::new() +impl Default for DecimalFormatter { + fn default() -> DecimalFormatter { + DecimalFormatter::new() } } -/// A formatted integer number that can be converted to a sequence of bytes. +/// A formatted decimal number that can be converted to a sequence of bytes. #[derive(Debug)] -pub(crate) struct Integer { +pub(crate) struct Decimal { buf: [u8; Self::MAX_LEN as usize], start: u8, + end: u8, } -impl Integer { +impl Decimal { /// Discovered via /// `i64::MIN.to_string().len().max(u64::MAX.to_string().len())`. const MAX_LEN: u8 = 20; + /// Discovered via `i64::MAX.to_string().len()`. + const MAX_I64_DIGITS: u8 = 19; + /// Discovered via `u64::MAX.to_string().len()`. + const MAX_U64_DIGITS: u8 = 20; /// Using the given formatter, turn the value given into an unsigned - /// integer representation using ASCII bytes. + /// decimal representation using ASCII bytes. #[cfg_attr(feature = "perf-inline", inline(always))] const fn unsigned( - formatter: &IntegerFormatter, + formatter: &DecimalFormatter, mut value: u64, - ) -> Integer { - let mut integer = - Integer { buf: [0; Self::MAX_LEN as usize], start: Self::MAX_LEN }; + ) -> Decimal { + let mut decimal = Decimal { + buf: [0; Self::MAX_LEN as usize], + start: Self::MAX_LEN, + end: Self::MAX_LEN, + }; loop { - integer.start -= 1; + decimal.start -= 1; let digit = (value % 10) as u8; value /= 10; - integer.buf[integer.start as usize] = b'0' + digit; + decimal.buf[decimal.start as usize] = b'0' + digit; if value == 0 { break; } } - while integer.len() < formatter.get_minimum_digits() { - integer.start -= 1; - integer.buf[integer.start as usize] = formatter.padding_byte; + while decimal.len() < formatter.get_unsigned_minimum_digits() { + decimal.start -= 1; + decimal.buf[decimal.start as usize] = formatter.padding_byte; } - integer + decimal } - /// Using the given formatter, turn the value given into a signed integer + /// Using the given formatter, turn the value given into a signed decimal /// representation using ASCII bytes. #[cfg_attr(feature = "perf-inline", inline(always))] - const fn signed(formatter: &IntegerFormatter, value: i64) -> Integer { + const fn signed(formatter: &DecimalFormatter, mut value: i64) -> Decimal { // Specialize the common case to generate tighter codegen. - if value >= 0 { - return Integer::unsigned(formatter, value.unsigned_abs()); + if value >= 0 && formatter.force_sign.is_none() { + let mut decimal = Decimal { + buf: [0; Self::MAX_LEN as usize], + start: Self::MAX_LEN, + end: Self::MAX_LEN, + }; + loop { + decimal.start -= 1; + + let digit = (value % 10) as u8; + value /= 10; + decimal.buf[decimal.start as usize] = b'0' + digit; + if value == 0 { + break; + } + } + + while decimal.len() < formatter.get_signed_minimum_digits() { + decimal.start -= 1; + decimal.buf[decimal.start as usize] = formatter.padding_byte; + } + return decimal; } - Integer::signed_cold(formatter, value) + Decimal::signed_cold(formatter, value) } #[cold] #[inline(never)] - const fn signed_cold(formatter: &IntegerFormatter, value: i64) -> Integer { - let mut integer = Integer::unsigned(formatter, value.unsigned_abs()); - if value < 0 { - integer.start -= 1; - integer.buf[integer.start as usize] = b'-'; + const fn signed_cold(formatter: &DecimalFormatter, value: i64) -> Decimal { + let sign = value.signum(); + let Some(mut value) = value.checked_abs() else { + let buf = [ + b'-', b'9', b'2', b'2', b'3', b'3', b'7', b'2', b'0', b'3', + b'6', b'8', b'5', b'4', b'7', b'7', b'5', b'8', b'0', b'8', + ]; + return Decimal { buf, start: 0, end: Self::MAX_LEN }; + }; + let mut decimal = Decimal { + buf: [0; Self::MAX_LEN as usize], + start: Self::MAX_LEN, + end: Self::MAX_LEN, + }; + loop { + decimal.start -= 1; + + let digit = (value % 10) as u8; + value /= 10; + decimal.buf[decimal.start as usize] = b'0' + digit; + if value == 0 { + break; + } } - integer + while decimal.len() < formatter.get_signed_minimum_digits() { + decimal.start -= 1; + decimal.buf[decimal.start as usize] = formatter.padding_byte; + } + if sign < 0 { + decimal.start -= 1; + decimal.buf[decimal.start as usize] = b'-'; + } else if let Some(zero_is_positive) = formatter.force_sign { + let ascii_sign = + if sign > 0 || zero_is_positive { b'+' } else { b'-' }; + decimal.start -= 1; + decimal.buf[decimal.start as usize] = ascii_sign; + } + decimal } /// Returns the total number of ASCII bytes (including the sign) that are - /// used to represent this integer number. + /// used to represent this decimal number. #[inline] const fn len(&self) -> u8 { - Self::MAX_LEN - self.start + self.end - self.start } - /// Returns the ASCII representation of this integer as a byte slice. + /// Returns the ASCII representation of this decimal as a byte slice. /// /// The slice returned is guaranteed to be valid ASCII. #[inline] fn as_bytes(&self) -> &[u8] { - &self.buf[usize::from(self.start)..] + &self.buf[usize::from(self.start)..usize::from(self.end)] } - /// Returns the ASCII representation of this integer as a string slice. + /// Returns the ASCII representation of this decimal as a string slice. #[inline] pub(crate) fn as_str(&self) -> &str { // SAFETY: This is safe because all bytes written to `self.buf` are @@ -373,10 +462,14 @@ impl DurationUnits { if let Some(min) = self.min { if min <= unit { - return Err(Error::from(E::OutOfOrderUnits { - found: unit, - previous: min, - })); + return Err(err!( + "found value {value:?} with unit {unit} \ + after unit {prev_unit}, but units must be \ + written from largest to smallest \ + (and they can't be repeated)", + unit = unit.singular(), + prev_unit = min.singular(), + )); } } // Given the above check, the given unit must be smaller than any we @@ -410,7 +503,12 @@ impl DurationUnits { ) -> Result<(), Error> { if let Some(min) = self.min { if min <= Unit::Hour { - return Err(Error::from(E::OutOfOrderHMS { found: min })); + return Err(err!( + "found `HH:MM:SS` after unit {min}, \ + but `HH:MM:SS` can only appear after \ + years, months, weeks or days", + min = min.singular(), + )); } } self.set_unit_value(Unit::Hour, hours)?; @@ -441,11 +539,15 @@ impl DurationUnits { /// return an error if the minimum unit is bigger than `Unit::Hour`. pub(crate) fn set_fraction(&mut self, fraction: u32) -> Result<(), Error> { assert!(fraction <= 999_999_999); + if self.min == Some(Unit::Nanosecond) { + return Err(err!("fractional nanoseconds are not supported")); + } if let Some(min) = self.min { - if min > Unit::Hour || min == Unit::Nanosecond { - return Err(Error::from(E::NotAllowedFractionalUnit { - found: min, - })); + if min > Unit::Hour { + return Err(err!( + "fractional {plural} are not supported", + plural = min.plural() + )); } } self.fraction = Some(fraction); @@ -540,6 +642,13 @@ impl DurationUnits { #[cold] #[inline(never)] fn to_span_general(&self) -> Result { + fn error_context(unit: Unit, value: i64) -> Error { + err!( + "failed to set value {value:?} as {unit} unit on span", + unit = unit.singular(), + ) + } + #[cfg_attr(feature = "perf-inline", inline(always))] fn set_time_unit( unit: Unit, @@ -573,7 +682,7 @@ impl DurationUnits { set(span) .or_else(|err| fractional_fallback(err, unit, value, span)) - .context(E::FailedValueSet { unit }) + .with_context(|| error_context(unit, value)) } let (min, _) = self.get_min_max_units()?; @@ -583,25 +692,25 @@ impl DurationUnits { let value = self.get_unit_value(Unit::Year)?; span = span .try_years(value) - .context(E::FailedValueSet { unit: Unit::Year })?; + .with_context(|| error_context(Unit::Year, value))?; } if self.values[Unit::Month.as_usize()] != 0 { let value = self.get_unit_value(Unit::Month)?; span = span .try_months(value) - .context(E::FailedValueSet { unit: Unit::Month })?; + .with_context(|| error_context(Unit::Month, value))?; } if self.values[Unit::Week.as_usize()] != 0 { let value = self.get_unit_value(Unit::Week)?; span = span .try_weeks(value) - .context(E::FailedValueSet { unit: Unit::Week })?; + .with_context(|| error_context(Unit::Week, value))?; } if self.values[Unit::Day.as_usize()] != 0 { let value = self.get_unit_value(Unit::Day)?; span = span .try_days(value) - .context(E::FailedValueSet { unit: Unit::Day })?; + .with_context(|| error_context(Unit::Day, value))?; } if self.values[Unit::Hour.as_usize()] != 0 { let value = self.get_unit_value(Unit::Hour)?; @@ -713,7 +822,11 @@ impl DurationUnits { fn to_signed_duration_general(&self) -> Result { let (min, max) = self.get_min_max_units()?; if max > Unit::Hour { - return Err(Error::from(E::NotAllowedCalendarUnit { unit: max })); + return Err(err!( + "parsing {unit} units into a `SignedDuration` is not supported \ + (perhaps try parsing into a `Span` instead)", + unit = max.singular(), + )); } let mut sdur = SignedDuration::ZERO; @@ -721,43 +834,85 @@ impl DurationUnits { let value = self.get_unit_value(Unit::Hour)?; sdur = SignedDuration::try_from_hours(value) .and_then(|nanos| sdur.checked_add(nanos)) - .ok_or(E::OverflowForUnit { unit: Unit::Hour })?; + .ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Hour.singular(), + ) + })?; } if self.values[Unit::Minute.as_usize()] != 0 { let value = self.get_unit_value(Unit::Minute)?; sdur = SignedDuration::try_from_mins(value) .and_then(|nanos| sdur.checked_add(nanos)) - .ok_or(E::OverflowForUnit { unit: Unit::Minute })?; + .ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Minute.singular(), + ) + })?; } if self.values[Unit::Second.as_usize()] != 0 { let value = self.get_unit_value(Unit::Second)?; sdur = SignedDuration::from_secs(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Second })?; + .ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Second.singular(), + ) + })?; } if self.values[Unit::Millisecond.as_usize()] != 0 { let value = self.get_unit_value(Unit::Millisecond)?; sdur = SignedDuration::from_millis(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?; + .ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Millisecond.singular(), + ) + })?; } if self.values[Unit::Microsecond.as_usize()] != 0 { let value = self.get_unit_value(Unit::Microsecond)?; sdur = SignedDuration::from_micros(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?; + .ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Microsecond.singular(), + ) + })?; } if self.values[Unit::Nanosecond.as_usize()] != 0 { let value = self.get_unit_value(Unit::Nanosecond)?; sdur = SignedDuration::from_nanos(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?; + .ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Nanosecond.singular(), + ) + })?; } if let Some(fraction) = self.get_fraction()? { sdur = sdur .checked_add(fractional_duration(min, fraction)?) - .ok_or(E::OverflowForUnitFractional { unit: min })?; + .ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` \ + overflowed when adding 0.{fraction} of unit {unit}", + unit = min.singular(), + ) + })?; } Ok(sdur) @@ -848,12 +1003,19 @@ impl DurationUnits { } if self.sign.is_negative() { - return Err(Error::from(E::NotAllowedNegative)); + return Err(err!( + "cannot parse negative duration into unsigned \ + `std::time::Duration`", + )); } let (min, max) = self.get_min_max_units()?; if max > Unit::Hour { - return Err(Error::from(E::NotAllowedCalendarUnit { unit: max })); + return Err(err!( + "parsing {unit} units into a `std::time::Duration` \ + is not supported (perhaps try parsing into a `Span` instead)", + unit = max.singular(), + )); } let mut sdur = core::time::Duration::ZERO; @@ -861,37 +1023,73 @@ impl DurationUnits { let value = self.values[Unit::Hour.as_usize()]; sdur = try_from_hours(value) .and_then(|nanos| sdur.checked_add(nanos)) - .ok_or(E::OverflowForUnit { unit: Unit::Hour })?; + .ok_or_else(|| { + err!( + "accumulated `std::time::Duration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Hour.singular(), + ) + })?; } if self.values[Unit::Minute.as_usize()] != 0 { let value = self.values[Unit::Minute.as_usize()]; sdur = try_from_mins(value) .and_then(|nanos| sdur.checked_add(nanos)) - .ok_or(E::OverflowForUnit { unit: Unit::Minute })?; + .ok_or_else(|| { + err!( + "accumulated `std::time::Duration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Minute.singular(), + ) + })?; } if self.values[Unit::Second.as_usize()] != 0 { let value = self.values[Unit::Second.as_usize()]; sdur = core::time::Duration::from_secs(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Second })?; + .ok_or_else(|| { + err!( + "accumulated `std::time::Duration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Second.singular(), + ) + })?; } if self.values[Unit::Millisecond.as_usize()] != 0 { let value = self.values[Unit::Millisecond.as_usize()]; sdur = core::time::Duration::from_millis(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?; + .ok_or_else(|| { + err!( + "accumulated `std::time::Duration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Millisecond.singular(), + ) + })?; } if self.values[Unit::Microsecond.as_usize()] != 0 { let value = self.values[Unit::Microsecond.as_usize()]; sdur = core::time::Duration::from_micros(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?; + .ok_or_else(|| { + err!( + "accumulated `std::time::Duration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Microsecond.singular(), + ) + })?; } if self.values[Unit::Nanosecond.as_usize()] != 0 { let value = self.values[Unit::Nanosecond.as_usize()]; sdur = core::time::Duration::from_nanos(value) .checked_add(sdur) - .ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?; + .ok_or_else(|| { + err!( + "accumulated `std::time::Duration` of `{sdur:?}` \ + overflowed when adding {value} of unit {unit}", + unit = Unit::Nanosecond.singular(), + ) + })?; } if let Some(fraction) = self.get_fraction()? { @@ -899,7 +1097,13 @@ impl DurationUnits { .checked_add( fractional_duration(min, fraction)?.unsigned_abs(), ) - .ok_or(E::OverflowForUnitFractional { unit: Unit::Hour })?; + .ok_or_else(|| { + err!( + "accumulated `std::time::Duration` of `{sdur:?}` \ + overflowed when adding 0.{fraction} of unit {unit}", + unit = min.singular(), + ) + })?; } Ok(sdur) @@ -918,7 +1122,7 @@ impl DurationUnits { /// were no parsed duration components.) fn get_min_max_units(&self) -> Result<(Unit, Unit), Error> { let (Some(min), Some(max)) = (self.min, self.max) else { - return Err(Error::from(E::EmptyDuration)); + return Err(err!("no parsed duration components")); }; Ok((min, max)) } @@ -939,12 +1143,21 @@ impl DurationUnits { } // Otherwise, if a conversion to `i64` fails, then that failure // is correct. - let mut value = i64::try_from(value) - .map_err(|_| E::SignedOverflowForUnit { unit })?; + let mut value = i64::try_from(value).map_err(|_| { + err!( + "`{sign}{value}` {unit} is too big (or small) \ + to fit into a signed 64-bit integer", + unit = unit.plural() + ) + })?; if sign.is_negative() { - value = value - .checked_neg() - .ok_or(E::SignedOverflowForUnit { unit })?; + value = value.checked_neg().ok_or_else(|| { + err!( + "`{sign}{value}` {unit} is too big (or small) \ + to fit into a signed 64-bit integer", + unit = unit.plural() + ) + })?; } Ok(value) } @@ -1045,13 +1258,21 @@ pub(crate) fn parse_temporal_fraction<'i>( } let digits = mkdigits(input); if digits.is_empty() { - return Err(Error::from(E::MissingFractionalDigits)); + return Err(err!( + "found decimal after seconds component, \ + but did not find any decimal digits after decimal", + )); } // I believe this error can never happen, since we know we have no more // than 9 ASCII digits. Any sequence of 9 ASCII digits can be parsed // into an `i64`. - let nanoseconds = - parse::fraction(digits).context(E::InvalidFraction)?; + let nanoseconds = parse::fraction(digits).map_err(|err| { + err!( + "failed to parse {digits:?} as fractional component \ + (up to 9 digits, nanosecond precision): {err}", + digits = escape::Bytes(digits), + ) + })?; // OK because parsing is forcefully limited to 9 digits, // which can never be greater than `999_999_99`, // which is less than `u32::MAX`. @@ -1190,10 +1411,18 @@ fn fractional_time_to_span( } if !sdur.is_zero() { let nanos = sdur.as_nanos(); - let nanos64 = - i64::try_from(nanos).map_err(|_| E::InvalidFractionNanos)?; - span = - span.try_nanoseconds(nanos64).context(E::InvalidFractionNanos)?; + let nanos64 = i64::try_from(nanos).map_err(|_| { + err!( + "failed to set nanosecond value {nanos} (it overflows \ + `i64`) on span determined from {value}.{fraction}", + ) + })?; + span = span.try_nanoseconds(nanos64).with_context(|| { + err!( + "failed to set nanosecond value {nanos64} on span \ + determined from {value}.{fraction}", + ) + })?; } Ok(span) @@ -1223,9 +1452,13 @@ fn fractional_time_to_duration( ) -> Result { let sdur = duration_unit_value(unit, value)?; let fraction_dur = fractional_duration(unit, fraction)?; - Ok(sdur - .checked_add(fraction_dur) - .ok_or(E::OverflowForUnitFractional { unit })?) + sdur.checked_add(fraction_dur).ok_or_else(|| { + err!( + "accumulated `SignedDuration` of `{sdur:?}` overflowed \ + when adding `{fraction_dur:?}` (from fractional {unit} units)", + unit = unit.singular(), + ) + }) } /// Converts the fraction of the given unit to a signed duration. @@ -1255,9 +1488,10 @@ fn fractional_duration( Unit::Millisecond => fraction / t::NANOS_PER_MICRO.value(), Unit::Microsecond => fraction / t::NANOS_PER_MILLI.value(), unit => { - return Err(Error::from(E::NotAllowedFractionalUnit { - found: unit, - })); + return Err(err!( + "fractional {unit} units are not allowed", + unit = unit.singular(), + )) } }; Ok(SignedDuration::from_nanos(nanos)) @@ -1282,13 +1516,17 @@ fn duration_unit_value( Unit::Hour => { let seconds = value .checked_mul(t::SECONDS_PER_HOUR.value()) - .ok_or(E::ConversionToSecondsFailed { unit: Unit::Hour })?; + .ok_or_else(|| { + err!("converting {value} hours to seconds overflows i64") + })?; SignedDuration::from_secs(seconds) } Unit::Minute => { let seconds = value .checked_mul(t::SECONDS_PER_MINUTE.value()) - .ok_or(E::ConversionToSecondsFailed { unit: Unit::Minute })?; + .ok_or_else(|| { + err!("converting {value} minutes to seconds overflows i64") + })?; SignedDuration::from_secs(seconds) } Unit::Second => SignedDuration::from_secs(value), @@ -1296,9 +1534,11 @@ fn duration_unit_value( Unit::Microsecond => SignedDuration::from_micros(value), Unit::Nanosecond => SignedDuration::from_nanos(value), unsupported => { - return Err(Error::from(E::NotAllowedCalendarUnit { - unit: unsupported, - })) + return Err(err!( + "parsing {unit} units into a `SignedDuration` is not supported \ + (perhaps try parsing into a `Span` instead)", + unit = unsupported.singular(), + )); } }; Ok(sdur) @@ -1311,30 +1551,43 @@ mod tests { use super::*; #[test] - fn integer() { - let x = IntegerFormatter::new().format_signed(i64::MIN); + fn decimal() { + let x = DecimalFormatter::new().format_signed(i64::MIN); assert_eq!(x.as_str(), "-9223372036854775808"); - let x = IntegerFormatter::new().format_signed(i64::MIN + 1); + let x = DecimalFormatter::new().format_signed(i64::MIN + 1); assert_eq!(x.as_str(), "-9223372036854775807"); - let x = IntegerFormatter::new().format_signed(i64::MAX); + let x = DecimalFormatter::new().format_signed(i64::MAX); assert_eq!(x.as_str(), "9223372036854775807"); - let x = IntegerFormatter::new().format_signed(0); + let x = + DecimalFormatter::new().force_sign(true).format_signed(i64::MAX); + assert_eq!(x.as_str(), "+9223372036854775807"); + + let x = DecimalFormatter::new().format_signed(0); assert_eq!(x.as_str(), "0"); - let x = IntegerFormatter::new().padding(4).format_signed(0); + let x = DecimalFormatter::new().force_sign(true).format_signed(0); + assert_eq!(x.as_str(), "+0"); + + let x = DecimalFormatter::new().force_sign(false).format_signed(0); + assert_eq!(x.as_str(), "-0"); + + let x = DecimalFormatter::new().padding(4).format_signed(0); assert_eq!(x.as_str(), "0000"); - let x = IntegerFormatter::new().padding(4).format_signed(789); + let x = DecimalFormatter::new().padding(4).format_signed(789); assert_eq!(x.as_str(), "0789"); - let x = IntegerFormatter::new().padding(4).format_signed(-789); + let x = DecimalFormatter::new().padding(4).format_signed(-789); assert_eq!(x.as_str(), "-0789"); - let x = IntegerFormatter::new().padding(4).format_signed(789); - assert_eq!(x.as_str(), "0789"); + let x = DecimalFormatter::new() + .force_sign(true) + .padding(4) + .format_signed(789); + assert_eq!(x.as_str(), "+0789"); } #[test] diff --git a/src/logging.rs b/src/logging.rs index 9a4076b..b0eb6c7 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -87,10 +87,10 @@ impl Logger { /// Create a new logger that logs to stderr and initialize it as the /// global logger. If there was a problem setting the logger, then an /// error is returned. - pub(crate) fn init() -> Result<(), log::SetLoggerError> { + pub(crate) fn init() -> Result<(), crate::Error> { #[cfg(all(feature = "std", feature = "logging"))] { - log::set_logger(LOGGER)?; + log::set_logger(LOGGER).map_err(crate::Error::adhoc)?; log::set_max_level(log::LevelFilter::Trace); Ok(()) } diff --git a/src/now.rs b/src/now.rs index c9725c3..6865850 100644 --- a/src/now.rs +++ b/src/now.rs @@ -83,16 +83,15 @@ mod sys { } else { SystemTime::UNIX_EPOCH.checked_sub(duration) }; - // It's a little sad that we have to panic here, but the - // standard SystemTime::now() API is infallible, so we kind - // of have to match it. With that said, a panic here would be - // highly unusual. It would imply that the system time is set - // to some extreme timestamp very far in the future or the - // past. + // It's a little sad that we have to panic here, but the standard + // SystemTime::now() API is infallible, so we kind of have to match it. + // With that said, a panic here would be highly unusual. It would imply + // that the system time is set to some extreme timestamp very far in the + // future or the past. let Some(timestamp) = result else { panic!( - "failed to get current time from Javascript date: \ - arithmetic on Unix epoch overflowed" + "failed to get current time: \ + subtracting {duration:?} from Unix epoch overflowed" ) }; timestamp diff --git a/src/shared/posix.rs b/src/shared/posix.rs index 62c58ef..2f5a7bd 100644 --- a/src/shared/posix.rs +++ b/src/shared/posix.rs @@ -3,6 +3,8 @@ use core::fmt::Debug; use super::{ util::{ array_str::Abbreviation, + error::{err, Error}, + escape::{Byte, Bytes}, itime::{ IAmbiguousOffset, IDate, IDateTime, IOffset, ITime, ITimeSecond, ITimestamp, IWeekday, @@ -15,9 +17,8 @@ use super::{ impl PosixTimeZone { /// Parse a POSIX `TZ` environment variable, assuming it's a rule and not /// an implementation defined value, from the given bytes. - pub fn parse( - bytes: &[u8], - ) -> Result, PosixTimeZoneError> { + #[cfg(feature = "alloc")] + pub fn parse(bytes: &[u8]) -> Result, Error> { // We enable the IANA v3+ extensions here. (Namely, that the time // specification hour value has the range `-167..=167` instead of // `0..=24`.) Requiring strict POSIX rules doesn't seem necessary @@ -30,10 +31,10 @@ impl PosixTimeZone { // only-jiff-start /// Like parse, but parses a prefix of the input given and returns whatever /// is remaining. + #[cfg(feature = "alloc")] pub fn parse_prefix<'b>( bytes: &'b [u8], - ) -> Result<(PosixTimeZone, &'b [u8]), PosixTimeZoneError> - { + ) -> Result<(PosixTimeZone, &'b [u8]), Error> { let parser = Parser { ianav3plus: true, ..Parser::new(bytes) }; parser.parse_prefix() } @@ -343,11 +344,12 @@ impl + Debug> PosixTimeZone { impl> core::fmt::Display for PosixTimeZone { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt( - &AbbreviationDisplay(self.std_abbrev.as_ref()), + write!( f, + "{}{}", + AbbreviationDisplay(self.std_abbrev.as_ref()), + self.std_offset )?; - core::fmt::Display::fmt(&self.std_offset, f)?; if let Some(ref dst) = self.dst { dst.display(&self.std_offset, f)?; } @@ -361,29 +363,22 @@ impl> PosixDst { std_offset: &PosixOffset, f: &mut core::fmt::Formatter, ) -> core::fmt::Result { - core::fmt::Display::fmt( - &AbbreviationDisplay(self.abbrev.as_ref()), - f, - )?; + write!(f, "{}", AbbreviationDisplay(self.abbrev.as_ref()))?; // The overwhelming common case is that DST is exactly one hour ahead // of standard time. So common that this is the default. So don't write // the offset if we don't need to. let default = PosixOffset { second: std_offset.second + 3600 }; if self.offset != default { - core::fmt::Display::fmt(&self.offset, f)?; + write!(f, "{}", self.offset)?; } - f.write_str(",")?; - core::fmt::Display::fmt(&self.rule, f)?; + write!(f, ",{}", self.rule)?; Ok(()) } } impl core::fmt::Display for PosixRule { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt(&self.start, f)?; - f.write_str(",")?; - core::fmt::Display::fmt(&self.end, f)?; - Ok(()) + write!(f, "{},{}", self.start, self.end) } } @@ -434,12 +429,11 @@ impl PosixDayTime { impl core::fmt::Display for PosixDayTime { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::Display::fmt(&self.date, f)?; + write!(f, "{}", self.date)?; // This is the default time, so don't write it if we // don't need to. if self.time != PosixTime::DEFAULT { - f.write_str("/")?; - core::fmt::Display::fmt(&self.time, f)?; + write!(f, "/{}", self.time)?; } Ok(()) } @@ -507,19 +501,10 @@ impl PosixDay { impl core::fmt::Display for PosixDay { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { - PosixDay::JulianOne(n) => { - f.write_str("J")?; - core::fmt::Display::fmt(&n, f) - } - PosixDay::JulianZero(n) => core::fmt::Display::fmt(&n, f), + PosixDay::JulianOne(n) => write!(f, "J{n}"), + PosixDay::JulianZero(n) => write!(f, "{n}"), PosixDay::WeekdayOfMonth { month, week, weekday } => { - f.write_str("M")?; - core::fmt::Display::fmt(&month, f)?; - f.write_str(".")?; - core::fmt::Display::fmt(&week, f)?; - f.write_str(".")?; - core::fmt::Display::fmt(&weekday, f)?; - Ok(()) + write!(f, "M{month}.{week}.{weekday}") } } } @@ -532,7 +517,7 @@ impl PosixTime { impl core::fmt::Display for PosixTime { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { if self.second.is_negative() { - f.write_str("-")?; + write!(f, "-")?; // The default is positive, so when // positive, we write nothing. } @@ -540,7 +525,7 @@ impl core::fmt::Display for PosixTime { let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - core::fmt::Display::fmt(&h, f)?; + write!(f, "{h}")?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -563,13 +548,13 @@ impl core::fmt::Display for PosixOffset { // N.B. `+` is the default, so we don't // need to write that out. if self.second > 0 { - f.write_str("-")?; + write!(f, "-")?; } let second = self.second.unsigned_abs(); let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - core::fmt::Display::fmt(&h, f)?; + write!(f, "{h}")?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -591,11 +576,9 @@ impl> core::fmt::Display for AbbreviationDisplay { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { let s = self.0.as_ref(); if s.chars().any(|ch| ch == '+' || ch == '-') { - f.write_str("<")?; - core::fmt::Display::fmt(&s, f)?; - f.write_str(">") + write!(f, "<{s}>") } else { - core::fmt::Display::fmt(&s, f) + write!(f, "{s}") } } } @@ -689,12 +672,15 @@ impl<'s> Parser<'s> { /// Parses a POSIX time zone from the current position of the parser and /// ensures that the entire TZ string corresponds to a single valid POSIX /// time zone. - fn parse( - &self, - ) -> Result, PosixTimeZoneError> { + fn parse(&self) -> Result, Error> { let (time_zone, remaining) = self.parse_prefix()?; if !remaining.is_empty() { - return Err(ErrorKind::FoundRemaining.into()); + return Err(err!( + "expected entire TZ string to be a valid POSIX \ + time zone, but found `{}` after what would otherwise \ + be a valid POSIX TZ string", + Bytes(remaining), + )); } Ok(time_zone) } @@ -703,8 +689,7 @@ impl<'s> Parser<'s> { /// returns the remaining input. fn parse_prefix( &self, - ) -> Result<(PosixTimeZone, &'s [u8]), PosixTimeZoneError> - { + ) -> Result<(PosixTimeZone, &'s [u8]), Error> { let time_zone = self.parse_posix_time_zone()?; Ok((time_zone, self.remaining())) } @@ -715,14 +700,18 @@ impl<'s> Parser<'s> { /// TZ string. fn parse_posix_time_zone( &self, - ) -> Result, PosixTimeZoneError> { + ) -> Result, Error> { if self.is_done() { - return Err(ErrorKind::Empty.into()); + return Err(err!( + "an empty string is not a valid POSIX time zone" + )); } - let std_abbrev = - self.parse_abbreviation().map_err(ErrorKind::AbbreviationStd)?; - let std_offset = - self.parse_posix_offset().map_err(ErrorKind::OffsetStd)?; + let std_abbrev = self + .parse_abbreviation() + .map_err(|e| err!("failed to parse standard abbreviation: {e}"))?; + let std_offset = self + .parse_posix_offset() + .map_err(|e| err!("failed to parse standard offset: {e}"))?; let mut dst = None; if !self.is_done() && (self.byte().is_ascii_alphabetic() || self.byte() == b'<') @@ -743,30 +732,49 @@ impl<'s> Parser<'s> { fn parse_posix_dst( &self, std_offset: &PosixOffset, - ) -> Result, PosixTimeZoneError> { - let abbrev = - self.parse_abbreviation().map_err(ErrorKind::AbbreviationDst)?; + ) -> Result, Error> { + let abbrev = self + .parse_abbreviation() + .map_err(|e| err!("failed to parse DST abbreviation: {e}"))?; if self.is_done() { - return Err(ErrorKind::FoundDstNoRule.into()); + return Err(err!( + "found DST abbreviation `{abbrev}`, but no transition \ + rule (this is technically allowed by POSIX, but has \ + unspecified behavior)", + )); } // This is the default: one hour ahead of standard time. We may // override this if the DST portion specifies an offset. (But it // usually doesn't.) let mut offset = PosixOffset { second: std_offset.second + 3600 }; if self.byte() != b',' { - offset = - self.parse_posix_offset().map_err(ErrorKind::OffsetDst)?; + offset = self + .parse_posix_offset() + .map_err(|e| err!("failed to parse DST offset: {e}"))?; if self.is_done() { - return Err(ErrorKind::FoundDstNoRuleWithOffset.into()); + return Err(err!( + "found DST abbreviation `{abbrev}` and offset \ + `{offset}s`, but no transition rule (this is \ + technically allowed by POSIX, but has \ + unspecified behavior)", + offset = offset.second, + )); } } if self.byte() != b',' { - return Err(ErrorKind::ExpectedCommaAfterDst.into()); + return Err(err!( + "after parsing DST offset in POSIX time zone string, \ + found `{}` but expected a ','", + Byte(self.byte()), + )); } if !self.bump() { - return Err(ErrorKind::FoundEndAfterComma.into()); + return Err(err!( + "after parsing DST offset in POSIX time zone string, \ + found end of string after a trailing ','", + )); } - let rule = self.parse_rule().map_err(ErrorKind::Rule)?; + let rule = self.parse_rule()?; Ok(PosixDst { abbrev, offset, rule }) } @@ -782,17 +790,18 @@ impl<'s> Parser<'s> { /// The string returned is guaranteed to be no more than 30 bytes. /// (This restriction is somewhat arbitrary, but it's so we can put /// the abbreviation in a fixed capacity array.) - fn parse_abbreviation(&self) -> Result { + fn parse_abbreviation(&self) -> Result { if self.byte() == b'<' { if !self.bump() { - return Err(AbbreviationError::Quoted( - QuotedAbbreviationError::UnexpectedEndAfterOpening, + return Err(err!( + "found opening '<' quote for abbreviation in \ + POSIX time zone string, and expected a name \ + following it, but found the end of string instead" )); } - self.parse_quoted_abbreviation().map_err(AbbreviationError::Quoted) + self.parse_quoted_abbreviation() } else { self.parse_unquoted_abbreviation() - .map_err(AbbreviationError::Unquoted) } } @@ -807,16 +816,19 @@ impl<'s> Parser<'s> { /// The string returned is guaranteed to be no more than 30 bytes. /// (This restriction is somewhat arbitrary, but it's so we can put /// the abbreviation in a fixed capacity array.) - fn parse_unquoted_abbreviation( - &self, - ) -> Result { + fn parse_unquoted_abbreviation(&self) -> Result { let start = self.pos(); for i in 0.. { if !self.byte().is_ascii_alphabetic() { break; } if i >= Abbreviation::capacity() { - return Err(UnquotedAbbreviationError::TooLong); + return Err(err!( + "expected abbreviation with at most {} bytes, \ + but found a longer abbreviation beginning with `{}`", + Abbreviation::capacity(), + Bytes(&self.tz[start..][..i]), + )); } if !self.bump() { break; @@ -831,10 +843,18 @@ impl<'s> Parser<'s> { // `end` is ASCII and thus should be UTF-8. But it doesn't // cost us anything to report an error here in case the // code above evolves somehow. - UnquotedAbbreviationError::InvalidUtf8 + err!( + "found abbreviation `{}`, but it is not valid UTF-8", + Bytes(&self.tz[start..end]), + ) })?; if abbrev.len() < 3 { - return Err(UnquotedAbbreviationError::TooShort); + return Err(err!( + "expected abbreviation with 3 or more bytes, but found \ + abbreviation {:?} with {} bytes", + abbrev, + abbrev.len(), + )); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -852,9 +872,7 @@ impl<'s> Parser<'s> { /// The string returned is guaranteed to be no more than 30 bytes. /// (This restriction is somewhat arbitrary, but it's so we can put /// the abbreviation in a fixed capacity array.) - fn parse_quoted_abbreviation( - &self, - ) -> Result { + fn parse_quoted_abbreviation(&self) -> Result { let start = self.pos(); for i in 0.. { if !self.byte().is_ascii_alphanumeric() @@ -864,7 +882,12 @@ impl<'s> Parser<'s> { break; } if i >= Abbreviation::capacity() { - return Err(QuotedAbbreviationError::TooLong); + return Err(err!( + "expected abbreviation with at most {} bytes, \ + but found a longer abbreviation beginning with `{}`", + Abbreviation::capacity(), + Bytes(&self.tz[start..][..i]), + )); } if !self.bump() { break; @@ -879,17 +902,33 @@ impl<'s> Parser<'s> { // `end` is ASCII and thus should be UTF-8. But it doesn't // cost us anything to report an error here in case the // code above evolves somehow. - QuotedAbbreviationError::InvalidUtf8 + err!( + "found abbreviation `{}`, but it is not valid UTF-8", + Bytes(&self.tz[start..end]), + ) })?; if self.is_done() { - return Err(QuotedAbbreviationError::UnexpectedEnd); + return Err(err!( + "found non-empty quoted abbreviation {abbrev:?}, but \ + did not find expected end-of-quoted abbreviation \ + '>' character", + )); } if self.byte() != b'>' { - return Err(QuotedAbbreviationError::UnexpectedLastByte); + return Err(err!( + "found non-empty quoted abbreviation {abbrev:?}, but \ + found `{}` instead of end-of-quoted abbreviation '>' \ + character", + Byte(self.byte()), + )); } self.bump(); if abbrev.len() < 3 { - return Err(QuotedAbbreviationError::TooShort); + return Err(err!( + "expected abbreviation with 3 or more bytes, but found \ + abbreviation {abbrev:?} with {} bytes", + abbrev.len(), + )); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -904,18 +943,30 @@ impl<'s> Parser<'s> { /// /// Upon success, the parser will be positioned immediately after the /// end of the offset. - fn parse_posix_offset(&self) -> Result { - let sign = self.parse_optional_sign()?.unwrap_or(1); + fn parse_posix_offset(&self) -> Result { + let sign = self + .parse_optional_sign() + .map_err(|e| { + err!( + "failed to parse sign for time offset \ + in POSIX time zone string: {e}", + ) + })? + .unwrap_or(1); let hour = self.parse_hour_posix()?; let (mut minute, mut second) = (0, 0); if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixOffsetError::IncompleteMinutes); + return Err(err!( + "incomplete time in POSIX timezone (missing minutes)", + )); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixOffsetError::IncompleteSeconds); + return Err(err!( + "incomplete time in POSIX timezone (missing seconds)", + )); } second = self.parse_second()?; } @@ -946,16 +997,19 @@ impl<'s> Parser<'s> { /// Upon success, the parser will be positioned immediately after the /// DST transition rule. In typical cases, this corresponds to the end /// of the TZ string. - fn parse_rule(&self) -> Result { - let start = self - .parse_posix_datetime() - .map_err(PosixRuleError::DateTimeStart)?; + fn parse_rule(&self) -> Result { + let start = self.parse_posix_datetime().map_err(|e| { + err!("failed to parse start of DST transition rule: {e}") + })?; if self.maybe_byte() != Some(b',') || !self.bump() { - return Err(PosixRuleError::ExpectedEnd); + return Err(err!( + "expected end of DST rule after parsing the start \ + of the DST rule" + )); } - let end = self - .parse_posix_datetime() - .map_err(PosixRuleError::DateTimeEnd)?; + let end = self.parse_posix_datetime().map_err(|e| { + err!("failed to parse end of DST transition rule: {e}") + })?; Ok(PosixRule { start, end }) } @@ -967,9 +1021,7 @@ impl<'s> Parser<'s> { /// Upon success, the parser will be positioned after the datetime /// specification. This will either be immediately after the date, or /// if it's present, the time part of the specification. - fn parse_posix_datetime( - &self, - ) -> Result { + fn parse_posix_datetime(&self) -> Result { let mut daytime = PosixDayTime { date: self.parse_posix_date()?, time: PosixTime::DEFAULT, @@ -978,7 +1030,10 @@ impl<'s> Parser<'s> { return Ok(daytime); } if !self.bump() { - return Err(PosixDateTimeError::ExpectedTime); + return Err(err!( + "expected time specification after '/' following a date + specification in a POSIX time zone DST transition rule", + )); } daytime.time = self.parse_posix_time()?; Ok(daytime) @@ -997,11 +1052,16 @@ impl<'s> Parser<'s> { /// /// Upon success, the parser will be positioned immediately after the /// date specification. - fn parse_posix_date(&self) -> Result { + fn parse_posix_date(&self) -> Result { match self.byte() { b'J' => { if !self.bump() { - return Err(PosixDateError::ExpectedJulianNoLeap); + return Err(err!( + "expected one-based Julian day after 'J' in date \ + specification of a POSIX time zone DST \ + transition rule, but got the end of the string \ + instead" + )); } Ok(PosixDay::JulianOne(self.parse_posix_julian_day_no_leap()?)) } @@ -1010,12 +1070,22 @@ impl<'s> Parser<'s> { )), b'M' => { if !self.bump() { - return Err(PosixDateError::ExpectedMonthWeekWeekday); + return Err(err!( + "expected month-week-weekday after 'M' in date \ + specification of a POSIX time zone DST \ + transition rule, but got the end of the string \ + instead" + )); } let (month, week, weekday) = self.parse_weekday_of_month()?; Ok(PosixDay::WeekdayOfMonth { month, week, weekday }) } - _ => Err(PosixDateError::UnexpectedByte), + _ => Err(err!( + "expected 'J', a digit or 'M' at the beginning of a date \ + specification of a POSIX time zone DST transition rule, \ + but got `{}` instead", + Byte(self.byte()), + )), } } @@ -1025,16 +1095,22 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned just after the `J` and at the /// first digit of the Julian day. Upon success, the parser will be /// positioned immediately following the day number. - fn parse_posix_julian_day_no_leap( - &self, - ) -> Result { + fn parse_posix_julian_day_no_leap(&self) -> Result { let number = self .parse_number_with_upto_n_digits(3) - .map_err(PosixJulianNoLeapError::Parse)?; - let number = i16::try_from(number) - .map_err(|_| PosixJulianNoLeapError::Range)?; + .map_err(|e| err!("invalid one based Julian day: {e}"))?; + let number = i16::try_from(number).map_err(|_| { + err!( + "one based Julian day `{number}` in POSIX time zone \ + does not fit into 16-bit integer" + ) + })?; if !(1 <= number && number <= 365) { - return Err(PosixJulianNoLeapError::Range); + return Err(err!( + "parsed one based Julian day `{number}`, \ + but one based Julian day in POSIX time zone \ + must be in range 1..=365", + )); } Ok(number) } @@ -1045,16 +1121,22 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the first digit of the /// Julian day. Upon success, the parser will be positioned immediately /// following the day number. - fn parse_posix_julian_day_with_leap( - &self, - ) -> Result { + fn parse_posix_julian_day_with_leap(&self) -> Result { let number = self .parse_number_with_upto_n_digits(3) - .map_err(PosixJulianLeapError::Parse)?; - let number = - i16::try_from(number).map_err(|_| PosixJulianLeapError::Range)?; + .map_err(|e| err!("invalid zero based Julian day: {e}"))?; + let number = i16::try_from(number).map_err(|_| { + err!( + "zero based Julian day `{number}` in POSIX time zone \ + does not fit into 16-bit integer" + ) + })?; if !(0 <= number && number <= 365) { - return Err(PosixJulianLeapError::Range); + return Err(err!( + "parsed zero based Julian day `{number}`, \ + but zero based Julian day in POSIX time zone \ + must be in range 0..=365", + )); } Ok(number) } @@ -1068,22 +1150,31 @@ impl<'s> Parser<'s> { /// /// The tuple returned is month (1..=12), week (1..=5) and weekday /// (0..=6 with 0=Sunday). - fn parse_weekday_of_month( - &self, - ) -> Result<(i8, i8, i8), WeekdayOfMonthError> { + fn parse_weekday_of_month(&self) -> Result<(i8, i8, i8), Error> { let month = self.parse_month()?; if self.maybe_byte() != Some(b'.') { - return Err(WeekdayOfMonthError::ExpectedDotAfterMonth); + return Err(err!( + "expected '.' after month `{month}` in \ + POSIX time zone rule" + )); } if !self.bump() { - return Err(WeekdayOfMonthError::ExpectedWeekAfterMonth); + return Err(err!( + "expected week after month `{month}` in \ + POSIX time zone rule" + )); } let week = self.parse_week()?; if self.maybe_byte() != Some(b'.') { - return Err(WeekdayOfMonthError::ExpectedDotAfterWeek); + return Err(err!( + "expected '.' after week `{week}` in POSIX time zone rule" + )); } if !self.bump() { - return Err(WeekdayOfMonthError::ExpectedDayOfWeekAfterWeek); + return Err(err!( + "expected day-of-week after week `{week}` in \ + POSIX time zone rule" + )); } let weekday = self.parse_weekday()?; Ok((month, week, weekday)) @@ -1095,9 +1186,17 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the first `h` (or the /// sign, if present). Upon success, the parser will be positioned /// immediately following the end of the time specification. - fn parse_posix_time(&self) -> Result { + fn parse_posix_time(&self) -> Result { let (sign, hour) = if self.ianav3plus { - let sign = self.parse_optional_sign()?.unwrap_or(1); + let sign = self + .parse_optional_sign() + .map_err(|e| { + err!( + "failed to parse sign for transition time \ + in POSIX time zone string: {e}", + ) + })? + .unwrap_or(1); let hour = self.parse_hour_ianav3plus()?; (sign, hour) } else { @@ -1106,12 +1205,18 @@ impl<'s> Parser<'s> { let (mut minute, mut second) = (0, 0); if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixTimeError::IncompleteMinutes); + return Err(err!( + "incomplete transition time in \ + POSIX time zone string (missing minutes)", + )); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(PosixTimeError::IncompleteSeconds); + return Err(err!( + "incomplete transition time in \ + POSIX time zone string (missing seconds)", + )); } second = self.parse_second()?; } @@ -1136,13 +1241,19 @@ impl<'s> Parser<'s> { /// This is expected to be positioned at the first digit. Upon success, /// the parser will be positioned after the month (which may contain /// two digits). - fn parse_month(&self) -> Result { - let number = self - .parse_number_with_upto_n_digits(2) - .map_err(MonthError::Parse)?; - let number = i8::try_from(number).map_err(|_| MonthError::Range)?; + fn parse_month(&self) -> Result { + let number = self.parse_number_with_upto_n_digits(2)?; + let number = i8::try_from(number).map_err(|_| { + err!( + "month `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(1 <= number && number <= 12) { - return Err(MonthError::Range); + return Err(err!( + "parsed month `{number}`, but month in \ + POSIX time zone must be in range 1..=12", + )); } Ok(number) } @@ -1151,14 +1262,19 @@ impl<'s> Parser<'s> { /// /// This is expected to be positioned at the first digit. Upon success, /// the parser will be positioned after the week digit. - fn parse_week(&self) -> Result { - let number = self - .parse_number_with_exactly_n_digits(1) - .map_err(WeekOfMonthError::Parse)?; - let number = - i8::try_from(number).map_err(|_| WeekOfMonthError::Range)?; + fn parse_week(&self) -> Result { + let number = self.parse_number_with_exactly_n_digits(1)?; + let number = i8::try_from(number).map_err(|_| { + err!( + "week `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(1 <= number && number <= 5) { - return Err(WeekOfMonthError::Range); + return Err(err!( + "parsed week `{number}`, but week in \ + POSIX time zone must be in range 1..=5" + )); } Ok(number) } @@ -1170,13 +1286,20 @@ impl<'s> Parser<'s> { /// /// The weekday returned is guaranteed to be in the range `0..=6`, with /// `0` corresponding to Sunday. - fn parse_weekday(&self) -> Result { - let number = self - .parse_number_with_exactly_n_digits(1) - .map_err(WeekdayError::Parse)?; - let number = i8::try_from(number).map_err(|_| WeekdayError::Range)?; + fn parse_weekday(&self) -> Result { + let number = self.parse_number_with_exactly_n_digits(1)?; + let number = i8::try_from(number).map_err(|_| { + err!( + "weekday `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 6) { - return Err(WeekdayError::Range); + return Err(err!( + "parsed weekday `{number}`, but weekday in \ + POSIX time zone must be in range `0..=6` \ + (with `0` corresponding to Sunday)", + )); } Ok(number) } @@ -1192,20 +1315,27 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first hour digit should occur. Upon success, the parser will be /// positioned immediately after the last hour digit. - fn parse_hour_ianav3plus(&self) -> Result { + fn parse_hour_ianav3plus(&self) -> Result { // Callers should only be using this method when IANA v3+ parsing // is enabled. assert!(self.ianav3plus); let number = self .parse_number_with_upto_n_digits(3) - .map_err(HourIanaError::Parse)?; - let number = - i16::try_from(number).map_err(|_| HourIanaError::Range)?; + .map_err(|e| err!("invalid hour digits: {e}"))?; + let number = i16::try_from(number).map_err(|_| { + err!( + "hour `{number}` in POSIX time zone \ + does not fit into 16-bit integer" + ) + })?; if !(0 <= number && number <= 167) { // The error message says -167 but the check above uses 0. // This is because the caller is responsible for parsing // the sign. - return Err(HourIanaError::Range); + return Err(err!( + "parsed hour `{number}`, but hour in IANA v3+ \ + POSIX time zone must be in range `-167..=167`", + )); } Ok(number) } @@ -1219,14 +1349,21 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first hour digit should occur. Upon success, the parser will be /// positioned immediately after the last hour digit. - fn parse_hour_posix(&self) -> Result { + fn parse_hour_posix(&self) -> Result { let number = self .parse_number_with_upto_n_digits(2) - .map_err(HourPosixError::Parse)?; - let number = - i8::try_from(number).map_err(|_| HourPosixError::Range)?; + .map_err(|e| err!("invalid hour digits: {e}"))?; + let number = i8::try_from(number).map_err(|_| { + err!( + "hour `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 24) { - return Err(HourPosixError::Range); + return Err(err!( + "parsed hour `{number}`, but hour in \ + POSIX time zone must be in range `0..=24`", + )); } Ok(number) } @@ -1238,13 +1375,21 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first minute digit should occur. Upon success, the parser will be /// positioned immediately after the second minute digit. - fn parse_minute(&self) -> Result { + fn parse_minute(&self) -> Result { let number = self .parse_number_with_exactly_n_digits(2) - .map_err(MinuteError::Parse)?; - let number = i8::try_from(number).map_err(|_| MinuteError::Range)?; + .map_err(|e| err!("invalid minute digits: {e}"))?; + let number = i8::try_from(number).map_err(|_| { + err!( + "minute `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 59) { - return Err(MinuteError::Range); + return Err(err!( + "parsed minute `{number}`, but minute in \ + POSIX time zone must be in range `0..=59`", + )); } Ok(number) } @@ -1256,13 +1401,21 @@ impl<'s> Parser<'s> { /// This assumes the parser is positioned at the position where the /// first second digit should occur. Upon success, the parser will be /// positioned immediately after the second second digit. - fn parse_second(&self) -> Result { + fn parse_second(&self) -> Result { let number = self .parse_number_with_exactly_n_digits(2) - .map_err(SecondError::Parse)?; - let number = i8::try_from(number).map_err(|_| SecondError::Range)?; + .map_err(|e| err!("invalid second digits: {e}"))?; + let number = i8::try_from(number).map_err(|_| { + err!( + "second `{number}` in POSIX time zone \ + does not fit into 8-bit integer" + ) + })?; if !(0 <= number && number <= 59) { - return Err(SecondError::Range); + return Err(err!( + "parsed second `{number}`, but second in \ + POSIX time zone must be in range `0..=59`", + )); } Ok(number) } @@ -1278,20 +1431,27 @@ impl<'s> Parser<'s> { fn parse_number_with_exactly_n_digits( &self, n: usize, - ) -> Result { + ) -> Result { assert!(n >= 1, "numbers must have at least 1 digit"); + let start = self.pos(); let mut number: i32 = 0; - for _ in 0..n { + for i in 0..n { if self.is_done() { - return Err(NumberError::ExpectedLength); + return Err(err!("expected {n} digits, but found {i}")); } let byte = self.byte(); let digit = match byte.checked_sub(b'0') { None => { - return Err(NumberError::InvalidDigit); + return Err(err!( + "invalid digit, expected 0-9 but got {}", + Byte(byte), + )); } Some(digit) if digit > 9 => { - return Err(NumberError::InvalidDigit); + return Err(err!( + "invalid digit, expected 0-9 but got {}", + Byte(byte), + )) } Some(digit) => { debug_assert!((0..=9).contains(&digit)); @@ -1301,7 +1461,12 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or(NumberError::TooBig)?; + .ok_or_else(|| { + err!( + "number `{}` too big to parse into 64-bit integer", + Bytes(&self.tz[start..][..i]), + ) + })?; self.bump(); } Ok(number) @@ -1313,16 +1478,14 @@ impl<'s> Parser<'s> { /// This assumes that `n >= 1` and that the parser is positioned at the /// first digit. Upon success, the parser is position immediately after /// the last digit (which can be at most `n`). - fn parse_number_with_upto_n_digits( - &self, - n: usize, - ) -> Result { + fn parse_number_with_upto_n_digits(&self, n: usize) -> Result { assert!(n >= 1, "numbers must have at least 1 digit"); + let start = self.pos(); let mut number: i32 = 0; for i in 0..n { if self.is_done() || !self.byte().is_ascii_digit() { if i == 0 { - return Err(NumberError::Empty); + return Err(err!("invalid number, no digits found")); } break; } @@ -1330,7 +1493,12 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or(NumberError::TooBig)?; + .ok_or_else(|| { + err!( + "number `{}` too big to parse into 64-bit integer", + Bytes(&self.tz[start..][..i]), + ) + })?; self.bump(); } Ok(number) @@ -1343,20 +1511,26 @@ impl<'s> Parser<'s> { /// is consumed and returned. Moreover, if one exists, then this /// guarantees that it is not the last byte in the input. That is, upon /// success, it is valid to call `self.byte()`. - fn parse_optional_sign(&self) -> Result, OptionalSignError> { + fn parse_optional_sign(&self) -> Result, Error> { if self.is_done() { return Ok(None); } Ok(match self.byte() { b'-' => { if !self.bump() { - return Err(OptionalSignError::ExpectedDigitAfterMinus); + return Err(err!( + "expected digit after '-' sign, \ + but got end of input", + )); } Some(-1) } b'+' => { if !self.bump() { - return Err(OptionalSignError::ExpectedDigitAfterPlus); + return Err(err!( + "expected digit after '+' sign, \ + but got end of input", + )); } Some(1) } @@ -1414,717 +1588,12 @@ impl<'s> Parser<'s> { } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PosixTimeZoneError { - kind: ErrorKind, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum ErrorKind { - AbbreviationDst(AbbreviationError), - AbbreviationStd(AbbreviationError), - Empty, - ExpectedCommaAfterDst, - FoundDstNoRule, - FoundDstNoRuleWithOffset, - FoundEndAfterComma, - FoundRemaining, - OffsetDst(PosixOffsetError), - OffsetStd(PosixOffsetError), - Rule(PosixRuleError), -} - -impl core::fmt::Display for PosixTimeZoneError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::ErrorKind::*; - match self.kind { - AbbreviationDst(ref err) => { - f.write_str("failed to parse DST time zone abbreviation: ")?; - core::fmt::Display::fmt(err, f) - } - AbbreviationStd(ref err) => { - f.write_str( - "failed to parse standard time zone abbreviation: ", - )?; - core::fmt::Display::fmt(err, f) - } - Empty => f.write_str( - "an empty string is not a valid POSIX time zone \ - transition rule", - ), - ExpectedCommaAfterDst => f.write_str( - "expected `,` after parsing DST offset \ - in POSIX time zone string", - ), - FoundDstNoRule => f.write_str( - "found DST abbreviation in POSIX time zone string, \ - but no transition rule \ - (this is technically allowed by POSIX, but has \ - unspecified behavior)", - ), - FoundDstNoRuleWithOffset => f.write_str( - "found DST abbreviation and offset in POSIX time zone string, \ - but no transition rule \ - (this is technically allowed by POSIX, but has \ - unspecified behavior)", - ), - FoundEndAfterComma => f.write_str( - "after parsing DST offset in POSIX time zone string, \ - found end of string after a trailing `,`", - ), - FoundRemaining => f.write_str( - "expected entire POSIX TZ string to be a valid \ - time zone transition rule, but found data after \ - parsing a valid time zone transition rule", - ), - OffsetDst(ref err) => { - f.write_str("failed to parse DST offset: ")?; - core::fmt::Display::fmt(err, f) - } - OffsetStd(ref err) => { - f.write_str("failed to parse standard offset: ")?; - core::fmt::Display::fmt(err, f) - } - Rule(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for PosixTimeZoneError { - fn from(kind: ErrorKind) -> PosixTimeZoneError { - PosixTimeZoneError { kind } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixOffsetError { - HourPosix(HourPosixError), - IncompleteMinutes, - IncompleteSeconds, - Minute(MinuteError), - OptionalSign(OptionalSignError), - Second(SecondError), -} - -impl core::fmt::Display for PosixOffsetError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixOffsetError::*; - match *self { - HourPosix(ref err) => core::fmt::Display::fmt(err, f), - IncompleteMinutes => f.write_str( - "incomplete time in \ - POSIX time zone string (missing minutes)", - ), - IncompleteSeconds => f.write_str( - "incomplete time in \ - POSIX time zone string (missing seconds)", - ), - Minute(ref err) => core::fmt::Display::fmt(err, f), - Second(ref err) => core::fmt::Display::fmt(err, f), - OptionalSign(ref err) => { - f.write_str( - "failed to parse sign for time offset \ - POSIX time zone string", - )?; - core::fmt::Display::fmt(err, f) - } - } - } -} - -impl From for PosixOffsetError { - fn from(err: HourPosixError) -> PosixOffsetError { - PosixOffsetError::HourPosix(err) - } -} - -impl From for PosixOffsetError { - fn from(err: MinuteError) -> PosixOffsetError { - PosixOffsetError::Minute(err) - } -} - -impl From for PosixOffsetError { - fn from(err: OptionalSignError) -> PosixOffsetError { - PosixOffsetError::OptionalSign(err) - } -} - -impl From for PosixOffsetError { - fn from(err: SecondError) -> PosixOffsetError { - PosixOffsetError::Second(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixRuleError { - DateTimeEnd(PosixDateTimeError), - DateTimeStart(PosixDateTimeError), - ExpectedEnd, -} - -impl core::fmt::Display for PosixRuleError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixRuleError::*; - match *self { - DateTimeEnd(ref err) => { - f.write_str("failed to parse end of DST transition rule: ")?; - core::fmt::Display::fmt(err, f) - } - DateTimeStart(ref err) => { - f.write_str("failed to parse start of DST transition rule: ")?; - core::fmt::Display::fmt(err, f) - } - ExpectedEnd => f.write_str( - "expected end of DST rule after parsing the start \ - of the DST rule", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixDateTimeError { - Date(PosixDateError), - ExpectedTime, - Time(PosixTimeError), -} - -impl core::fmt::Display for PosixDateTimeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixDateTimeError::*; - match *self { - Date(ref err) => core::fmt::Display::fmt(err, f), - ExpectedTime => f.write_str( - "expected time specification after `/` following a date - specification in a POSIX time zone DST transition rule", - ), - Time(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for PosixDateTimeError { - fn from(err: PosixDateError) -> PosixDateTimeError { - PosixDateTimeError::Date(err) - } -} - -impl From for PosixDateTimeError { - fn from(err: PosixTimeError) -> PosixDateTimeError { - PosixDateTimeError::Time(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixDateError { - ExpectedJulianNoLeap, - ExpectedMonthWeekWeekday, - JulianLeap(PosixJulianLeapError), - JulianNoLeap(PosixJulianNoLeapError), - UnexpectedByte, - WeekdayOfMonth(WeekdayOfMonthError), -} - -impl core::fmt::Display for PosixDateError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixDateError::*; - match *self { - ExpectedJulianNoLeap => f.write_str( - "expected one-based Julian day after `J` in date \ - specification of a POSIX time zone DST \ - transition rule, but found the end of input", - ), - ExpectedMonthWeekWeekday => f.write_str( - "expected month-week-weekday after `M` in date \ - specification of a POSIX time zone DST \ - transition rule, but found the end of input", - ), - JulianLeap(ref err) => core::fmt::Display::fmt(err, f), - JulianNoLeap(ref err) => core::fmt::Display::fmt(err, f), - UnexpectedByte => f.write_str( - "expected `J`, a digit or `M` at the beginning of a date \ - specification of a POSIX time zone DST transition rule", - ), - WeekdayOfMonth(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for PosixDateError { - fn from(err: PosixJulianLeapError) -> PosixDateError { - PosixDateError::JulianLeap(err) - } -} - -impl From for PosixDateError { - fn from(err: PosixJulianNoLeapError) -> PosixDateError { - PosixDateError::JulianNoLeap(err) - } -} - -impl From for PosixDateError { - fn from(err: WeekdayOfMonthError) -> PosixDateError { - PosixDateError::WeekdayOfMonth(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixJulianNoLeapError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for PosixJulianNoLeapError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixJulianNoLeapError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid one-based Julian day digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed one-based Julian day, but it's not in supported \ - range of `1..=365`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixJulianLeapError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for PosixJulianLeapError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixJulianLeapError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid zero-based Julian day digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed zero-based Julian day, but it's not in supported \ - range of `0..=365`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum AbbreviationError { - Quoted(QuotedAbbreviationError), - Unquoted(UnquotedAbbreviationError), -} - -impl core::fmt::Display for AbbreviationError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::AbbreviationError::*; - match *self { - Quoted(ref err) => core::fmt::Display::fmt(err, f), - Unquoted(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum UnquotedAbbreviationError { - InvalidUtf8, - TooLong, - TooShort, -} - -impl core::fmt::Display for UnquotedAbbreviationError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::UnquotedAbbreviationError::*; - match *self { - InvalidUtf8 => f.write_str( - "unquoted time zone abbreviation must be valid UTF-8", - ), - TooLong => write!( - f, - "expected unquoted time zone abbreviation with at most \ - {} bytes, but found an abbreviation that is longer", - Abbreviation::capacity(), - ), - TooShort => f.write_str( - "expected unquoted time zone abbreviation to have length of \ - 3 or more bytes, but an abbreviation that is shorter", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum QuotedAbbreviationError { - InvalidUtf8, - TooLong, - TooShort, - UnexpectedEnd, - UnexpectedEndAfterOpening, - UnexpectedLastByte, -} - -impl core::fmt::Display for QuotedAbbreviationError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::QuotedAbbreviationError::*; - match *self { - InvalidUtf8 => f.write_str( - "quoted time zone abbreviation must be valid UTF-8", - ), - TooLong => write!( - f, - "expected quoted time zone abbreviation with at most \ - {} bytes, but found an abbreviation that is longer", - Abbreviation::capacity(), - ), - TooShort => f.write_str( - "expected quoted time zone abbreviation to have length of \ - 3 or more bytes, but an abbreviation that is shorter", - ), - UnexpectedEnd => f.write_str( - "found non-empty quoted time zone abbreviation, but \ - found end of input before an end-of-quoted abbreviation \ - `>` character", - ), - UnexpectedEndAfterOpening => f.write_str( - "found opening `<` quote for time zone abbreviation in \ - POSIX time zone transition rule, and expected a name \ - following it, but found the end of input instead", - ), - UnexpectedLastByte => f.write_str( - "found non-empty quoted time zone abbreviation, but \ - found did not find end-of-quoted abbreviation `>` \ - character", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum WeekdayOfMonthError { - ExpectedDayOfWeekAfterWeek, - ExpectedDotAfterMonth, - ExpectedDotAfterWeek, - ExpectedWeekAfterMonth, - Month(MonthError), - WeekOfMonth(WeekOfMonthError), - Weekday(WeekdayError), -} - -impl core::fmt::Display for WeekdayOfMonthError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::WeekdayOfMonthError::*; - match *self { - ExpectedDayOfWeekAfterWeek => f.write_str( - "expected day-of-week after week in POSIX time zone rule", - ), - ExpectedDotAfterMonth => { - f.write_str("expected `.` after month in POSIX time zone rule") - } - ExpectedWeekAfterMonth => f.write_str( - "expected week after month in POSIX time zone rule", - ), - ExpectedDotAfterWeek => { - f.write_str("expected `.` after week in POSIX time zone rule") - } - Month(ref err) => core::fmt::Display::fmt(err, f), - WeekOfMonth(ref err) => core::fmt::Display::fmt(err, f), - Weekday(ref err) => core::fmt::Display::fmt(err, f), - } - } -} - -impl From for WeekdayOfMonthError { - fn from(err: MonthError) -> WeekdayOfMonthError { - WeekdayOfMonthError::Month(err) - } -} - -impl From for WeekdayOfMonthError { - fn from(err: WeekOfMonthError) -> WeekdayOfMonthError { - WeekdayOfMonthError::WeekOfMonth(err) - } -} - -impl From for WeekdayOfMonthError { - fn from(err: WeekdayError) -> WeekdayOfMonthError { - WeekdayOfMonthError::Weekday(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum PosixTimeError { - HourIana(HourIanaError), - HourPosix(HourPosixError), - IncompleteMinutes, - IncompleteSeconds, - Minute(MinuteError), - OptionalSign(OptionalSignError), - Second(SecondError), -} - -impl core::fmt::Display for PosixTimeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::PosixTimeError::*; - match *self { - HourIana(ref err) => core::fmt::Display::fmt(err, f), - HourPosix(ref err) => core::fmt::Display::fmt(err, f), - IncompleteMinutes => f.write_str( - "incomplete time zone transition time in \ - POSIX time zone string (missing minutes)", - ), - IncompleteSeconds => f.write_str( - "incomplete time zone transition time in \ - POSIX time zone string (missing seconds)", - ), - Minute(ref err) => core::fmt::Display::fmt(err, f), - Second(ref err) => core::fmt::Display::fmt(err, f), - OptionalSign(ref err) => { - f.write_str( - "failed to parse sign for time zone transition time", - )?; - core::fmt::Display::fmt(err, f) - } - } - } -} - -impl From for PosixTimeError { - fn from(err: HourIanaError) -> PosixTimeError { - PosixTimeError::HourIana(err) - } -} - -impl From for PosixTimeError { - fn from(err: HourPosixError) -> PosixTimeError { - PosixTimeError::HourPosix(err) - } -} - -impl From for PosixTimeError { - fn from(err: MinuteError) -> PosixTimeError { - PosixTimeError::Minute(err) - } -} - -impl From for PosixTimeError { - fn from(err: OptionalSignError) -> PosixTimeError { - PosixTimeError::OptionalSign(err) - } -} - -impl From for PosixTimeError { - fn from(err: SecondError) -> PosixTimeError { - PosixTimeError::Second(err) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum MonthError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for MonthError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::MonthError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid month digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed month, but it's not in supported \ - range of `1..=12`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum WeekOfMonthError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for WeekOfMonthError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::WeekOfMonthError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid week-of-month digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed week-of-month, but it's not in supported \ - range of `1..=5`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum WeekdayError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for WeekdayError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::WeekdayError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid weekday digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed weekday, but it's not in supported \ - range of `0..=6` (with `0` corresponding to Sunday)", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum HourIanaError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for HourIanaError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::HourIanaError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid hour digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed hours, but it's not in supported \ - range of `-167..=167`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum HourPosixError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for HourPosixError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::HourPosixError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid hour digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed hours, but it's not in supported \ - range of `0..=24`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum MinuteError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for MinuteError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::MinuteError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid minute digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed minutes, but it's not in supported \ - range of `0..=59`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum SecondError { - Parse(NumberError), - Range, -} - -impl core::fmt::Display for SecondError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::SecondError::*; - match *self { - Parse(ref err) => { - f.write_str("invalid second digits: ")?; - core::fmt::Display::fmt(err, f) - } - Range => f.write_str( - "parsed seconds, but it's not in supported \ - range of `0..=59`", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum NumberError { - Empty, - ExpectedLength, - InvalidDigit, - TooBig, -} - -impl core::fmt::Display for NumberError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::NumberError::*; - match *self { - Empty => f.write_str("invalid number, no digits found"), - ExpectedLength => f.write_str( - "expected a fixed number of digits, \ - but found incorrect number", - ), - InvalidDigit => f.write_str("expected digit in range `0..=9`"), - TooBig => f.write_str( - "parsed number too big to fit into a 32-bit signed integer", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum OptionalSignError { - ExpectedDigitAfterMinus, - ExpectedDigitAfterPlus, -} - -impl core::fmt::Display for OptionalSignError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::OptionalSignError::*; - match *self { - ExpectedDigitAfterMinus => f.write_str( - "expected digit after `-` sign, \ - but got end of input", - ), - ExpectedDigitAfterPlus => f.write_str( - "expected digit after `+` sign, \ - but got end of input", - ), - } - } -} - +// Tests require parsing, and parsing requires alloc. +#[cfg(feature = "alloc")] #[cfg(test)] mod tests { + use alloc::string::ToString; + use super::*; fn posix_time_zone( @@ -2132,26 +1601,21 @@ mod tests { ) -> PosixTimeZone { let input = input.as_ref(); let tz = PosixTimeZone::parse(input).unwrap(); - #[cfg(feature = "alloc")] - { - use alloc::string::ToString; - - // While we're here, assert that converting the TZ back - // to a string matches what we got. In the original version - // of the POSIX TZ parser, we were very meticulous about - // capturing the exact AST of the time zone. But I've - // since simplified the data structure considerably such - // that it is lossy in terms of what was actually parsed - // (but of course, not lossy in terms of the semantic - // meaning of the time zone). - // - // So to account for this, we serialize to a string and - // then parse it back. We should get what we started with. - let reparsed = - PosixTimeZone::parse(tz.to_string().as_bytes()).unwrap(); - assert_eq!(tz, reparsed); - assert_eq!(tz.to_string(), reparsed.to_string()); - } + // While we're here, assert that converting the TZ back + // to a string matches what we got. In the original version + // of the POSIX TZ parser, we were very meticulous about + // capturing the exact AST of the time zone. But I've + // since simplified the data structure considerably such + // that it is lossy in terms of what was actually parsed + // (but of course, not lossy in terms of the semantic + // meaning of the time zone). + // + // So to account for this, we serialize to a string and + // then parse it back. We should get what we started with. + let reparsed = + PosixTimeZone::parse(tz.to_string().as_bytes()).unwrap(); + assert_eq!(tz, reparsed); + assert_eq!(tz.to_string(), reparsed.to_string()); tz } diff --git a/src/shared/tzif.rs b/src/shared/tzif.rs index ef74b85..cef139e 100644 --- a/src/shared/tzif.rs +++ b/src/shared/tzif.rs @@ -3,6 +3,8 @@ use alloc::{string::String, vec}; use super::{ util::{ array_str::Abbreviation, + error::{err, Error}, + escape::{Byte, Bytes}, itime::{IOffset, ITimestamp}, }, PosixTimeZone, TzifDateTime, TzifFixed, TzifIndicator, TzifLocalTimeType, @@ -55,7 +57,7 @@ const FATTEN_UP_TO_YEAR: i16 = 2038; // // For "normal" cases, there should be at most two transitions per // year. So this limit permits 300/2=150 years of transition data. -// (Although we won't go above `FATTEN_UP_TO_YEAR`. See above.) +// (Although we won't go above 2036. See above.) const FATTEN_MAX_TRANSITIONS: usize = 300; impl TzifOwned { @@ -76,11 +78,11 @@ impl TzifOwned { pub(crate) fn parse( name: Option, bytes: &[u8], - ) -> Result { + ) -> Result { let original = bytes; let name = name.into(); - let (header32, rest) = - Header::parse(4, bytes).map_err(TzifErrorKind::Header32)?; + let (header32, rest) = Header::parse(4, bytes) + .map_err(|e| err!("failed to parse 32-bit header: {e}"))?; let (mut tzif, rest) = if header32.version == 0 { TzifOwned::parse32(name, header32, rest)? } else { @@ -113,7 +115,7 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> Result<(TzifOwned, &'b [u8]), TzifError> { + ) -> Result<(TzifOwned, &'b [u8]), Error> { let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -144,11 +146,14 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> Result<(TzifOwned, &'b [u8]), TzifError> { - let (_, rest) = - try_split_at(SplitAtError::V1, bytes, header32.data_block_len()?)?; - let (header64, rest) = - Header::parse(8, rest).map_err(TzifErrorKind::Header64)?; + ) -> Result<(TzifOwned, &'b [u8]), Error> { + let (_, rest) = try_split_at( + "V1 TZif data block", + bytes, + header32.data_block_len()?, + )?; + let (header64, rest) = Header::parse(8, rest) + .map_err(|e| err!("failed to parse 64-bit header: {e}"))?; let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -183,9 +188,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TzifError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::TransitionTimes, + "transition times data block", bytes, header.transition_times_len()?, )?; @@ -225,8 +230,8 @@ impl TzifOwned { let clamped = timestamp.clamp(TIMESTAMP_MIN, TIMESTAMP_MAX); // only-jiff-start warn!( - "found Unix timestamp `{timestamp}` that is outside \ - Jiff's supported range, clamping to `{clamped}`", + "found Unix timestamp {timestamp} that is outside \ + Jiff's supported range, clamping to {clamped}", ); // only-jiff-end timestamp = clamped; @@ -241,17 +246,21 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TransitionTypeError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::TransitionTypes, + "transition types data block", bytes, - header.transition_types_len(), + header.transition_types_len()?, )?; // We skip the first transition because it is our minimum dummy // transition. for (transition_index, &type_index) in (1..).zip(bytes) { if usize::from(type_index) >= header.tzh_typecnt { - return Err(TransitionTypeError::ExceedsLocalTimeTypes); + return Err(err!( + "found transition type index {type_index}, + but there are only {} local time types", + header.tzh_typecnt, + )); } self.transitions.infos[transition_index].type_index = type_index; } @@ -262,9 +271,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TzifError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::LocalTimeTypes, + "local time types data block", bytes, header.local_time_types_len()?, )?; @@ -272,8 +281,8 @@ impl TzifOwned { while let Some(chunk) = it.next() { let offset = from_be_bytes_i32(&chunk[..4]); if !(OFFSET_MIN <= offset && offset <= OFFSET_MAX) { - return Err(TzifError::from( - LocalTimeTypeError::InvalidOffset { offset }, + return Err(err!( + "found local time type with out-of-bounds offset: {offset}" )); } let is_dst = chunk[4] == 1; @@ -293,30 +302,49 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TimeZoneDesignatorError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::TimeZoneDesignations, + "time zone designations data block", bytes, - header.time_zone_designations_len(), + header.time_zone_designations_len()?, )?; - self.fixed.designations = String::from_utf8(bytes.to_vec()) - .map_err(|_| TimeZoneDesignatorError::InvalidUtf8)?; + self.fixed.designations = + String::from_utf8(bytes.to_vec()).map_err(|_| { + err!( + "time zone designations are not valid UTF-8: {:?}", + Bytes(bytes), + ) + })?; // Holy hell, this is brutal. The boundary conditions are crazy. - for typ in self.types.iter_mut() { + for (i, typ) in self.types.iter_mut().enumerate() { let start = usize::from(typ.designation.0); - let suffix = self - .fixed - .designations - .get(start..) - .ok_or(TimeZoneDesignatorError::InvalidStart)?; - let len = suffix - .find('\x00') - .ok_or(TimeZoneDesignatorError::MissingNul)?; - let end = start - .checked_add(len) - .ok_or(TimeZoneDesignatorError::InvalidLength)?; - typ.designation.1 = u8::try_from(end) - .map_err(|_| TimeZoneDesignatorError::InvalidEnd)?; + let Some(suffix) = self.fixed.designations.get(start..) else { + return Err(err!( + "local time type {i} has designation index of {start}, \ + but cannot be more than {}", + self.fixed.designations.len(), + )); + }; + let Some(len) = suffix.find('\x00') else { + return Err(err!( + "local time type {i} has designation index of {start}, \ + but could not find NUL terminator after it in \ + designations: {:?}", + self.fixed.designations, + )); + }; + let Some(end) = start.checked_add(len) else { + return Err(err!( + "local time type {i} has designation index of {start}, \ + but its length {len} is too big", + )); + }; + typ.designation.1 = u8::try_from(end).map_err(|_| { + err!( + "local time type {i} has designation range of \ + {start}..{end}, but end is too big", + ) + })?; } Ok(rest) } @@ -329,9 +357,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], TzifError> { + ) -> Result<&'b [u8], Error> { let (bytes, rest) = try_split_at( - SplitAtError::LeapSeconds, + "leap seconds data block", bytes, header.leap_second_len()?, )?; @@ -350,7 +378,7 @@ impl TzifOwned { if !(TIMESTAMP_MIN <= occur && occur <= TIMESTAMP_MAX) { // only-jiff-start warn!( - "leap second occurrence `{occur}` is \ + "leap second occurrence {occur} is \ not in Jiff's supported range" ) // only-jiff-end @@ -364,16 +392,16 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], IndicatorError> { + ) -> Result<&'b [u8], Error> { let (std_wall_bytes, rest) = try_split_at( - SplitAtError::StandardWallIndicators, + "standard/wall indicators data block", bytes, - header.standard_wall_len(), + header.standard_wall_len()?, )?; let (ut_local_bytes, rest) = try_split_at( - SplitAtError::UTLocalIndicators, + "UT/local indicators data block", rest, - header.ut_local_len(), + header.ut_local_len()?, )?; if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() { // This is a weird case, but technically possible only if all @@ -381,8 +409,14 @@ impl TzifOwned { // because it would require the corresponding std/wall indicator // to be 1 too. Which it can't be, because there aren't any. So // we just check that they're all zeros. - if ut_local_bytes.iter().any(|&byte| byte != 0) { - return Err(IndicatorError::UtLocalNonZero); + for (i, &byte) in ut_local_bytes.iter().enumerate() { + if byte != 0 { + return Err(err!( + "found UT/local indicator '{byte}' for local time \ + type {i}, but it must be 0 since all std/wall \ + indicators are 0", + )); + } } } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() { for (i, &byte) in std_wall_bytes.iter().enumerate() { @@ -393,7 +427,10 @@ impl TzifOwned { } else if byte == 1 { TzifIndicator::LocalStandard } else { - return Err(IndicatorError::InvalidStdWallIndicator); + return Err(err!( + "found invalid std/wall indicator '{byte}' for \ + local time type {i}, it must be 0 or 1", + )); }; } } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() { @@ -407,9 +444,18 @@ impl TzifOwned { (1, 0) => TzifIndicator::LocalStandard, (1, 1) => TzifIndicator::UTStandard, (0, 1) => { - return Err(IndicatorError::InvalidUtWallCombination); + return Err(err!( + "found illegal ut-wall combination for \ + local time type {i}, only local-wall, \ + local-standard and ut-standard are allowed", + )) + } + _ => { + return Err(err!( + "found illegal std/wall or ut/local value for \ + local time type {i}, each must be 0 or 1", + )) } - _ => return Err(IndicatorError::InvalidCombination), }; } } else { @@ -426,31 +472,42 @@ impl TzifOwned { &mut self, _header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], FooterError> { + ) -> Result<&'b [u8], Error> { if bytes.is_empty() { - return Err(FooterError::UnexpectedEnd); + return Err(err!( + "invalid V2+ TZif footer, expected \\n, \ + but found unexpected end of data", + )); } if bytes[0] != b'\n' { - return Err(FooterError::MismatchEnd); + return Err(err!( + "invalid V2+ TZif footer, expected {:?}, but found {:?}", + Byte(b'\n'), + Byte(bytes[0]), + )); } let bytes = &bytes[1..]; // Only scan up to 1KB for a NUL terminator in case we somehow got // passed a huge block of bytes. let toscan = &bytes[..bytes.len().min(1024)]; - let nlat = toscan - .iter() - .position(|&b| b == b'\n') - .ok_or(FooterError::TerminatorNotFound)?; + let Some(nlat) = toscan.iter().position(|&b| b == b'\n') else { + return Err(err!( + "invalid V2 TZif footer, could not find {:?} \ + terminator in: {:?}", + Byte(b'\n'), + Bytes(toscan), + )); + }; let (bytes, rest) = bytes.split_at(nlat); if !bytes.is_empty() { // We could in theory limit TZ strings to their strict POSIX // definition here for TZif V2, but I don't think there is any // harm in allowing the extensions in V2 formatted TZif data. Note - // that GNU tooling allows it via the `TZ` environment variable + // that the GNU tooling allow it via the `TZ` environment variable // even though POSIX doesn't specify it. This all seems okay to me // because the V3+ extension is a strict superset of functionality. - let posix_tz = PosixTimeZone::parse(bytes) - .map_err(FooterError::InvalidPosixTz)?; + let posix_tz = + PosixTimeZone::parse(bytes).map_err(|e| err!("{e}"))?; self.fixed.posix_tz = Some(posix_tz); } Ok(&rest[1..]) @@ -463,9 +520,7 @@ impl TzifOwned { /// RFC 8536 says, "If the string is nonempty and one or more /// transitions appear in the version 2+ data, the string MUST be /// consistent with the last version 2+ transition." - fn verify_posix_time_zone_consistency( - &self, - ) -> Result<(), InconsistentPosixTimeZoneError> { + fn verify_posix_time_zone_consistency(&self) -> Result<(), Error> { // We need to be a little careful, since we always have at least one // transition (accounting for the dummy `Timestamp::MIN` transition). // So if we only have 1 transition and a POSIX TZ string, then we @@ -492,13 +547,35 @@ impl TzifOwned { let (ioff, abbrev, is_dst) = tz.to_offset_info(ITimestamp::from_second(*last)); if ioff.second != typ.offset { - return Err(InconsistentPosixTimeZoneError::Offset); + return Err(err!( + "expected last transition to have DST offset \ + of {expected_offset}, but got {got_offset} \ + according to POSIX TZ string {tz}", + expected_offset = typ.offset, + got_offset = ioff.second, + tz = tz, + )); } if is_dst != typ.is_dst { - return Err(InconsistentPosixTimeZoneError::Dst); + return Err(err!( + "expected last transition to have is_dst={expected_dst}, \ + but got is_dst={got_dst} according to POSIX TZ \ + string {tz}", + expected_dst = typ.is_dst, + got_dst = is_dst, + tz = tz, + )); } if abbrev != self.designation(&typ) { - return Err(InconsistentPosixTimeZoneError::Designation); + return Err(err!( + "expected last transition to have \ + designation={expected_abbrev}, \ + but got designation={got_abbrev} according to POSIX TZ \ + string {tz}", + expected_abbrev = self.designation(&typ), + got_abbrev = abbrev, + tz = tz, + )); } Ok(()) } @@ -514,6 +591,7 @@ impl TzifOwned { /// and the timestamps in TZif data are, of course, all in UTC.) fn add_civil_datetimes_to_transitions(&mut self) { fn to_datetime(timestamp: i64, offset: i32) -> TzifDateTime { + use crate::shared::util::itime::{IOffset, ITimestamp}; let its = ITimestamp { second: timestamp, nanosecond: 0 }; let ioff = IOffset { second: offset }; let dt = its.to_datetime(ioff); @@ -788,14 +866,14 @@ impl Header { fn parse( time_size: usize, bytes: &[u8], - ) -> Result<(Header, &[u8]), HeaderError> { + ) -> Result<(Header, &[u8]), Error> { assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8"); if bytes.len() < 44 { - return Err(HeaderError::TooShort); + return Err(err!("invalid header: too short")); } let (magic, rest) = bytes.split_at(4); if magic != b"TZif" { - return Err(HeaderError::MismatchMagic); + return Err(err!("invalid header: magic bytes mismatch")); } let (version, rest) = rest.split_at(1); let (_reserved, rest) = rest.split_at(15); @@ -807,42 +885,40 @@ impl Header { let (tzh_typecnt_bytes, rest) = rest.split_at(4); let (tzh_charcnt_bytes, rest) = rest.split_at(4); - let tzh_ttisutcnt = - from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Ut, convert: e } - })?; - let tzh_ttisstdcnt = - from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Std, convert: e } - })?; - let tzh_leapcnt = - from_be_bytes_u32_to_usize(tzh_leapcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Leap, convert: e } - })?; - let tzh_timecnt = - from_be_bytes_u32_to_usize(tzh_timecnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Time, convert: e } - })?; - let tzh_typecnt = - from_be_bytes_u32_to_usize(tzh_typecnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Type, convert: e } - })?; - let tzh_charcnt = - from_be_bytes_u32_to_usize(tzh_charcnt_bytes).map_err(|e| { - HeaderError::ParseCount { kind: CountKind::Char, convert: e } - })?; + let tzh_ttisutcnt = from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes) + .map_err(|e| err!("failed to parse tzh_ttisutcnt: {e}"))?; + let tzh_ttisstdcnt = from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes) + .map_err(|e| err!("failed to parse tzh_ttisstdcnt: {e}"))?; + let tzh_leapcnt = from_be_bytes_u32_to_usize(tzh_leapcnt_bytes) + .map_err(|e| err!("failed to parse tzh_leapcnt: {e}"))?; + let tzh_timecnt = from_be_bytes_u32_to_usize(tzh_timecnt_bytes) + .map_err(|e| err!("failed to parse tzh_timecnt: {e}"))?; + let tzh_typecnt = from_be_bytes_u32_to_usize(tzh_typecnt_bytes) + .map_err(|e| err!("failed to parse tzh_typecnt: {e}"))?; + let tzh_charcnt = from_be_bytes_u32_to_usize(tzh_charcnt_bytes) + .map_err(|e| err!("failed to parse tzh_charcnt: {e}"))?; if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt { - return Err(HeaderError::MismatchUtType); + return Err(err!( + "expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \ + or equal to tzh_typecnt={tzh_typecnt}", + )); } if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt { - return Err(HeaderError::MismatchStdType); + return Err(err!( + "expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \ + or equal to tzh_typecnt={tzh_typecnt}", + )); } if tzh_typecnt < 1 { - return Err(HeaderError::ZeroType); + return Err(err!( + "expected tzh_typecnt={tzh_typecnt} to be at least 1", + )); } if tzh_charcnt < 1 { - return Err(HeaderError::ZeroChar); + return Err(err!( + "expected tzh_charcnt={tzh_charcnt} to be at least 1", + )); } let header = Header { @@ -873,514 +949,64 @@ impl Header { /// /// This is useful for, e.g., skipping over the 32-bit V1 data block in /// V2+ TZif formatted files. - fn data_block_len(&self) -> Result { + fn data_block_len(&self) -> Result { let a = self.transition_times_len()?; - let b = self.transition_types_len(); + let b = self.transition_types_len()?; let c = self.local_time_types_len()?; - let d = self.time_zone_designations_len(); + let d = self.time_zone_designations_len()?; let e = self.leap_second_len()?; - let f = self.standard_wall_len(); - let g = self.ut_local_len(); + let f = self.standard_wall_len()?; + let g = self.ut_local_len()?; a.checked_add(b) .and_then(|z| z.checked_add(c)) .and_then(|z| z.checked_add(d)) .and_then(|z| z.checked_add(e)) .and_then(|z| z.checked_add(f)) .and_then(|z| z.checked_add(g)) - .ok_or(HeaderError::InvalidDataBlock { version: self.version }) + .ok_or_else(|| { + err!( + "length of data block in V{} tzfile is too big", + self.version + ) + }) } - fn transition_times_len(&self) -> Result { - self.tzh_timecnt - .checked_mul(self.time_size) - .ok_or(HeaderError::InvalidTimeCount) + fn transition_times_len(&self) -> Result { + self.tzh_timecnt.checked_mul(self.time_size).ok_or_else(|| { + err!("tzh_timecnt value {} is too big", self.tzh_timecnt) + }) } - fn transition_types_len(&self) -> usize { - self.tzh_timecnt + fn transition_types_len(&self) -> Result { + Ok(self.tzh_timecnt) } - fn local_time_types_len(&self) -> Result { - self.tzh_typecnt.checked_mul(6).ok_or(HeaderError::InvalidTypeCount) + fn local_time_types_len(&self) -> Result { + self.tzh_typecnt.checked_mul(6).ok_or_else(|| { + err!("tzh_typecnt value {} is too big", self.tzh_typecnt) + }) } - fn time_zone_designations_len(&self) -> usize { - self.tzh_charcnt + fn time_zone_designations_len(&self) -> Result { + Ok(self.tzh_charcnt) } - fn leap_second_len(&self) -> Result { + fn leap_second_len(&self) -> Result { let record_len = self .time_size .checked_add(4) .expect("4-or-8 plus 4 always fits in usize"); - self.tzh_leapcnt - .checked_mul(record_len) - .ok_or(HeaderError::InvalidLeapSecondCount) + self.tzh_leapcnt.checked_mul(record_len).ok_or_else(|| { + err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt) + }) } - fn standard_wall_len(&self) -> usize { - self.tzh_ttisstdcnt + fn standard_wall_len(&self) -> Result { + Ok(self.tzh_ttisstdcnt) } - fn ut_local_len(&self) -> usize { - self.tzh_ttisutcnt - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct TzifError { - kind: TzifErrorKind, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum TzifErrorKind { - Footer(FooterError), - Header(HeaderError), - Header32(HeaderError), - Header64(HeaderError), - InconsistentPosixTimeZone(InconsistentPosixTimeZoneError), - Indicator(IndicatorError), - LocalTimeType(LocalTimeTypeError), - SplitAt(SplitAtError), - TimeZoneDesignator(TimeZoneDesignatorError), - TransitionType(TransitionTypeError), -} - -#[cfg(feature = "std")] -impl std::error::Error for TzifError {} - -impl core::fmt::Display for TzifError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::TzifErrorKind::*; - match self.kind { - Footer(ref err) => { - f.write_str("invalid TZif footer: ")?; - err.fmt(f) - } - Header(ref err) => { - f.write_str("invalid TZif header: ")?; - err.fmt(f) - } - Header32(ref err) => { - f.write_str("invalid 32-bit TZif header: ")?; - err.fmt(f) - } - Header64(ref err) => { - f.write_str("invalid 64-bit TZif header: ")?; - err.fmt(f) - } - InconsistentPosixTimeZone(ref err) => { - f.write_str( - "found inconsistency with \ - POSIX time zone transition rule \ - in TZif file footer: ", - )?; - err.fmt(f) - } - Indicator(ref err) => { - f.write_str("failed to parse indicators: ")?; - err.fmt(f) - } - LocalTimeType(ref err) => { - f.write_str("failed to parse local time types: ")?; - err.fmt(f) - } - SplitAt(ref err) => err.fmt(f), - TimeZoneDesignator(ref err) => { - f.write_str("failed to parse time zone designators: ")?; - err.fmt(f) - } - TransitionType(ref err) => { - f.write_str("failed to parse time zone transition types: ")?; - err.fmt(f) - } - } - } -} - -impl From for TzifError { - fn from(kind: TzifErrorKind) -> TzifError { - TzifError { kind } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum TransitionTypeError { - ExceedsLocalTimeTypes, - Split(SplitAtError), -} - -impl From for TzifError { - fn from(err: TransitionTypeError) -> TzifError { - TzifErrorKind::TransitionType(err).into() - } -} - -impl From for TransitionTypeError { - fn from(err: SplitAtError) -> TransitionTypeError { - TransitionTypeError::Split(err) - } -} - -impl core::fmt::Display for TransitionTypeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::TransitionTypeError::*; - - match *self { - ExceedsLocalTimeTypes => f.write_str( - "found time zone transition type index \ - that exceeds the number of local time types", - ), - Split(ref err) => err.fmt(f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum LocalTimeTypeError { - InvalidOffset { offset: i32 }, - Split(SplitAtError), -} - -impl From for TzifError { - fn from(err: LocalTimeTypeError) -> TzifError { - TzifErrorKind::LocalTimeType(err).into() - } -} - -impl From for LocalTimeTypeError { - fn from(err: SplitAtError) -> LocalTimeTypeError { - LocalTimeTypeError::Split(err) - } -} - -impl core::fmt::Display for LocalTimeTypeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::LocalTimeTypeError::*; - - match *self { - InvalidOffset { offset } => write!( - f, - "found local time type with \ - out-of-bounds time zone offset: {offset}, \ - Jiff's allowed range is `{OFFSET_MIN}..={OFFSET_MAX}`" - ), - Split(ref err) => err.fmt(f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum TimeZoneDesignatorError { - InvalidEnd, - InvalidLength, - InvalidStart, - InvalidUtf8, - MissingNul, - Split(SplitAtError), -} - -impl From for TzifError { - fn from(err: TimeZoneDesignatorError) -> TzifError { - TzifErrorKind::TimeZoneDesignator(err).into() - } -} - -impl From for TimeZoneDesignatorError { - fn from(err: SplitAtError) -> TimeZoneDesignatorError { - TimeZoneDesignatorError::Split(err) - } -} - -impl core::fmt::Display for TimeZoneDesignatorError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::TimeZoneDesignatorError::*; - - match *self { - InvalidEnd => f.write_str( - "found invalid end of time zone designator \ - for local time type", - ), - InvalidLength => f.write_str( - "found invalid length of time zone designator \ - for local time type", - ), - InvalidStart => f.write_str( - "found invalid start of time zone designator \ - for local time type", - ), - InvalidUtf8 => { - f.write_str("found invalid UTF-8 in time zone designators") - } - MissingNul => f.write_str( - "could not find NUL terminator for time zone designator", - ), - Split(ref err) => err.fmt(f), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum IndicatorError { - InvalidCombination, - InvalidStdWallIndicator, - InvalidUtWallCombination, - Split(SplitAtError), - UtLocalNonZero, -} - -impl From for TzifError { - fn from(err: IndicatorError) -> TzifError { - TzifErrorKind::Indicator(err).into() - } -} - -impl From for IndicatorError { - fn from(err: SplitAtError) -> IndicatorError { - IndicatorError::Split(err) - } -} - -impl core::fmt::Display for IndicatorError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::IndicatorError::*; - - match *self { - InvalidCombination => f.write_str( - "found invalid std/wall or UT/local value for \ - local time type, each must be 0 or 1", - ), - InvalidStdWallIndicator => f.write_str( - "found invalid std/wall indicator, \ - expected it to be 0 or 1", - ), - InvalidUtWallCombination => f.write_str( - "found invalid UT-wall combination for \ - local time type, only local-wall, \ - local-standard and UT-standard are allowed", - ), - Split(ref err) => err.fmt(f), - UtLocalNonZero => f.write_str( - "found non-zero UT/local indicator, \ - but all such indicators should be zero", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum InconsistentPosixTimeZoneError { - Designation, - Dst, - Offset, -} - -impl From for TzifError { - fn from(err: InconsistentPosixTimeZoneError) -> TzifError { - TzifErrorKind::InconsistentPosixTimeZone(err).into() - } -} - -impl core::fmt::Display for InconsistentPosixTimeZoneError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::InconsistentPosixTimeZoneError::*; - - match *self { - Designation => f.write_str( - "expected last transition in TZif file to have \ - a time zone abbreviation matching the abbreviation \ - derived from the POSIX time zone transition rule", - ), - Dst => f.write_str( - "expected last transition in TZif file to have \ - a DST status matching the status derived from the \ - POSIX time zone transition rule", - ), - Offset => f.write_str( - "expected last transition in TZif file to have \ - DST offset matching the offset derived from the \ - POSIX time zone transition rule", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum FooterError { - InvalidPosixTz(crate::shared::posix::PosixTimeZoneError), - MismatchEnd, - TerminatorNotFound, - UnexpectedEnd, -} - -impl From for TzifError { - fn from(err: FooterError) -> TzifError { - TzifErrorKind::Footer(err).into() - } -} - -impl core::fmt::Display for FooterError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::FooterError::*; - - match *self { - InvalidPosixTz(ref err) => { - f.write_str("invalid POSIX time zone transition rule")?; - core::fmt::Display::fmt(err, f) - } - MismatchEnd => f.write_str( - "expected to find `\\n` at the beginning of \ - the TZif file footer, \ - but found something else instead", - ), - TerminatorNotFound => f.write_str( - "expected to find `\\n` terminating \ - the TZif file footer, \ - but no line terminator could be found", - ), - UnexpectedEnd => f.write_str( - "expected to find `\\n` at the beginning of \ - the TZif file footer, \ - but found unexpected end of data", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum HeaderError { - InvalidDataBlock { version: u8 }, - InvalidLeapSecondCount, - InvalidTimeCount, - InvalidTypeCount, - MismatchMagic, - MismatchStdType, - MismatchUtType, - ParseCount { kind: CountKind, convert: U32UsizeError }, - TooShort, - ZeroChar, - ZeroType, -} - -impl From for TzifError { - fn from(err: HeaderError) -> TzifError { - TzifErrorKind::Header(err).into() - } -} - -impl core::fmt::Display for HeaderError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::HeaderError::*; - - match *self { - InvalidDataBlock { version } => write!( - f, - "length of data block in V{version} TZif file is too big", - ), - InvalidLeapSecondCount => { - f.write_str("number of leap seconds is too big") - } - InvalidTimeCount => { - f.write_str("number of transition times is too big") - } - InvalidTypeCount => { - f.write_str("number of local time types is too big") - } - MismatchMagic => f.write_str("magic bytes mismatch"), - MismatchStdType => f.write_str( - "expected number of standard/wall indicators to be zero \ - or equal to the number of local time types", - ), - MismatchUtType => f.write_str( - "expected number of UT/local indicators to be zero \ - or equal to the number of local time types", - ), - ParseCount { ref kind, ref convert } => { - write!(f, "failed to parse `{kind}`: {convert}") - } - TooShort => f.write_str("too short"), - ZeroChar => f.write_str( - "expected number of time zone abbreviations fo be at least 1", - ), - ZeroType => f.write_str( - "expected number of local time types fo be at least 1", - ), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum CountKind { - Ut, - Std, - Leap, - Time, - Type, - Char, -} - -impl core::fmt::Display for CountKind { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::CountKind::*; - match *self { - Ut => f.write_str("tzh_ttisutcnt"), - Std => f.write_str("tzh_ttisstdcnt"), - Leap => f.write_str("tzh_leapcnt"), - Time => f.write_str("tzh_timecnt"), - Type => f.write_str("tzh_typecnt"), - Char => f.write_str("tzh_charcnt"), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum SplitAtError { - V1, - LeapSeconds, - LocalTimeTypes, - StandardWallIndicators, - TimeZoneDesignations, - TransitionTimes, - TransitionTypes, - UTLocalIndicators, -} - -impl From for TzifError { - fn from(err: SplitAtError) -> TzifError { - TzifErrorKind::SplitAt(err).into() - } -} - -impl core::fmt::Display for SplitAtError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::SplitAtError::*; - - f.write_str("expected bytes for '")?; - f.write_str(match *self { - V1 => "v1 TZif", - LeapSeconds => "leap seconds", - LocalTimeTypes => "local time types", - StandardWallIndicators => "standard/wall indicators", - TimeZoneDesignations => "time zone designations", - TransitionTimes => "transition times", - TransitionTypes => "transition types", - UTLocalIndicators => "UT/local indicators", - })?; - f.write_str("data block', but did not find enough bytes")?; - Ok(()) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct U32UsizeError; - -impl core::fmt::Display for U32UsizeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!( - f, - "failed to parse integer because it is bigger than `{max}`", - max = usize::MAX, - ) + fn ut_local_len(&self) -> Result { + Ok(self.tzh_ttisutcnt) } } @@ -1390,12 +1016,16 @@ impl core::fmt::Display for U32UsizeError { /// returned. The error message will include the `what` string given, which is /// meant to describe the thing being split. fn try_split_at<'b>( - what: SplitAtError, + what: &'static str, bytes: &'b [u8], at: usize, -) -> Result<(&'b [u8], &'b [u8]), SplitAtError> { +) -> Result<(&'b [u8], &'b [u8]), Error> { if at > bytes.len() { - Err(what) + Err(err!( + "expected at least {at} bytes for {what}, \ + but found only {} bytes", + bytes.len(), + )) } else { Ok(bytes.split_at(at)) } @@ -1412,9 +1042,14 @@ fn try_split_at<'b>( /// /// This errors if the `u32` parsed from the given bytes cannot fit in a /// `usize`. -fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result { +fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result { let n = from_be_bytes_u32(bytes); - usize::try_from(n).map_err(|_| U32UsizeError) + usize::try_from(n).map_err(|_| { + err!( + "failed to parse integer {n} (too big, max allowed is {}", + usize::MAX + ) + }) } /// Interprets the given slice as an unsigned 32-bit big endian integer and diff --git a/src/shared/util/error.rs b/src/shared/util/error.rs new file mode 100644 index 0000000..36597e2 --- /dev/null +++ b/src/shared/util/error.rs @@ -0,0 +1,46 @@ +macro_rules! err { + ($($tt:tt)*) => {{ + crate::shared::util::error::Error::from_args(format_args!($($tt)*)) + }} +} + +pub(crate) use err; + +/// An error that can be returned when parsing. +#[derive(Clone, Debug)] +pub struct Error { + #[cfg(feature = "alloc")] + message: alloc::boxed::Box, + // only-jiff-start + #[cfg(not(feature = "alloc"))] + message: &'static str, + // only-jiff-end +} + +impl Error { + pub(crate) fn from_args<'a>(message: core::fmt::Arguments<'a>) -> Error { + #[cfg(feature = "alloc")] + { + use alloc::string::ToString; + + let message = message.to_string().into_boxed_str(); + Error { message } + } + // only-jiff-start + #[cfg(not(feature = "alloc"))] + { + let message = message.as_str().unwrap_or( + "unknown Jiff error (better error messages require \ + enabling the `alloc` feature for the `jiff` crate)", + ); + Error { message } + } + // only-jiff-end + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Display::fmt(&self.message, f) + } +} diff --git a/src/shared/util/escape.rs b/src/shared/util/escape.rs new file mode 100644 index 0000000..1593f90 --- /dev/null +++ b/src/shared/util/escape.rs @@ -0,0 +1,88 @@ +/*! +Provides convenience routines for escaping raw bytes. + +This was copied from `regex-automata` with a few light edits. +*/ + +use super::utf8; + +/// Provides a convenient `Debug` implementation for a `u8`. +/// +/// The `Debug` impl treats the byte as an ASCII, and emits a human +/// readable representation of it. If the byte isn't ASCII, then it's +/// emitted as a hex escape sequence. +#[derive(Clone, Copy)] +pub(crate) struct Byte(pub u8); + +impl core::fmt::Display for Byte { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + if self.0 == b' ' { + return write!(f, " "); + } + // 10 bytes is enough for any output from ascii::escape_default. + let mut bytes = [0u8; 10]; + let mut len = 0; + for (i, mut b) in core::ascii::escape_default(self.0).enumerate() { + // capitalize \xab to \xAB + if i >= 2 && b'a' <= b && b <= b'f' { + b -= 32; + } + bytes[len] = b; + len += 1; + } + write!(f, "{}", core::str::from_utf8(&bytes[..len]).unwrap()) + } +} + +impl core::fmt::Debug for Byte { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "\"")?; + core::fmt::Display::fmt(self, f)?; + write!(f, "\"")?; + Ok(()) + } +} + +/// Provides a convenient `Debug` implementation for `&[u8]`. +/// +/// This generally works best when the bytes are presumed to be mostly +/// UTF-8, but will work for anything. For any bytes that aren't UTF-8, +/// they are emitted as hex escape sequences. +#[derive(Clone, Copy)] +pub(crate) struct Bytes<'a>(pub &'a [u8]); + +impl<'a> core::fmt::Display for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + // This is a sad re-implementation of a similar impl found in bstr. + let mut bytes = self.0; + while let Some(result) = utf8::decode(bytes) { + let ch = match result { + Ok(ch) => ch, + Err(errant_bytes) => { + // The decode API guarantees `errant_bytes` is non-empty. + write!(f, r"\x{:02x}", errant_bytes[0])?; + bytes = &bytes[1..]; + continue; + } + }; + bytes = &bytes[ch.len_utf8()..]; + match ch { + '\0' => write!(f, "\\0")?, + '\x01'..='\x7f' => { + write!(f, "{}", (ch as u8).escape_ascii())?; + } + _ => write!(f, "{}", ch.escape_debug())?, + } + } + Ok(()) + } +} + +impl<'a> core::fmt::Debug for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "\"")?; + core::fmt::Display::fmt(self, f)?; + write!(f, "\"")?; + Ok(()) + } +} diff --git a/src/shared/util/itime.rs b/src/shared/util/itime.rs index 291289f..5336eca 100644 --- a/src/shared/util/itime.rs +++ b/src/shared/util/itime.rs @@ -22,6 +22,8 @@ they are internal types. Specifically, to distinguish them from Jiff's public types. For example, `Date` versus `IDate`. */ +use super::error::{err, Error}; + #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] pub(crate) struct ITimestamp { pub(crate) second: i64, @@ -139,13 +141,11 @@ impl IDateTime { pub(crate) fn checked_add_seconds( &self, seconds: i32, - ) -> Result { - let day_second = self - .time - .to_second() - .second - .checked_add(seconds) - .ok_or_else(|| RangeError::DateTimeSeconds)?; + ) -> Result { + let day_second = + self.time.to_second().second.checked_add(seconds).ok_or_else( + || err!("adding `{seconds}s` to datetime overflowed"), + )?; let days = day_second.div_euclid(86400); let second = day_second.rem_euclid(86400); let date = self.date.checked_add_days(days)?; @@ -160,8 +160,8 @@ pub(crate) struct IEpochDay { } impl IEpochDay { - pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; - pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; + const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; + const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; /// Converts days since the Unix epoch to a Gregorian date. /// @@ -217,17 +217,20 @@ impl IEpochDay { /// If this would overflow an `i32` or result in an out-of-bounds epoch /// day, then this returns an error. #[inline] - pub(crate) fn checked_add( - &self, - amount: i32, - ) -> Result { + pub(crate) fn checked_add(&self, amount: i32) -> Result { let epoch_day = self.epoch_day; - let sum = epoch_day - .checked_add(amount) - .ok_or_else(|| RangeError::EpochDayI32)?; + let sum = epoch_day.checked_add(amount).ok_or_else(|| { + err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32") + })?; let ret = IEpochDay { epoch_day: sum }; if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) { - return Err(RangeError::EpochDayDays); + return Err(err!( + "adding `{amount}` to epoch day `{epoch_day}` \ + resulted in `{sum}`, which is not in the required \ + epoch day range of `{min}..={max}`", + min = IEpochDay::MIN.epoch_day, + max = IEpochDay::MAX.epoch_day, + )); } Ok(ret) } @@ -255,11 +258,14 @@ impl IDate { year: i16, month: i8, day: i8, - ) -> Result { + ) -> Result { if day > 28 { let max_day = days_in_month(year, month); if day > max_day { - return Err(RangeError::DateInvalidDays { year, month }); + return Err(err!( + "day={day} is out of range for year={year} \ + and month={month}, must be in range 1..={max_day}", + )); } } Ok(IDate { year, month, day }) @@ -275,22 +281,37 @@ impl IDate { pub(crate) fn from_day_of_year( year: i16, day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 366) { - return Err(RangeError::DateInvalidDayOfYear { year }); + return Err(err!( + "day-of-year={day} is out of range for year={year}, \ + must be in range 1..={max_day}", + max_day = days_in_year(year), + )); } let start = IDate { year, month: 1, day: 1 }.to_epoch_day(); let end = start .checked_add(i32::from(day) - 1) - // This can only happen when `year=9999` and `day=366`. - .map_err(|_| RangeError::DayOfYear)? + .map_err(|_| { + err!( + "failed to find date for \ + year={year} and day-of-year={day}: \ + adding `{day}` to `{start}` overflows \ + Jiff's range", + start = start.epoch_day, + ) + })? .to_date(); // If we overflowed into the next year, then `day` is too big. if year != end.year { // Can only happen given day=366 and this is a leap year. debug_assert_eq!(day, 366); debug_assert!(!is_leap_year(year)); - return Err(RangeError::DateInvalidDayOfYear { year }); + return Err(err!( + "day-of-year={day} is out of range for year={year}, \ + must be in range 1..={max_day}", + max_day = days_in_year(year), + )); } Ok(end) } @@ -306,9 +327,12 @@ impl IDate { pub(crate) fn from_day_of_year_no_leap( year: i16, mut day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 365) { - return Err(RangeError::DateInvalidDayOfYearNoLeap); + return Err(err!( + "day-of-year={day} is out of range for year={year}, \ + must be in range 1..=365", + )); } if day >= 60 && is_leap_year(year) { day += 1; @@ -366,9 +390,12 @@ impl IDate { &self, nth: i8, weekday: IWeekday, - ) -> Result { + ) -> Result { if nth == 0 || !(-5 <= nth && nth <= 5) { - return Err(RangeError::NthWeekdayOfMonth); + return Err(err!( + "got nth weekday of `{nth}`, but \ + must be non-zero and in range `-5..=5`", + )); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -385,10 +412,13 @@ impl IDate { // of `Day`, we can't let this boundary condition escape. So we // check it here. if day < 1 { - return Err(RangeError::DateInvalidDays { - year: self.year, - month: self.month, - }); + return Err(err!( + "day={day} is out of range for year={year} \ + and month={month}, must be in range 1..={max_day}", + year = self.year, + month = self.month, + max_day = days_in_month(self.year, self.month), + )); } IDate::try_new(self.year, self.month, day) } @@ -396,12 +426,16 @@ impl IDate { /// Returns the day before this date. #[inline] - pub(crate) fn yesterday(self) -> Result { + pub(crate) fn yesterday(self) -> Result { if self.day == 1 { if self.month == 1 { let year = self.year - 1; if year <= -10000 { - return Err(RangeError::Yesterday); + return Err(err!( + "returning yesterday for -9999-01-01 is not \ + possible because it is less than Jiff's supported + minimum date", + )); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -414,12 +448,16 @@ impl IDate { /// Returns the day after this date. #[inline] - pub(crate) fn tomorrow(self) -> Result { + pub(crate) fn tomorrow(self) -> Result { if self.day >= 28 && self.day == days_in_month(self.year, self.month) { if self.month == 12 { let year = self.year + 1; if year >= 10000 { - return Err(RangeError::Tomorrow); + return Err(err!( + "returning tomorrow for 9999-12-31 is not \ + possible because it is greater than Jiff's supported + maximum date", + )); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -431,20 +469,34 @@ impl IDate { /// Returns the year one year before this date. #[inline] - pub(crate) fn prev_year(self) -> Result { + pub(crate) fn prev_year(self) -> Result { let year = self.year - 1; if year <= -10_000 { - return Err(RangeError::YearPrevious); + return Err(err!( + "returning previous year for {year:04}-{month:02}-{day:02} is \ + not possible because it is less than Jiff's supported \ + minimum date", + year = self.year, + month = self.month, + day = self.day, + )); } Ok(year) } /// Returns the year one year from this date. #[inline] - pub(crate) fn next_year(self) -> Result { + pub(crate) fn next_year(self) -> Result { let year = self.year + 1; if year >= 10_000 { - return Err(RangeError::YearNext); + return Err(err!( + "returning next year for {year:04}-{month:02}-{day:02} is \ + not possible because it is greater than Jiff's supported \ + maximum date", + year = self.year, + month = self.month, + day = self.day, + )); } Ok(year) } @@ -454,7 +506,7 @@ impl IDate { pub(crate) fn checked_add_days( &self, amount: i32, - ) -> Result { + ) -> Result { match amount { 0 => Ok(*self), -1 => self.yesterday(), @@ -666,84 +718,6 @@ pub(crate) enum IAmbiguousOffset { Fold { before: IOffset, after: IOffset }, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum RangeError { - DateInvalidDayOfYear { year: i16 }, - DateInvalidDayOfYearNoLeap, - DateInvalidDays { year: i16, month: i8 }, - DateTimeSeconds, - DayOfYear, - EpochDayDays, - EpochDayI32, - NthWeekdayOfMonth, - Tomorrow, - YearNext, - YearPrevious, - Yesterday, -} - -impl core::fmt::Display for RangeError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use self::RangeError::*; - - match *self { - DateInvalidDayOfYear { year } => write!( - f, - "number of days for `{year:04}` is invalid, \ - must be in range `1..={max_day}`", - max_day = days_in_year(year), - ), - DateInvalidDayOfYearNoLeap => f.write_str( - "number of days is invalid, must be in range `1..=365`", - ), - DateInvalidDays { year, month } => write!( - f, - "number of days for `{year:04}-{month:02}` is invalid, \ - must be in range `1..={max_day}`", - max_day = days_in_month(year, month), - ), - DateTimeSeconds => { - f.write_str("adding seconds to datetime overflowed") - } - DayOfYear => f.write_str("day of year is invalid"), - EpochDayDays => write!( - f, - "adding to epoch day resulted in a value outside \ - the allowed range of `{min}..={max}`", - min = IEpochDay::MIN.epoch_day, - max = IEpochDay::MAX.epoch_day, - ), - EpochDayI32 => f.write_str( - "adding to epoch day overflowed 32-bit signed integer", - ), - NthWeekdayOfMonth => f.write_str( - "invalid nth weekday of month, \ - must be non-zero and in range `-5..=5`", - ), - Tomorrow => f.write_str( - "returning tomorrow for `9999-12-31` is not \ - possible because it is greater than Jiff's supported - maximum date", - ), - YearNext => f.write_str( - "creating a date for a year following `9999` is \ - not possible because it is greater than Jiff's supported \ - maximum date", - ), - YearPrevious => f.write_str( - "creating a date for a year preceding `-9999` is \ - not possible because it is less than Jiff's supported \ - minimum date", - ), - Yesterday => f.write_str( - "returning yesterday for `-9999-01-01` is not \ - possible because it is less than Jiff's supported - minimum date", - ), - } - } -} - /// Returns true if and only if the given year is a leap year. /// /// A leap year is a year with 366 days. Typical years have 365 days. @@ -946,20 +920,4 @@ mod tests { let d1 = IDate { year: 9999, month: 12, day: 31 }; assert_eq!(d1.tomorrow().ok(), None); } - - #[test] - fn from_day_of_year() { - assert_eq!( - IDate::from_day_of_year(9999, 365), - Ok(IDate { year: 9999, month: 12, day: 31 }), - ); - assert_eq!( - IDate::from_day_of_year(9998, 366), - Err(RangeError::DateInvalidDayOfYear { year: 9998 }), - ); - assert_eq!( - IDate::from_day_of_year(9999, 366), - Err(RangeError::DayOfYear), - ); - } } diff --git a/src/shared/util/mod.rs b/src/shared/util/mod.rs index 971f365..a31dc54 100644 --- a/src/shared/util/mod.rs +++ b/src/shared/util/mod.rs @@ -1,2 +1,5 @@ pub(crate) mod array_str; +pub(crate) mod error; +pub(crate) mod escape; pub(crate) mod itime; +pub(crate) mod utf8; diff --git a/src/shared/util/utf8.rs b/src/shared/util/utf8.rs new file mode 100644 index 0000000..1cccc47 --- /dev/null +++ b/src/shared/util/utf8.rs @@ -0,0 +1,37 @@ +/// Decodes the next UTF-8 encoded codepoint from the given byte slice. +/// +/// If no valid encoding of a codepoint exists at the beginning of the +/// given byte slice, then a 1-3 byte slice is returned (which is guaranteed +/// to be a prefix of `bytes`). That byte slice corresponds either to a single +/// invalid byte, or to a prefix of a valid UTF-8 encoding of a Unicode scalar +/// value (but which ultimately did not lead to a valid encoding). +/// +/// This returns `None` if and only if `bytes` is empty. +/// +/// This never panics. +/// +/// *WARNING*: This is not designed for performance. If you're looking for +/// a fast UTF-8 decoder, this is not it. If you feel like you need one in +/// this crate, then please file an issue and discuss your use case. +pub(crate) fn decode(bytes: &[u8]) -> Option> { + if bytes.is_empty() { + return None; + } + let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) { + Ok(s) => s, + Err(ref err) if err.valid_up_to() > 0 => { + core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap() + } + // In this case, we want to return 1-3 bytes that make up a prefix of + // a potentially valid codepoint. + Err(err) => { + return Some(Err( + &bytes[..err.error_len().unwrap_or_else(|| bytes.len())] + )) + } + }; + // OK because we guaranteed above that `string` + // must be non-empty. And thus, `str::chars` must + // yield at least one Unicode scalar value. + Some(Ok(string.chars().next().unwrap())) +} diff --git a/src/signed_duration.rs b/src/signed_duration.rs index 852ea93..f70f1ab 100644 --- a/src/signed_duration.rs +++ b/src/signed_duration.rs @@ -2,10 +2,10 @@ use core::time::Duration; use crate::{ civil::{Date, DateTime, Time}, - error::{signed_duration::Error as E, ErrorContext}, + error::{err, ErrorContext}, fmt::{friendly, temporal}, tz::Offset, - util::{rangeint::TryRFrom, t}, + util::{escape, rangeint::TryRFrom, t}, Error, RoundMode, Timestamp, Unit, Zoned, }; @@ -65,7 +65,8 @@ const MINS_PER_HOUR: i64 = 60; /// /// assert_eq!( /// "P1d".parse::().unwrap_err().to_string(), -/// "parsing ISO 8601 duration in this context requires that \ +/// "failed to parse \"P1d\" as an ISO 8601 duration string: \ +/// parsing ISO 8601 duration into a `SignedDuration` requires that \ /// the duration contain a time component and no components of days or \ /// greater", /// ); @@ -1455,13 +1456,24 @@ impl SignedDuration { #[inline] pub fn try_from_secs_f64(secs: f64) -> Result { if !secs.is_finite() { - return Err(Error::from(E::ConvertNonFinite)); + return Err(err!( + "could not convert non-finite seconds \ + {secs} to signed duration", + )); } if secs < (i64::MIN as f64) { - return Err(Error::slim_range("floating point seconds")); + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + minimum value of {:?}", + SignedDuration::MIN, + )); } if secs > (i64::MAX as f64) { - return Err(Error::slim_range("floating point seconds")); + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + maximum value of {:?}", + SignedDuration::MAX, + )); } let mut int_secs = secs.trunc() as i64; @@ -1469,9 +1481,15 @@ impl SignedDuration { (secs.fract() * (NANOS_PER_SEC as f64)).round() as i32; if int_nanos.unsigned_abs() == 1_000_000_000 { let increment = i64::from(int_nanos.signum()); - int_secs = int_secs - .checked_add(increment) - .ok_or_else(|| Error::slim_range("floating point seconds"))?; + int_secs = int_secs.checked_add(increment).ok_or_else(|| { + err!( + "floating point seconds {secs} overflows signed duration \ + maximum value of {max:?} after rounding its fractional \ + component of {fract:?}", + max = SignedDuration::MAX, + fract = secs.fract(), + ) + })?; int_nanos = 0; } Ok(SignedDuration::new_unchecked(int_secs, int_nanos)) @@ -1510,13 +1528,24 @@ impl SignedDuration { #[inline] pub fn try_from_secs_f32(secs: f32) -> Result { if !secs.is_finite() { - return Err(Error::from(E::ConvertNonFinite)); + return Err(err!( + "could not convert non-finite seconds \ + {secs} to signed duration", + )); } if secs < (i64::MIN as f32) { - return Err(Error::slim_range("floating point seconds")); + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + minimum value of {:?}", + SignedDuration::MIN, + )); } if secs > (i64::MAX as f32) { - return Err(Error::slim_range("floating point seconds")); + return Err(err!( + "floating point seconds {secs} overflows signed duration \ + maximum value of {:?}", + SignedDuration::MAX, + )); } let mut int_nanos = (secs.fract() * (NANOS_PER_SEC as f32)).round() as i32; @@ -1524,9 +1553,15 @@ impl SignedDuration { if int_nanos.unsigned_abs() == 1_000_000_000 { let increment = i64::from(int_nanos.signum()); // N.B. I haven't found a way to trigger this error path in tests. - int_secs = int_secs - .checked_add(increment) - .ok_or_else(|| Error::slim_range("floating point seconds"))?; + int_secs = int_secs.checked_add(increment).ok_or_else(|| { + err!( + "floating point seconds {secs} overflows signed duration \ + maximum value of {max:?} after rounding its fractional \ + component of {fract:?}", + max = SignedDuration::MAX, + fract = secs.fract(), + ) + })?; int_nanos = 0; } Ok(SignedDuration::new_unchecked(int_secs, int_nanos)) @@ -1988,18 +2023,25 @@ impl SignedDuration { time2: std::time::SystemTime, ) -> Result { match time2.duration_since(time1) { - Ok(dur) => { - SignedDuration::try_from(dur).context(E::ConvertSystemTime) - } + Ok(dur) => SignedDuration::try_from(dur).with_context(|| { + err!( + "unsigned duration {dur:?} for system time since \ + Unix epoch overflowed signed duration" + ) + }), Err(err) => { let dur = err.duration(); - let dur = SignedDuration::try_from(dur) - .context(E::ConvertSystemTime)?; - dur.checked_neg() - .ok_or_else(|| { - Error::slim_range("signed duration seconds") - }) - .context(E::ConvertSystemTime) + let dur = + SignedDuration::try_from(dur).with_context(|| { + err!( + "unsigned duration {dur:?} for system time before \ + Unix epoch overflowed signed duration" + ) + })?; + dur.checked_neg().ok_or_else(|| { + err!("negating duration {dur:?} from before the Unix epoch \ + overflowed signed duration") + }) } } } @@ -2113,15 +2155,17 @@ impl SignedDuration { /// /// assert_eq!( /// SignedDuration::MAX.round(Unit::Hour).unwrap_err().to_string(), - /// "rounding signed duration to nearest hour \ - /// resulted in a value outside the supported \ - /// range of a `jiff::SignedDuration`", + /// "rounding `2562047788015215h 30m 7s 999ms 999µs 999ns` to \ + /// nearest hour in increments of 1 resulted in \ + /// 9223372036854777600 seconds, which does not fit into an i64 \ + /// and thus overflows `SignedDuration`", /// ); /// assert_eq!( /// SignedDuration::MIN.round(Unit::Hour).unwrap_err().to_string(), - /// "rounding signed duration to nearest hour \ - /// resulted in a value outside the supported \ - /// range of a `jiff::SignedDuration`", + /// "rounding `2562047788015215h 30m 8s 999ms 999µs 999ns ago` to \ + /// nearest hour in increments of 1 resulted in \ + /// -9223372036854777600 seconds, which does not fit into an i64 \ + /// and thus overflows `SignedDuration`", /// ); /// ``` /// @@ -2132,9 +2176,9 @@ impl SignedDuration { /// /// assert_eq!( /// SignedDuration::ZERO.round(Unit::Day).unwrap_err().to_string(), - /// "rounding `jiff::SignedDuration` failed \ - /// because a calendar unit of 'days' was provided \ - /// (to round by calendar units, you must use a `jiff::Span`)", + /// "rounding `SignedDuration` failed \ + /// because a calendar unit of days was provided \ + /// (to round by calendar units, you must use a `Span`)", /// ); /// ``` #[inline] @@ -2347,19 +2391,16 @@ impl core::fmt::Debug for SignedDuration { if f.alternate() { if self.subsec_nanos() == 0 { - core::fmt::Display::fmt(&self.as_secs(), f)?; - f.write_str("s") + write!(f, "{}s", self.as_secs()) } else if self.as_secs() == 0 { - core::fmt::Display::fmt(&self.subsec_nanos(), f)?; - f.write_str("ns") + write!(f, "{}ns", self.subsec_nanos()) } else { - core::fmt::Display::fmt(&self.as_secs(), f)?; - f.write_str("s ")?; - core::fmt::Display::fmt( - &self.subsec_nanos().unsigned_abs(), + write!( f, - )?; - f.write_str("ns") + "{}s {}ns", + self.as_secs(), + self.subsec_nanos().unsigned_abs() + ) } } else { friendly::DEFAULT_SPAN_PRINTER @@ -2373,8 +2414,9 @@ impl TryFrom for SignedDuration { type Error = Error; fn try_from(d: Duration) -> Result { - let secs = i64::try_from(d.as_secs()) - .map_err(|_| Error::slim_range("unsigned duration seconds"))?; + let secs = i64::try_from(d.as_secs()).map_err(|_| { + err!("seconds in unsigned duration {d:?} overflowed i64") + })?; // Guaranteed to succeed since 0<=nanos<=999,999,999. let nanos = i32::try_from(d.subsec_nanos()).unwrap(); Ok(SignedDuration::new_unchecked(secs, nanos)) @@ -2387,10 +2429,14 @@ impl TryFrom for Duration { fn try_from(sd: SignedDuration) -> Result { // This isn't needed, but improves error messages. if sd.is_negative() { - return Err(Error::slim_range("negative duration seconds")); + return Err(err!( + "cannot convert negative duration `{sd:?}` to \ + unsigned `std::time::Duration`", + )); } - let secs = u64::try_from(sd.as_secs()) - .map_err(|_| Error::slim_range("signed duration seconds"))?; + let secs = u64::try_from(sd.as_secs()).map_err(|_| { + err!("seconds in signed duration {sd:?} overflowed u64") + })?; // Guaranteed to succeed because the above only succeeds // when `sd` is non-negative. And when `sd` is non-negative, // we are guaranteed that 0<=nanos<=999,999,999. @@ -2725,9 +2771,12 @@ impl SignedDurationRound { /// Does the actual duration rounding. fn round(&self, dur: SignedDuration) -> Result { if self.smallest > Unit::Hour { - return Err(Error::from(E::RoundCalendarUnit { - unit: self.smallest, - })); + return Err(err!( + "rounding `SignedDuration` failed because \ + a calendar unit of {plural} was provided \ + (to round by calendar units, you must use a `Span`)", + plural = self.smallest.plural(), + )); } let nanos = t::NoUnits128::new_unchecked(dur.as_nanos()); let increment = t::NoUnits::new_unchecked(self.increment); @@ -2740,7 +2789,12 @@ impl SignedDurationRound { let seconds = rounded / t::NANOS_PER_SECOND; let seconds = t::NoUnits::try_rfrom("seconds", seconds).map_err(|_| { - Error::from(E::RoundOverflowed { unit: self.smallest }) + err!( + "rounding `{dur:#}` to nearest {singular} in increments \ + of {increment} resulted in {seconds} seconds, which does \ + not fit into an i64 and thus overflows `SignedDuration`", + singular = self.smallest.singular(), + ) })?; let subsec_nanos = rounded % t::NANOS_PER_SECOND; // OK because % 1_000_000_000 above guarantees that the result fits @@ -2784,22 +2838,25 @@ impl From<(Unit, i64)> for SignedDurationRound { /// (We do the same thing for `Span`.) #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_iso_or_friendly(bytes: &[u8]) -> Result { - let Some((&byte, tail)) = bytes.split_first() else { - return Err(crate::Error::from( - crate::error::fmt::Error::HybridDurationEmpty, + if bytes.is_empty() { + return Err(err!( + "an empty string is not a valid `SignedDuration`, \ + expected either a ISO 8601 or Jiff's 'friendly' \ + format", )); - }; - let mut first = byte; - // N.B. Unsigned durations don't support negative durations (of - // course), but we still check for it here so that we can defer to - // the dedicated parsers. They will provide their own error messages. + } + let mut first = bytes[0]; if first == b'+' || first == b'-' { - let Some(&byte) = tail.first() else { - return Err(crate::Error::from( - crate::error::fmt::Error::HybridDurationPrefix { sign: first }, + if bytes.len() == 1 { + return Err(err!( + "found nothing after sign `{sign}`, \ + which is not a valid `SignedDuration`, \ + expected either a ISO 8601 or Jiff's 'friendly' \ + format", + sign = escape::Byte(first), )); - }; - first = byte; + } + first = bytes[1]; } if first == b'P' || first == b'p' { temporal::DEFAULT_SPAN_PARSER.parse_duration(bytes) @@ -2991,15 +3048,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, + @"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format", ); insta::assert_snapshot!( p("+").unwrap_err(), - @r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, + @"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format", ); insta::assert_snapshot!( p("-").unwrap_err(), - @r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, + @"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format", ); } @@ -3036,15 +3093,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#, + @"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2", ); insta::assert_snapshot!( p("+").unwrap_err(), - @r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#, + @"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3", ); insta::assert_snapshot!( p("-").unwrap_err(), - @r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#, + @"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3", ); } diff --git a/src/span.rs b/src/span.rs index 48e9826..7119bd7 100644 --- a/src/span.rs +++ b/src/span.rs @@ -3,11 +3,12 @@ use core::{cmp::Ordering, time::Duration as UnsignedDuration}; use crate::{ civil::{Date, DateTime, Time}, duration::{Duration, SDuration}, - error::{span::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, fmt::{friendly, temporal}, tz::TimeZone, util::{ borrow::DumbCow, + escape, rangeint::{ri64, ri8, RFrom, RInto, TryRFrom, TryRInto}, round::increment, t::{self, Constant, NoUnits, NoUnits128, Sign, C}, @@ -556,13 +557,12 @@ pub(crate) use span_eq; /// span.total(Unit::Hour).unwrap_err().to_string(), /// "using unit 'day' in a span or configuration requires that either \ /// a relative reference time be given or \ -/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ +/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// // Opt into invariant 24 hour days without a relative date: /// let marker = SpanRelativeTo::days_are_24_hours(); /// let hours = span.total((Unit::Hour, marker))?; -/// assert_eq!(hours, 24.0); /// // Or use a relative civil date, and all days are 24 hours: /// let date = civil::date(2020, 1, 1); /// let hours = span.total((Unit::Hour, date))?; @@ -662,11 +662,10 @@ pub(crate) use span_eq; /// assert_eq!( /// Duration::try_from(span).unwrap_err().to_string(), /// "failed to convert span to duration without relative datetime \ -/// (must use `jiff::Span::to_duration` instead): using unit 'day' \ -/// in a span or configuration requires that either a relative \ -/// reference time be given or \ -/// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ -/// invariant 24-hour days, but neither were provided", +/// (must use `Span::to_duration` instead): using unit 'day' in a \ +/// span or configuration requires that either a relative reference \ +/// time be given or `SpanRelativeTo::days_are_24_hours()` is used \ +/// to indicate invariant 24-hour days, but neither were provided", /// ); /// /// # Ok::<(), Box>(()) @@ -2326,8 +2325,7 @@ impl Span { /// Converts a `Span` to a [`SignedDuration`] relative to the date given. /// /// In most cases, it is unlikely that you'll need to use this routine to - /// convert a `Span` to a `SignedDuration` and instead will be ably to - /// use `SignedDuration::try_from(span)`. Namely, by default: + /// convert a `Span` to a `SignedDuration`. Namely, by default: /// /// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours. /// * [`Timestamp::until`] guarantees that the biggest non-zero unit is @@ -2338,14 +2336,12 @@ impl Span { /// * [`Time::until`] guarantees that the biggest non-zero unit is hours. /// /// In the above, only [`DateTime::until`] and [`Date::until`] return - /// calendar units by default, and thus would require this routine. (In - /// which case, one may pass [`SpanRelativeTo::days_are_24_hours`] or an - /// actual relative date to resolve the length of a day.) + /// calendar units by default. In which case, one may pass + /// [`SpanRelativeTo::days_are_24_hours`] or an actual relative date to + /// resolve the length of a day. /// - /// Of course, one may change the defaults. For example, if one - /// uses `Zoned::until` with the largest unit set to `Unit::Year` - /// and the resulting `Span` includes non-zero calendar units, then - /// `SignedDuration::try_from` will fail because there is no relative date. + /// Of course, any of the above can be changed by asking, for example, + /// `Zoned::until` to return units up to years. /// /// # Errors /// @@ -2402,10 +2398,24 @@ impl Span { let relspan = result .and_then(|r| r.into_relative_span(Unit::Second, *self)) .with_context(|| match relative.kind { - SpanRelativeToKind::Civil(_) => E::ToDurationCivil, - SpanRelativeToKind::Zoned(_) => E::ToDurationZoned, + SpanRelativeToKind::Civil(dt) => { + err!( + "could not compute normalized relative span \ + from datetime {dt} and span {self}", + ) + } + SpanRelativeToKind::Zoned(ref zdt) => { + err!( + "could not compute normalized relative span \ + from datetime {zdt} and span {self}", + ) + } SpanRelativeToKind::DaysAre24Hours => { - E::ToDurationDaysAre24Hours + err!( + "could not compute normalized relative span \ + from {self} when all days are assumed to be \ + 24 hours", + ) } })?; debug_assert!(relspan.span.largest_unit() <= Unit::Second); @@ -3188,7 +3198,13 @@ impl Span { &self, ) -> Option { let non_time_unit = self.largest_calendar_unit()?; - Some(Error::from(E::NotAllowedCalendarUnits { unit: non_time_unit })) + Some(err!( + "operation can only be performed with units of hours \ + or smaller, but found non-zero {unit} units \ + (operations on `Timestamp`, `tz::Offset` and `civil::Time` \ + don't support calendar units in a `Span`)", + unit = non_time_unit.singular(), + )) } /// Returns the largest non-zero calendar unit, or `None` if there are no @@ -3256,7 +3272,7 @@ impl Span { if self.nanoseconds != C(0) { write!(buf, ", nanoseconds: {:?}", self.nanoseconds).unwrap(); } - buf.push_str(" }}"); + write!(buf, " }}").unwrap(); buf } @@ -3289,8 +3305,8 @@ impl Span { match (span.is_zero(), new_is_zero) { (_, true) => Sign::N::<0>(), (true, false) => units.signum().rinto(), - // If the old and new span are both non-zero, and we know our - // new units are not negative, then the sign remains unchanged. + // If the old and new span are both non-zero, and we know our new + // units are not negative, then the sign remains unchanged. (false, false) => new.sign, } } @@ -3457,7 +3473,10 @@ impl TryFrom for UnsignedDuration { fn try_from(sp: Span) -> Result { // This isn't needed, but improves error messages. if sp.is_negative() { - return Err(Error::from(E::ConvertNegative)); + return Err(err!( + "cannot convert negative span {sp:?} \ + to unsigned std::time::Duration", + )); } SignedDuration::try_from(sp).and_then(UnsignedDuration::try_from) } @@ -3524,15 +3543,18 @@ impl TryFrom for Span { #[inline] fn try_from(d: UnsignedDuration) -> Result { - let seconds = i64::try_from(d.as_secs()) - .map_err(|_| Error::slim_range("unsigned duration seconds"))?; + let seconds = i64::try_from(d.as_secs()).map_err(|_| { + err!("seconds from {d:?} overflows a 64-bit signed integer") + })?; let nanoseconds = i64::from(d.subsec_nanos()); let milliseconds = nanoseconds / t::NANOS_PER_MILLI.value(); let microseconds = (nanoseconds % t::NANOS_PER_MILLI.value()) / t::NANOS_PER_MICRO.value(); let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value(); - let span = Span::new().try_seconds(seconds)?; + let span = Span::new().try_seconds(seconds).with_context(|| { + err!("duration {d:?} overflows limits of a Jiff `Span`") + })?; // These are all OK because `Duration::subsec_nanos` is guaranteed to // return less than 1_000_000_000 nanoseconds. And splitting that up // into millis, micros and nano components is guaranteed to fit into @@ -3584,8 +3606,10 @@ impl TryFrom for SignedDuration { #[inline] fn try_from(sp: Span) -> Result { - requires_relative_date_err(sp.largest_unit()) - .context(E::ConvertSpanToSignedDuration)?; + requires_relative_date_err(sp.largest_unit()).context( + "failed to convert span to duration without relative datetime \ + (must use `Span::to_duration` instead)", + )?; Ok(sp.to_duration_invariant()) } } @@ -3654,7 +3678,9 @@ impl TryFrom for Span { / t::NANOS_PER_MICRO.value(); let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value(); - let span = Span::new().try_seconds(seconds)?; + let span = Span::new().try_seconds(seconds).with_context(|| { + err!("signed duration {d:?} overflows limits of a Jiff `Span`") + })?; // These are all OK because `|SignedDuration::subsec_nanos|` is // guaranteed to return less than 1_000_000_000 nanoseconds. And // splitting that up into millis, micros and nano components is @@ -4428,7 +4454,7 @@ impl<'a> SpanArithmetic<'a> { /// span1.checked_add(span2).unwrap_err().to_string(), /// "using unit 'day' in a span or configuration requires that \ /// either a relative reference time be given or \ - /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// let sum = span1.checked_add( @@ -4658,7 +4684,7 @@ impl<'a> SpanCompare<'a> { /// required. Otherwise, you get an error. /// /// ``` - /// use jiff::{SpanCompare, ToSpan}; + /// use jiff::{SpanCompare, ToSpan, Unit}; /// /// let span1 = 2.days().hours(12); /// let span2 = 60.hours(); @@ -4667,7 +4693,7 @@ impl<'a> SpanCompare<'a> { /// span1.compare(span2).unwrap_err().to_string(), /// "using unit 'day' in a span or configuration requires that \ /// either a relative reference time be given or \ - /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// let ordering = span1.compare( @@ -4898,7 +4924,7 @@ impl<'a> SpanTotal<'a> { /// span.total(Unit::Hour).unwrap_err().to_string(), /// "using unit 'day' in a span or configuration requires that either \ /// a relative reference time be given or \ - /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// @@ -5406,9 +5432,8 @@ impl<'a> SpanRound<'a> { /// span.round(Unit::Day).unwrap_err().to_string(), /// "error with `smallest` rounding option: using unit 'day' in a \ /// span or configuration requires that either a relative reference \ - /// time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is \ - /// used to indicate invariant 24-hour days, but neither were \ - /// provided", + /// time be given or `SpanRelativeTo::days_are_24_hours()` is used \ + /// to indicate invariant 24-hour days, but neither were provided", /// ); /// let rounded = span.round( /// SpanRound::new().smallest(Unit::Day).days_are_24_hours(), @@ -5461,8 +5486,11 @@ impl<'a> SpanRound<'a> { let max = existing_largest.max(largest); let increment = increment::for_span(smallest, self.increment)?; if largest < smallest { - return Err(Error::from( - E::NotAllowedLargestSmallerThanSmallest { smallest, largest }, + return Err(err!( + "largest unit ('{largest}') cannot be smaller than \ + smallest unit ('{smallest}')", + largest = largest.singular(), + smallest = smallest.singular(), )); } let relative = match self.relative { @@ -5488,13 +5516,14 @@ impl<'a> SpanRound<'a> { // no reasonable invariant interpretation of the span. And this // is only true when everything is less than 'day'. requires_relative_date_err(smallest) - .context(E::OptionSmallest)?; + .context("error with `smallest` rounding option")?; if let Some(largest) = self.largest { requires_relative_date_err(largest) - .context(E::OptionLargest)?; + .context("error with `largest` rounding option")?; } - requires_relative_date_err(existing_largest) - .context(E::OptionLargestInSpan)?; + requires_relative_date_err(existing_largest).context( + "error with largest unit in span to be rounded", + )?; assert!(max <= Unit::Week); return Ok(round_span_invariant( span, smallest, largest, increment, mode, @@ -5644,7 +5673,7 @@ impl<'a> SpanRelativeTo<'a> { /// span.total(Unit::Hour).unwrap_err().to_string(), /// "using unit 'day' in a span or configuration requires that either \ /// a relative reference time be given or \ - /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// // Opt into invariant 24 hour days without a relative date: @@ -5680,7 +5709,7 @@ impl<'a> SpanRelativeTo<'a> { /// span.total(Unit::Hour).unwrap_err().to_string(), /// "using unit 'week' in a span or configuration requires that either \ /// a relative reference time be given or \ - /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// // Opt into invariant 24 hour days without a relative date: @@ -5785,10 +5814,13 @@ impl<'a> SpanRelativeTo<'a> { } SpanRelativeToKind::DaysAre24Hours => { if matches!(unit, Unit::Year | Unit::Month) { - return Err(Error::from( - E::RequiresRelativeYearOrMonthGivenDaysAre24Hours { - unit, - }, + return Err(err!( + "using unit '{unit}' in span or configuration \ + requires that a relative reference time be given \ + (`SpanRelativeTo::days_are_24_hours()` was given \ + but this only permits using days and weeks \ + without a relative reference time)", + unit = unit.singular(), )); } Ok(None) @@ -6197,12 +6229,27 @@ impl<'a> RelativeSpanKind<'a> { RelativeSpanKind::Civil { ref start, ref end } => start .datetime .until((largest, end.datetime)) - .context(E::FailedSpanBetweenDateTimes { unit: largest })?, - RelativeSpanKind::Zoned { ref start, ref end } => { - start.zoned.until((largest, &*end.zoned)).context( - E::FailedSpanBetweenZonedDateTimes { unit: largest }, - )? - } + .with_context(|| { + err!( + "failed to get span between {start} and {end} \ + with largest unit as {unit}", + start = start.datetime, + end = end.datetime, + unit = largest.plural(), + ) + })?, + RelativeSpanKind::Zoned { ref start, ref end } => start + .zoned + .until((largest, &*end.zoned)) + .with_context(|| { + err!( + "failed to get span between {start} and {end} \ + with largest unit as {unit}", + start = start.zoned, + end = end.zoned, + unit = largest.plural(), + ) + })?, }; Ok(RelativeSpan { span, kind: self }) } @@ -6243,7 +6290,9 @@ impl RelativeCivil { fn new(datetime: DateTime) -> Result { let timestamp = datetime .to_zoned(TimeZone::UTC) - .context(E::ConvertDateTimeToTimestamp)? + .with_context(|| { + err!("failed to convert {datetime} to timestamp") + })? .timestamp(); Ok(RelativeCivil { datetime, timestamp }) } @@ -6259,10 +6308,14 @@ impl RelativeCivil { /// converted to a timestamp in UTC. This only occurs near the minimum and /// maximum datetime values. fn checked_add(&self, span: Span) -> Result { - let datetime = self.datetime.checked_add(span)?; + let datetime = self.datetime.checked_add(span).with_context(|| { + err!("failed to add {span} to {dt}", dt = self.datetime) + })?; let timestamp = datetime .to_zoned(TimeZone::UTC) - .context(E::ConvertDateTimeToTimestamp)? + .with_context(|| { + err!("failed to convert {datetime} to timestamp") + })? .timestamp(); Ok(RelativeCivil { datetime, timestamp }) } @@ -6282,10 +6335,15 @@ impl RelativeCivil { &self, duration: SignedDuration, ) -> Result { - let datetime = self.datetime.checked_add(duration)?; + let datetime = + self.datetime.checked_add(duration).with_context(|| { + err!("failed to add {duration:?} to {dt}", dt = self.datetime) + })?; let timestamp = datetime .to_zoned(TimeZone::UTC) - .context(E::ConvertDateTimeToTimestamp)? + .with_context(|| { + err!("failed to convert {datetime} to timestamp") + })? .timestamp(); Ok(RelativeCivil { datetime, timestamp }) } @@ -6303,9 +6361,15 @@ impl RelativeCivil { largest: Unit, other: &RelativeCivil, ) -> Result { - self.datetime - .until((largest, other.datetime)) - .context(E::FailedSpanBetweenDateTimes { unit: largest }) + self.datetime.until((largest, other.datetime)).with_context(|| { + err!( + "failed to get span between {dt1} and {dt2} \ + with largest unit as {unit}", + unit = largest.plural(), + dt1 = self.datetime, + dt2 = other.datetime, + ) + }) } } @@ -6326,7 +6390,9 @@ impl<'a> RelativeZoned<'a> { &self, span: Span, ) -> Result, Error> { - let zoned = self.zoned.checked_add(span)?; + let zoned = self.zoned.checked_add(span).with_context(|| { + err!("failed to add {span} to {zoned}", zoned = self.zoned) + })?; Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) }) } @@ -6340,7 +6406,9 @@ impl<'a> RelativeZoned<'a> { &self, duration: SignedDuration, ) -> Result, Error> { - let zoned = self.zoned.checked_add(duration)?; + let zoned = self.zoned.checked_add(duration).with_context(|| { + err!("failed to add {duration:?} to {zoned}", zoned = self.zoned) + })?; Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) }) } @@ -6357,9 +6425,15 @@ impl<'a> RelativeZoned<'a> { largest: Unit, other: &RelativeZoned<'a>, ) -> Result { - self.zoned - .until((largest, &*other.zoned)) - .context(E::FailedSpanBetweenZonedDateTimes { unit: largest }) + self.zoned.until((largest, &*other.zoned)).with_context(|| { + err!( + "failed to get span between {zdt1} and {zdt2} \ + with largest unit as {unit}", + unit = largest.plural(), + zdt1 = self.zoned, + zdt2 = other.zoned, + ) + }) } /// Returns the borrowed version of self; useful when you need to convert @@ -6438,7 +6512,13 @@ impl Nudge { increment, ); let span = Span::from_invariant_nanoseconds(largest, rounded_nanos) - .context(E::ConvertNanoseconds { unit: largest })? + .with_context(|| { + err!( + "failed to convert rounded nanoseconds {rounded_nanos} \ + to span for largest unit as {unit}", + unit = largest.plural(), + ) + })? .years_ranged(balanced.get_years_ranged()) .months_ranged(balanced.get_months_ranged()) .weeks_ranged(balanced.get_weeks_ranged()); @@ -6471,7 +6551,13 @@ impl Nudge { * balanced.get_units_ranged(smallest).div_ceil(increment); let span = balanced .without_lower(smallest) - .try_units_ranged(smallest, truncated.rinto())?; + .try_units_ranged(smallest, truncated.rinto()) + .with_context(|| { + err!( + "failed to set {unit} to {truncated} on span {balanced}", + unit = smallest.singular() + ) + })?; let (relative0, relative1) = clamp_relative_span( relative_start, span, @@ -6492,7 +6578,14 @@ impl Nudge { let grew_big_unit = ((rounded.get() as f64) - exact).signum() == (sign.get() as f64); - let span = span.try_units_ranged(smallest, rounded.rinto())?; + let span = span + .try_units_ranged(smallest, rounded.rinto()) + .with_context(|| { + err!( + "failed to set {unit} to {truncated} on span {span}", + unit = smallest.singular() + ) + })?; let rounded_relative_end = if grew_big_unit { relative1 } else { relative0 }; Ok(Nudge { span, rounded_relative_end, grew_big_unit }) @@ -6538,7 +6631,13 @@ impl Nudge { let span = Span::from_invariant_nanoseconds(Unit::Hour, rounded_time_nanos) - .context(E::ConvertNanoseconds { unit: Unit::Hour })? + .with_context(|| { + err!( + "failed to convert rounded nanoseconds \ + {rounded_time_nanos} to span for largest unit as {unit}", + unit = Unit::Hour.plural(), + ) + })? .years_ranged(balanced.get_years_ranged()) .months_ranged(balanced.get_months_ranged()) .weeks_ranged(balanced.get_weeks_ranged()) @@ -6583,8 +6682,23 @@ impl Nudge { let span_start = balanced.without_lower(unit); let new_units = span_start .get_units_ranged(unit) - .try_checked_add("bubble-units", sign)?; - let span_end = span_start.try_units_ranged(unit, new_units)?; + .try_checked_add("bubble-units", sign) + .with_context(|| { + err!( + "failed to add sign {sign} to {unit} value {value}", + unit = unit.plural(), + value = span_start.get_units_ranged(unit), + ) + })?; + let span_end = span_start + .try_units_ranged(unit, new_units) + .with_context(|| { + err!( + "failed to set {unit} to value \ + {new_units} on span {span_start}", + unit = unit.plural(), + ) + })?; let threshold = match relative.kind { RelativeSpanKind::Civil { ref start, .. } => { start.checked_add(span_end)?.timestamp @@ -6628,8 +6742,13 @@ fn round_span_invariant( let nanos = span.to_invariant_nanoseconds(); let rounded = mode.round_by_unit_in_nanoseconds(nanos, smallest, increment); - Span::from_invariant_nanoseconds(largest, rounded) - .context(E::ConvertNanoseconds { unit: largest }) + Span::from_invariant_nanoseconds(largest, rounded).with_context(|| { + err!( + "failed to convert rounded nanoseconds {rounded} \ + to span for largest unit as {unit}", + unit = largest.plural(), + ) + }) } /// Returns the nanosecond timestamps of `relative + span` and `relative + @@ -6653,9 +6772,24 @@ fn clamp_relative_span( unit: Unit, amount: NoUnits, ) -> Result<(NoUnits128, NoUnits128), Error> { - let amount = - span.get_units_ranged(unit).try_checked_add("clamp-units", amount)?; - let span_amount = span.try_units_ranged(unit, amount)?; + let amount = span + .get_units_ranged(unit) + .try_checked_add("clamp-units", amount) + .with_context(|| { + err!( + "failed to add {amount} to {unit} \ + value {value} on span {span}", + unit = unit.plural(), + value = span.get_units_ranged(unit), + ) + })?; + let span_amount = + span.try_units_ranged(unit, amount).with_context(|| { + err!( + "failed to set {unit} unit to {amount} on span {span}", + unit = unit.plural(), + ) + })?; let relative0 = relative.checked_add(span)?.to_nanosecond(); let relative1 = relative.checked_add(span_amount)?.to_nanosecond(); Ok((relative0, relative1)) @@ -6677,22 +6811,25 @@ fn clamp_relative_span( /// (We do the same thing for `SignedDuration`.) #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_iso_or_friendly(bytes: &[u8]) -> Result { - let Some((&byte, tail)) = bytes.split_first() else { - return Err(crate::Error::from( - crate::error::fmt::Error::HybridDurationEmpty, + if bytes.is_empty() { + return Err(err!( + "an empty string is not a valid `Span`, \ + expected either a ISO 8601 or Jiff's 'friendly' \ + format", )); - }; - let mut first = byte; - // N.B. Unsigned durations don't support negative durations (of - // course), but we still check for it here so that we can defer to - // the dedicated parsers. They will provide their own error messages. + } + let mut first = bytes[0]; if first == b'+' || first == b'-' { - let Some(&byte) = tail.first() else { - return Err(crate::Error::from( - crate::error::fmt::Error::HybridDurationPrefix { sign: first }, + if bytes.len() == 1 { + return Err(err!( + "found nothing after sign `{sign}`, \ + which is not a valid `Span`, \ + expected either a ISO 8601 or Jiff's 'friendly' \ + format", + sign = escape::Byte(first), )); - }; - first = byte; + } + first = bytes[1]; } if first == b'P' || first == b'p' { temporal::DEFAULT_SPAN_PARSER.parse_span(bytes) @@ -6703,11 +6840,23 @@ fn parse_iso_or_friendly(bytes: &[u8]) -> Result { fn requires_relative_date_err(unit: Unit) -> Result<(), Error> { if unit.is_variable() { - return Err(Error::from(if matches!(unit, Unit::Week | Unit::Day) { - E::RequiresRelativeWeekOrDay { unit } + return Err(if matches!(unit, Unit::Week | Unit::Day) { + err!( + "using unit '{unit}' in a span or configuration \ + requires that either a relative reference time be given \ + or `SpanRelativeTo::days_are_24_hours()` is used to \ + indicate invariant 24-hour days, \ + but neither were provided", + unit = unit.singular(), + ) } else { - E::RequiresRelativeYearOrMonth { unit } - })); + err!( + "using unit '{unit}' in a span or configuration \ + requires that a relative reference time be given, \ + but none was provided", + unit = unit.singular(), + ) + }); } Ok(()) } @@ -7241,15 +7390,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, + @"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format", ); insta::assert_snapshot!( p("+").unwrap_err(), - @r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, + @"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format", ); insta::assert_snapshot!( p("-").unwrap_err(), - @r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, + @"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format", ); } @@ -7286,15 +7435,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 2"#, + @"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2", ); insta::assert_snapshot!( p("+").unwrap_err(), - @r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#, + @"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3", ); insta::assert_snapshot!( p("-").unwrap_err(), - @r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format at line 1 column 3"#, + @"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 3", ); } } diff --git a/src/timestamp.rs b/src/timestamp.rs index be37120..22a4732 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -2,7 +2,7 @@ use core::time::Duration as UnsignedDuration; use crate::{ duration::{Duration, SDuration}, - error::{timestamp::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -279,7 +279,8 @@ use crate::{ /// let result = "2024-06-30 08:30[America/New_York]".parse::(); /// assert_eq!( /// result.unwrap_err().to_string(), -/// "failed to find offset component, \ +/// "failed to find offset component in \ +/// \"2024-06-30 08:30[America/New_York]\", \ /// which is required for parsing a timestamp", /// ); /// ``` @@ -1519,7 +1520,9 @@ impl Timestamp { let time_seconds = self.as_second_ranged(); let sum = time_seconds .try_checked_add("span", span_seconds) - .context(E::OverflowAddSpan)?; + .with_context(|| { + err!("adding {span} to {self} overflowed") + })?; return Ok(Timestamp::from_second_ranged(sum)); } } @@ -1527,7 +1530,7 @@ impl Timestamp { let span_nanos = span.to_invariant_nanoseconds(); let sum = time_nanos .try_checked_add("span", span_nanos) - .context(E::OverflowAddSpan)?; + .with_context(|| err!("adding {span} to {self} overflowed"))?; Ok(Timestamp::from_nanosecond_ranged(sum)) } @@ -1537,7 +1540,9 @@ impl Timestamp { duration: SignedDuration, ) -> Result { let start = self.as_duration(); - let end = start.checked_add(duration).ok_or(E::OverflowAddDuration)?; + let end = start.checked_add(duration).ok_or_else(|| { + err!("overflow when adding {duration:?} to {self}") + })?; Timestamp::from_duration(end) } @@ -1643,7 +1648,9 @@ impl Timestamp { duration: A, ) -> Result { let duration: TimestampArithmetic = duration.into(); - duration.saturating_add(self).context(E::RequiresSaturatingTimeUnits) + duration.saturating_add(self).context( + "saturating `Timestamp` arithmetic requires only time units", + ) } /// This routine is identical to [`Timestamp::saturating_add`] with the @@ -3422,10 +3429,11 @@ impl TimestampDifference { .get_largest() .unwrap_or_else(|| self.round.get_smallest().max(Unit::Second)); if largest >= Unit::Day { - return Err(Error::from( - crate::error::util::RoundingIncrementError::Unsupported { - unit: largest, - }, + return Err(err!( + "unit {largest} is not supported when computing the \ + difference between timestamps (must use units smaller \ + than 'day')", + largest = largest.singular(), )); } let nano1 = t1.as_nanosecond_ranged().without_bounds(); @@ -3848,7 +3856,7 @@ mod tests { fn timestamp_saturating_add() { insta::assert_snapshot!( Timestamp::MIN.saturating_add(Span::new().days(1)).unwrap_err(), - @"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)", + @"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)", ) } @@ -3856,7 +3864,7 @@ mod tests { fn timestamp_saturating_sub() { insta::assert_snapshot!( Timestamp::MAX.saturating_sub(Span::new().days(1)).unwrap_err(), - @"saturating timestamp arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero 'day' units (operations on `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)", + @"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)", ) } diff --git a/src/tz/ambiguous.rs b/src/tz/ambiguous.rs index c662d09..45319f5 100644 --- a/src/tz/ambiguous.rs +++ b/src/tz/ambiguous.rs @@ -1,6 +1,6 @@ use crate::{ civil::DateTime, - error::{tz::ambiguous::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, shared::util::itime::IAmbiguousOffset, tz::{Offset, TimeZone}, Timestamp, Zoned, @@ -655,10 +655,18 @@ impl AmbiguousTimestamp { let offset = match self.offset() { AmbiguousOffset::Unambiguous { offset } => offset, AmbiguousOffset::Gap { before, after } => { - return Err(Error::from(E::BecauseGap { before, after })); + return Err(err!( + "the datetime {dt} is ambiguous since it falls into \ + a gap between offsets {before} and {after}", + dt = self.dt, + )); } AmbiguousOffset::Fold { before, after } => { - return Err(Error::from(E::BecauseFold { before, after })); + return Err(err!( + "the datetime {dt} is ambiguous since it falls into \ + a fold between offsets {before} and {after}", + dt = self.dt, + )); } }; offset.to_timestamp(self.dt) @@ -1031,10 +1039,13 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn compatible(self) -> Result { - let ts = self - .ts - .compatible() - .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; + let ts = self.ts.compatible().with_context(|| { + err!( + "error converting datetime {dt} to instant in time zone {tz}", + dt = self.datetime(), + tz = self.time_zone().diagnostic_name(), + ) + })?; Ok(ts.to_zoned(self.tz)) } @@ -1090,10 +1101,13 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn earlier(self) -> Result { - let ts = self - .ts - .earlier() - .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; + let ts = self.ts.earlier().with_context(|| { + err!( + "error converting datetime {dt} to instant in time zone {tz}", + dt = self.datetime(), + tz = self.time_zone().diagnostic_name(), + ) + })?; Ok(ts.to_zoned(self.tz)) } @@ -1149,10 +1163,13 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn later(self) -> Result { - let ts = self - .ts - .later() - .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; + let ts = self.ts.later().with_context(|| { + err!( + "error converting datetime {dt} to instant in time zone {tz}", + dt = self.datetime(), + tz = self.time_zone().diagnostic_name(), + ) + })?; Ok(ts.to_zoned(self.tz)) } @@ -1203,10 +1220,13 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn unambiguous(self) -> Result { - let ts = self - .ts - .unambiguous() - .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; + let ts = self.ts.unambiguous().with_context(|| { + err!( + "error converting datetime {dt} to instant in time zone {tz}", + dt = self.datetime(), + tz = self.time_zone().diagnostic_name(), + ) + })?; Ok(ts.to_zoned(self.tz)) } diff --git a/src/tz/concatenated.rs b/src/tz/concatenated.rs index 2d268ee..84c1257 100644 --- a/src/tz/concatenated.rs +++ b/src/tz/concatenated.rs @@ -4,10 +4,7 @@ use alloc::{ }; use crate::{ - error::{ - tz::concatenated::{Error as E, ALLOC_LIMIT}, - Error, ErrorContext, - }, + error::{err, Error, ErrorContext}, tz::TimeZone, util::{array_str::ArrayStr, escape, utf8}, }; @@ -74,7 +71,7 @@ impl ConcatenatedTzif { alloc(scratch1, self.header.index_len())?; self.rdr .read_exact_at(scratch1, self.header.index_offset) - .context(E::FailedReadIndex)?; + .context("failed to read index block")?; let mut index = &**scratch1; while !index.is_empty() { @@ -97,7 +94,7 @@ impl ConcatenatedTzif { let start = self.header.data_offset.saturating_add(entry.start()); self.rdr .read_exact_at(scratch2, start) - .context(E::FailedReadData)?; + .context("failed to read TZif data block")?; return TimeZone::tzif(name, scratch2).map(Some); } Ok(None) @@ -117,7 +114,7 @@ impl ConcatenatedTzif { alloc(scratch, self.header.index_len())?; self.rdr .read_exact_at(scratch, self.header.index_offset) - .context(E::FailedReadIndex)?; + .context("failed to read index block")?; let names_len = self.header.index_len() / IndexEntry::LEN; // Why are we careless with this alloc? Well, its size is proportional @@ -157,17 +154,31 @@ impl Header { fn read(rdr: &R) -> Result { // 12 bytes plus 3 4-byte big endian integers. let mut buf = [0; 12 + 3 * 4]; - rdr.read_exact_at(&mut buf, 0).context(E::FailedReadHeader)?; + rdr.read_exact_at(&mut buf, 0) + .context("failed to read concatenated TZif header")?; if &buf[..6] != b"tzdata" { - return Err(Error::from(E::ExpectedFirstSixBytes)); + return Err(err!( + "expected first 6 bytes of concatenated TZif header \ + to be `tzdata`, but found `{found}`", + found = escape::Bytes(&buf[..6]), + )); } if buf[11] != 0 { - return Err(Error::from(E::ExpectedLastByte)); + return Err(err!( + "expected last byte of concatenated TZif header \ + to be NUL, but found `{found}`", + found = escape::Bytes(&buf[..12]), + )); } let version = { - let version = core::str::from_utf8(&buf[6..11]) - .map_err(|_| E::ExpectedVersion)?; + let version = core::str::from_utf8(&buf[6..11]).map_err(|_| { + err!( + "expected version in concatenated TZif header to \ + be valid UTF-8, but found `{found}`", + found = escape::Bytes(&buf[6..11]), + ) + })?; // OK because `version` is exactly 5 bytes, by construction. ArrayStr::new(version).unwrap() }; @@ -176,12 +187,19 @@ impl Header { // OK because the sub-slice is sized to exactly 4 bytes. let data_offset = u64::from(read_be32(&buf[16..20])); if index_offset > data_offset { - return Err(Error::from(E::InvalidIndexDataOffsets)); + return Err(err!( + "invalid index ({index_offset}) and data ({data_offset}) \ + offsets, expected index offset to be less than or equal \ + to data offset", + )); } // we don't read 20..24 since we don't care about zonetab (yet) let header = Header { version, index_offset, data_offset }; if header.index_len() % IndexEntry::LEN != 0 { - return Err(Error::from(E::InvalidLengthIndexBlock)); + return Err(err!( + "length of index block is not a multiple {len}", + len = IndexEntry::LEN, + )); } Ok(header) } @@ -250,8 +268,12 @@ impl<'a> IndexEntry<'a> { /// /// This returns an error if the name isn't valid UTF-8. fn name(&self) -> Result<&str, Error> { - core::str::from_utf8(self.name_bytes()) - .map_err(|_| Error::from(E::ExpectedIanaName)) + core::str::from_utf8(self.name_bytes()).map_err(|_| { + err!( + "IANA time zone identifier `{name}` is not valid UTF-8", + name = escape::Bytes(self.name_bytes()), + ) + }) } /// Returns the IANA time zone identifier as a byte slice. @@ -328,12 +350,20 @@ fn read_be32(bytes: &[u8]) -> u32 { impl Read for [u8] { fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> { let offset = usize::try_from(offset) - .map_err(|_| E::InvalidOffsetOverflowSlice)?; + .map_err(|_| err!("offset `{offset}` overflowed `usize`"))?; let Some(slice) = self.get(offset..) else { - return Err(Error::from(E::InvalidOffsetTooBig)); + return Err(err!( + "given offset `{offset}` is not valid \ + (only {len} bytes are available)", + len = self.len(), + )); }; if buf.len() > slice.len() { - return Err(Error::from(E::ExpectedMoreData)); + return Err(err!( + "unexpected EOF, expected {len} bytes but only have {have}", + len = buf.len(), + have = slice.len() + )); } buf.copy_from_slice(&slice[..buf.len()]); Ok(()) @@ -365,7 +395,9 @@ impl Read for std::fs::File { offset = u64::try_from(n) .ok() .and_then(|n| n.checked_add(offset)) - .ok_or(E::InvalidOffsetOverflowFile)?; + .ok_or_else(|| { + err!("offset overflow when reading from `File`") + })?; } Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} Err(e) => return Err(Error::io(e)), @@ -387,9 +419,9 @@ impl Read for std::fs::File { fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> { use std::io::{Read as _, Seek as _, SeekFrom}; let mut file = self; - file.seek(SeekFrom::Start(offset)) - .map_err(Error::io) - .context(E::FailedSeek)?; + file.seek(SeekFrom::Start(offset)).map_err(Error::io).with_context( + || err!("failed to seek to offset {offset} in `File`"), + )?; file.read_exact(buf).map_err(Error::io) } } @@ -411,13 +443,31 @@ impl Read for std::fs::File { /// enough in that kind of environment by far. The goal is to avoid OOM for /// exorbitantly large allocations through some kind of attack vector. fn alloc(bytes: &mut Vec, additional: usize) -> Result<(), Error> { - if additional > ALLOC_LIMIT { - return Err(Error::from(E::AllocRequestOverLimit)); + // At time of writing, the biggest TZif data file is a few KB. And the + // index block is tens of KB. So impose a limit that is a couple of orders + // of magnitude bigger, but still overall pretty small for... some systems. + // Anyway, I welcome improvements to this heuristic! + const LIMIT: usize = 10 * 1 << 20; + + if additional > LIMIT { + return Err(err!( + "attempted to allocate more than {LIMIT} bytes \ + while reading concatenated TZif data, which \ + exceeds a heuristic limit to prevent huge allocations \ + (please file a bug if this error is inappropriate)", + )); } - bytes.try_reserve_exact(additional).map_err(|_| E::AllocFailed)?; + bytes.try_reserve_exact(additional).map_err(|_| { + err!( + "failed to allocation {additional} bytes \ + for reading concatenated TZif data" + ) + })?; // This... can't actually happen right? - let new_len = - bytes.len().checked_add(additional).ok_or(E::AllocOverflow)?; + let new_len = bytes + .len() + .checked_add(additional) + .ok_or_else(|| err!("total allocation length overflowed `usize`"))?; bytes.resize(new_len, 0); Ok(()) } diff --git a/src/tz/db/bundled/disabled.rs b/src/tz/db/bundled/disabled.rs index 84d8062..7b0256a 100644 --- a/src/tz/db/bundled/disabled.rs +++ b/src/tz/db/bundled/disabled.rs @@ -25,6 +25,6 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("Bundled(unavailable)") + write!(f, "Bundled(unavailable)") } } diff --git a/src/tz/db/bundled/enabled.rs b/src/tz/db/bundled/enabled.rs index 434b3a0..7850ccb 100644 --- a/src/tz/db/bundled/enabled.rs +++ b/src/tz/db/bundled/enabled.rs @@ -26,8 +26,8 @@ impl Database { Err(_err) => { warn!( "failed to parse TZif data from bundled \ - tzdb for time zone `{canonical_name}` \ - (this is likely a bug, please report it): {_err}" + tzdb for time zone {canonical_name} \ + (this is like a bug, please report it): {_err}" ); return None; } @@ -48,7 +48,7 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("Bundled(available)") + write!(f, "Bundled(available)") } } diff --git a/src/tz/db/concatenated/disabled.rs b/src/tz/db/concatenated/disabled.rs index 06b8f78..4da2274 100644 --- a/src/tz/db/concatenated/disabled.rs +++ b/src/tz/db/concatenated/disabled.rs @@ -10,12 +10,14 @@ impl Database { #[cfg(feature = "std")] pub(crate) fn from_path( - _path: &std::path::Path, + path: &std::path::Path, ) -> Result { - Err(crate::error::Error::from( - crate::error::CrateFeatureError::TzdbConcatenated, - ) - .context(crate::error::tz::db::Error::DisabledConcatenated)) + Err(crate::error::err!( + "system concatenated tzdb unavailable: \ + crate feature `tzdb-concatenated` is disabled, \ + opening tzdb at {path} has therefore failed", + path = path.display(), + )) } pub(crate) fn none() -> Database { @@ -39,6 +41,6 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("Concatenated(unavailable)") + write!(f, "Concatenated(unavailable)") } } diff --git a/src/tz/db/concatenated/enabled.rs b/src/tz/db/concatenated/enabled.rs index 4c95896..bd28720 100644 --- a/src/tz/db/concatenated/enabled.rs +++ b/src/tz/db/concatenated/enabled.rs @@ -13,7 +13,7 @@ use std::{ }; use crate::{ - error::{tz::db::Error as E, Error}, + error::{err, Error}, timestamp::Timestamp, tz::{ concatenated::ConcatenatedTzif, db::special_time_zone, TimeZone, @@ -203,13 +203,13 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("Concatenated(")?; + write!(f, "Concatenated(")?; if let Some(ref path) = self.path { - path.display().fmt(f)?; + write!(f, "{}", path.display())?; } else { - f.write_str("unavailable")?; + write!(f, "unavailable")?; } - f.write_str(")") + write!(f, ")") } } @@ -540,7 +540,11 @@ fn read_names_and_version( let names: Vec> = db.available(scratch)?.into_iter().map(Arc::from).collect(); if names.is_empty() { - return Err(Error::from(E::ConcatenatedMissingIanaIdentifiers)); + return Err(err!( + "found no IANA time zone identifiers in \ + concatenated tzdata file at {path}", + path = path.display(), + )); } Ok((names, db.version())) } diff --git a/src/tz/db/mod.rs b/src/tz/db/mod.rs index 8c004d3..d8d7a4e 100644 --- a/src/tz/db/mod.rs +++ b/src/tz/db/mod.rs @@ -1,5 +1,5 @@ use crate::{ - error::{tz::db::Error as E, Error}, + error::{err, Error}, tz::TimeZone, util::{sync::Arc, utf8}, }; @@ -457,10 +457,22 @@ impl TimeZoneDatabase { /// # Ok::<(), Box>(()) /// ``` pub fn get(&self, name: &str) -> Result { - let inner = self - .inner - .as_deref() - .ok_or_else(|| E::failed_time_zone_no_database_configured(name))?; + let inner = self.inner.as_deref().ok_or_else(|| { + if cfg!(feature = "std") { + err!( + "failed to find time zone `{name}` since there is no \ + time zone database configured", + ) + } else { + err!( + "failed to find time zone `{name}`, there is no \ + global time zone database configured (and is currently \ + impossible to do so without Jiff's `std` feature \ + enabled, if you need this functionality, please file \ + an issue on Jiff's tracker with your use case)", + ) + } + })?; match *inner { Kind::ZoneInfo(ref db) => { if let Some(tz) = db.get(name) { @@ -481,7 +493,7 @@ impl TimeZoneDatabase { } } } - Err(Error::from(E::failed_time_zone(name))) + Err(err!("failed to find time zone `{name}` in time zone database")) } /// Returns a list of all available time zone identifiers from this @@ -560,16 +572,16 @@ impl TimeZoneDatabase { impl core::fmt::Debug for TimeZoneDatabase { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("TimeZoneDatabase(")?; + write!(f, "TimeZoneDatabase(")?; let Some(inner) = self.inner.as_deref() else { - return f.write_str("unavailable)"); + return write!(f, "unavailable)"); }; match *inner { - Kind::ZoneInfo(ref db) => core::fmt::Debug::fmt(db, f)?, - Kind::Concatenated(ref db) => core::fmt::Debug::fmt(db, f)?, - Kind::Bundled(ref db) => core::fmt::Debug::fmt(db, f)?, + Kind::ZoneInfo(ref db) => write!(f, "{db:?}")?, + Kind::Concatenated(ref db) => write!(f, "{db:?}")?, + Kind::Bundled(ref db) => write!(f, "{db:?}")?, } - f.write_str(")") + write!(f, ")") } } @@ -675,7 +687,7 @@ impl<'d> TimeZoneName<'d> { impl<'d> core::fmt::Display for TimeZoneName<'d> { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str(self.as_str()) + write!(f, "{}", self.as_str()) } } diff --git a/src/tz/db/zoneinfo/disabled.rs b/src/tz/db/zoneinfo/disabled.rs index 0290977..a9c79d4 100644 --- a/src/tz/db/zoneinfo/disabled.rs +++ b/src/tz/db/zoneinfo/disabled.rs @@ -10,12 +10,14 @@ impl Database { #[cfg(feature = "std")] pub(crate) fn from_dir( - _dir: &std::path::Path, + dir: &std::path::Path, ) -> Result { - Err(crate::error::Error::from( - crate::error::CrateFeatureError::TzdbZoneInfo, - ) - .context(crate::error::tz::db::Error::DisabledZoneInfo)) + Err(crate::error::err!( + "system tzdb unavailable: \ + crate feature `tzdb-zoneinfo` is disabled, \ + opening tzdb at {dir} has therefore failed", + dir = dir.display(), + )) } pub(crate) fn none() -> Database { @@ -39,6 +41,6 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("ZoneInfo(unavailable)") + write!(f, "ZoneInfo(unavailable)") } } diff --git a/src/tz/db/zoneinfo/enabled.rs b/src/tz/db/zoneinfo/enabled.rs index f4237d4..1390394 100644 --- a/src/tz/db/zoneinfo/enabled.rs +++ b/src/tz/db/zoneinfo/enabled.rs @@ -17,7 +17,7 @@ use std::{ }; use crate::{ - error::{tz::db::Error as E, Error}, + error::{err, Error}, timestamp::Timestamp, tz::{ db::special_time_zone, tzif::is_possibly_tzif, TimeZone, @@ -204,13 +204,13 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("ZoneInfo(")?; + write!(f, "ZoneInfo(")?; if let Some(ref dir) = self.dir { - core::fmt::Display::fmt(&dir.display(), f)?; + write!(f, "{}", dir.display())?; } else { - f.write_str("unavailable")?; + write!(f, "unavailable")?; } - f.write_str(")") + write!(f, ")") } } @@ -560,7 +560,7 @@ impl ZoneInfoName { fn new(base: &Path, time_zone_name: &Path) -> Result { let full = base.join(time_zone_name); let original = parse::os_str_utf8(time_zone_name.as_os_str()) - .map_err(|err| Error::from(err).path(base))?; + .map_err(|err| err.path(base))?; let lower = original.to_ascii_lowercase(); let inner = ZoneInfoNameInner { full, @@ -792,18 +792,14 @@ fn walk(start: &Path) -> Result, Error> { let time_zone_name = match path.strip_prefix(start) { Ok(time_zone_name) => time_zone_name, - // I think this error case is actually not possible. - // Or if it does, is a legitimate bug. Namely, `start` - // should always be a prefix of `path`, since `path` - // is itself derived, ultimately, from `start`. - Err(_err) => { + Err(err) => { trace!( "failed to extract time zone name from {} \ - using {} as a base: {_err}", + using {} as a base: {err}", path.display(), start.display(), ); - seterr(&path, Error::from(E::ZoneInfoStripPrefix)); + seterr(&path, Error::adhoc(err)); continue; } }; @@ -821,7 +817,7 @@ fn walk(start: &Path) -> Result, Error> { if names.is_empty() { let err = first_err .take() - .unwrap_or_else(|| Error::from(E::ZoneInfoNoTzifFiles)); + .unwrap_or_else(|| err!("{}: no TZif files", start.display())); Err(err) } else { // If we found at least one valid name, then we declare success and diff --git a/src/tz/offset.rs b/src/tz/offset.rs index f081cf8..b7b492a 100644 --- a/src/tz/offset.rs +++ b/src/tz/offset.rs @@ -6,7 +6,7 @@ use core::{ use crate::{ civil, duration::{Duration, SDuration}, - error::{tz::offset::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, shared::util::itime::IOffset, span::Span, timestamp::Timestamp, @@ -526,8 +526,12 @@ impl Offset { .to_idatetime() .zip2(self.to_ioffset()) .map(|(idt, ioff)| idt.to_timestamp(ioff)); - Timestamp::from_itimestamp(its) - .context(E::ConvertDateTimeToTimestamp { offset: self }) + Timestamp::from_itimestamp(its).with_context(|| { + err!( + "converting {dt} with offset {offset} to timestamp overflowed", + offset = self, + ) + }) } /// Adds the given span of time to this offset. @@ -656,11 +660,21 @@ impl Offset { ) -> Result { let duration = t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs()) - .context(E::OverflowAddSignedDuration)?; + .with_context(|| { + err!( + "adding signed duration {duration:?} \ + to offset {self} overflowed maximum offset seconds" + ) + })?; let offset_seconds = self.seconds_ranged(); let seconds = offset_seconds .try_checked_add("offset-seconds", duration) - .context(E::OverflowAddSignedDuration)?; + .with_context(|| { + err!( + "adding signed duration {duration:?} \ + to offset {self} overflowed" + ) + })?; Ok(Offset::from_seconds_ranged(seconds)) } @@ -961,7 +975,8 @@ impl Offset { /// assert_eq!(Offset::MAX.to_string(), "+25:59:59"); /// assert_eq!( /// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(), - /// "rounding time zone offset resulted in a duration that overflows", + /// "rounding offset `+25:59:59` resulted in a duration of 26h, \ + /// which overflows `Offset`", /// ); /// ``` #[inline] @@ -1110,7 +1125,7 @@ impl core::fmt::Display for Offset { let minutes = self.part_minutes_ranged().abs().get(); let seconds = self.part_seconds_ranged().abs().get(); if hours == 0 && minutes == 0 && seconds == 0 { - f.write_str("+00") + write!(f, "+00") } else if hours != 0 && minutes == 0 && seconds == 0 { write!(f, "{sign}{hours:02}") } else if minutes != 0 && seconds == 0 { @@ -1332,10 +1347,11 @@ impl TryFrom for Offset { } else if subsec <= -500_000_000 { seconds = seconds.saturating_sub(1); } - let seconds = - i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?; + let seconds = i32::try_from(seconds).map_err(|_| { + err!("`SignedDuration` of {sdur} overflows `Offset`") + })?; Offset::from_seconds(seconds) - .map_err(|_| Error::from(E::OverflowSignedDuration)) + .map_err(|_| err!("`SignedDuration` of {sdur} overflows `Offset`")) } } @@ -1583,11 +1599,20 @@ impl OffsetRound { fn round(&self, offset: Offset) -> Result { let smallest = self.0.get_smallest(); if !(Unit::Second <= smallest && smallest <= Unit::Hour) { - return Err(Error::from(E::RoundInvalidUnit { unit: smallest })); + return Err(err!( + "rounding `Offset` failed because \ + a unit of {plural} was provided, but offset rounding \ + can only use hours, minutes or seconds", + plural = smallest.plural(), + )); } let rounded_sdur = SignedDuration::from(offset).round(self.0)?; - Offset::try_from(rounded_sdur) - .map_err(|_| Error::from(E::RoundOverflow)) + Offset::try_from(rounded_sdur).map_err(|_| { + err!( + "rounding offset `{offset}` resulted in a duration \ + of {rounded_sdur:?}, which overflows `Offset`", + ) + }) } } @@ -1863,10 +1888,10 @@ impl OffsetConflict { /// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone()); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "datetime could not resolve to a timestamp since `reject` \ - /// conflict resolution was chosen, and because datetime has offset \ - /// `-00:45`, but the time zone `Africa/Monrovia` for the given \ - /// datetime unambiguously has offset `-00:44:30`", + /// "datetime 1968-02-01T23:15:00 could not resolve to a timestamp \ + /// since 'reject' conflict resolution was chosen, and because \ + /// datetime has offset -00:45, but the time zone Africa/Monrovia \ + /// for the given datetime unambiguously has offset -00:44:30", /// ); /// let is_equal = |parsed: Offset, candidate: Offset| { /// parsed == candidate || candidate.round(Unit::Minute).map_or( @@ -1925,10 +1950,11 @@ impl OffsetConflict { /// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::(); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "datetime could not resolve to a timestamp since `reject` \ - /// conflict resolution was chosen, and because datetime has offset \ - /// `-00:45`, but the time zone `Africa/Monrovia` for the given \ - /// datetime unambiguously has offset `-00:44:30`", + /// "parsing \"1970-06-01T00-00:45:00[Africa/Monrovia]\" failed: \ + /// datetime 1970-06-01T00:00:00 could not resolve to a timestamp \ + /// since 'reject' conflict resolution was chosen, and because \ + /// datetime has offset -00:45, but the time zone Africa/Monrovia \ + /// for the given datetime unambiguously has offset -00:44:30", /// ); /// ``` pub fn resolve_with( @@ -2020,13 +2046,13 @@ impl OffsetConflict { let amb = tz.to_ambiguous_timestamp(dt); match amb.offset() { - Unambiguous { offset } if !is_equal(given, offset) => { - Err(Error::from(E::ResolveRejectUnambiguous { - given, - offset, - tz, - })) - } + Unambiguous { offset } if !is_equal(given, offset) => Err(err!( + "datetime {dt} could not resolve to a timestamp since \ + 'reject' conflict resolution was chosen, and because \ + datetime has offset {given}, but the time zone {tzname} for \ + the given datetime unambiguously has offset {offset}", + tzname = tz.diagnostic_name(), + )), Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)), Gap { before, after } => { // In `jiff 0.1`, we reported an error when we found a gap @@ -2039,22 +2065,28 @@ impl OffsetConflict { // changed to treat all offsets in a gap as invalid). // // Ref: https://github.com/tc39/proposal-temporal/issues/2892 - Err(Error::from(E::ResolveRejectGap { - given, - before, - after, - tz, - })) + Err(err!( + "datetime {dt} could not resolve to timestamp \ + since 'reject' conflict resolution was chosen, and \ + because datetime has offset {given}, but the time \ + zone {tzname} for the given datetime falls in a gap \ + (between offsets {before} and {after}), and all \ + offsets for a gap are regarded as invalid", + tzname = tz.diagnostic_name(), + )) } Fold { before, after } if !is_equal(given, before) && !is_equal(given, after) => { - Err(Error::from(E::ResolveRejectFold { - given, - before, - after, - tz, - })) + Err(err!( + "datetime {dt} could not resolve to timestamp \ + since 'reject' conflict resolution was chosen, and \ + because datetime has offset {given}, but the time \ + zone {tzname} for the given datetime falls in a fold \ + between offsets {before} and {after}, neither of which \ + match the offset", + tzname = tz.diagnostic_name(), + )) } Fold { .. } => { let kind = Unambiguous { offset: given }; diff --git a/src/tz/posix.rs b/src/tz/posix.rs index ee65b71..cd0d224 100644 --- a/src/tz/posix.rs +++ b/src/tz/posix.rs @@ -72,14 +72,14 @@ use core::fmt::Debug; use crate::{ civil::DateTime, - error::{tz::posix::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, shared, timestamp::Timestamp, tz::{ timezone::TimeZoneAbbreviation, AmbiguousOffset, Dst, Offset, TimeZoneOffsetInfo, TimeZoneTransition, }, - util::{array_str::Abbreviation, parse}, + util::{array_str::Abbreviation, escape::Bytes, parse}, }; /// The result of parsing the POSIX `TZ` environment variable. @@ -114,7 +114,11 @@ impl PosixTzEnv { let bytes = bytes.as_ref(); if bytes.get(0) == Some(&b':') { let Ok(string) = core::str::from_utf8(&bytes[1..]) else { - return Err(Error::from(E::ColonPrefixInvalidUtf8)); + return Err(err!( + "POSIX time zone string with a ':' prefix contains \ + invalid UTF-8: {:?}", + Bytes(&bytes[1..]), + )); }; Ok(PosixTzEnv::Implementation(string.into())) } else { @@ -134,11 +138,8 @@ impl PosixTzEnv { impl core::fmt::Display for PosixTzEnv { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { - PosixTzEnv::Rule(ref tz) => core::fmt::Display::fmt(tz, f), - PosixTzEnv::Implementation(ref imp) => { - f.write_str(":")?; - core::fmt::Display::fmt(imp, f) - } + PosixTzEnv::Rule(ref tz) => write!(f, "{tz}"), + PosixTzEnv::Implementation(ref imp) => write!(f, ":{imp}"), } } } @@ -210,8 +211,10 @@ impl PosixTimeZone { ) -> Result { let bytes = bytes.as_ref(); let inner = shared::PosixTimeZone::parse(bytes.as_ref()) - .map_err(Error::posix_tz) - .context(E::InvalidPosixTz)?; + .map_err(Error::shared) + .map_err(|e| { + e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes))) + })?; Ok(PosixTimeZone { inner }) } @@ -224,8 +227,13 @@ impl PosixTimeZone { let bytes = bytes.as_ref(); let (inner, remaining) = shared::PosixTimeZone::parse_prefix(bytes.as_ref()) - .map_err(Error::posix_tz) - .context(E::InvalidPosixTz)?; + .map_err(Error::shared) + .map_err(|e| { + e.context(err!( + "invalid POSIX TZ string {:?}", + Bytes(bytes) + )) + })?; Ok((PosixTimeZone { inner }, remaining)) } diff --git a/src/tz/system/mod.rs b/src/tz/system/mod.rs index a8e6487..8cae45c 100644 --- a/src/tz/system/mod.rs +++ b/src/tz/system/mod.rs @@ -3,7 +3,7 @@ use std::{sync::RwLock, time::Duration}; use alloc::string::ToString; use crate::{ - error::{tz::system::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase}, util::cache::Expiration, }; @@ -141,20 +141,22 @@ pub(crate) fn get(db: &TimeZoneDatabase) -> Result { pub(crate) fn get_force(db: &TimeZoneDatabase) -> Result { match get_env_tz(db) { Ok(Some(tz)) => { - debug!("checked `TZ` environment variable and found {tz:?}"); + debug!("checked TZ environment variable and found {tz:?}"); return Ok(tz); } Ok(None) => { - debug!("`TZ` environment variable is not set"); + debug!("TZ environment variable is not set"); } Err(err) => { - return Err(err.context(E::FailedEnvTz)); + return Err(err.context( + "TZ environment variable set, but failed to read value", + )); } } if let Some(tz) = sys::get(db) { return Ok(tz); } - Err(Error::from(E::FailedSystemTimeZone)) + Err(err!("failed to find system time zone")) } /// Materializes a `TimeZone` from a `TZ` environment variable. @@ -182,8 +184,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result, Error> { // `TZ=UTC`. if tzenv.is_empty() { debug!( - "`TZ` environment variable set to empty value, \ - assuming `TZ=UTC` in order to conform to \ + "TZ environment variable set to empty value, \ + assuming TZ=UTC in order to conform to \ widespread convention among Unix tooling", ); return Ok(Some(TimeZone::UTC)); @@ -194,7 +196,15 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result, Error> { "failed to parse {tzenv:?} as POSIX TZ rule \ (attempting to treat it as an IANA time zone): {_err}", ); - tzenv.to_str().ok_or(E::FailedPosixTzAndUtf8)?.to_string() + tzenv + .to_str() + .ok_or_else(|| { + err!( + "failed to parse {tzenv:?} as a POSIX TZ transition \ + string, or as valid UTF-8", + ) + })? + .to_string() } Ok(PosixTzEnv::Implementation(string)) => string.to_string(), Ok(PosixTzEnv::Rule(tz)) => { @@ -221,20 +231,26 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result, Error> { // No zoneinfo means this is probably a IANA Time Zone name. But... // it could just be a file path. debug!( - "could not find {needle:?} in `TZ={tz_name_or_path:?}`, \ - therefore attempting lookup in `{db:?}`", + "could not find {needle:?} in TZ={tz_name_or_path:?}, \ + therefore attempting lookup in {db:?}", ); return match db.get(&tz_name_or_path) { Ok(tz) => Ok(Some(tz)), Err(_err) => { debug!( - "using `TZ={tz_name_or_path:?}` as time zone name failed, \ - could not find time zone in zoneinfo database `{db:?}` \ + "using TZ={tz_name_or_path:?} as time zone name failed, \ + could not find time zone in zoneinfo database {db:?} \ (continuing to try and read `{tz_name_or_path}` as \ a TZif file)", ); sys::read(db, &tz_name_or_path) - .ok_or_else(|| Error::from(E::FailedEnvTzAsTzif)) + .ok_or_else(|| { + err!( + "failed to read TZ={tz_name_or_path:?} \ + as a TZif file after attempting a tzdb \ + lookup for `{tz_name_or_path}`", + ) + }) .map(Some) } }; @@ -244,16 +260,16 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result, Error> { // `zoneinfo/`. Once we have that, we try to look it up in our tzdb. let name = &tz_name_or_path[rpos + needle.len()..]; debug!( - "extracted `{name}` from `TZ={tz_name_or_path}` \ + "extracted {name:?} from TZ={tz_name_or_path:?} \ and assuming it is an IANA time zone name", ); match db.get(&name) { Ok(tz) => return Ok(Some(tz)), Err(_err) => { debug!( - "using `{name}` from `TZ={tz_name_or_path}`, \ - could not find time zone in zoneinfo database `{db:?}` \ - (continuing to try and use `{tz_name_or_path}`)", + "using {name:?} from TZ={tz_name_or_path:?}, \ + could not find time zone in zoneinfo database {db:?} \ + (continuing to try and use {tz_name_or_path:?})", ); } } @@ -263,7 +279,13 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result, Error> { // and read the data as TZif. This will give us time zone data if it works, // but without a name. sys::read(db, &tz_name_or_path) - .ok_or_else(|| Error::from(E::FailedEnvTzAsTzif)) + .ok_or_else(|| { + err!( + "failed to read TZ={tz_name_or_path:?} \ + as a TZif file after attempting a tzdb \ + lookup for `{name}`", + ) + }) .map(Some) } @@ -276,8 +298,8 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result, Error> { fn read_unnamed_tzif_file(path: &str) -> Result { let data = std::fs::read(path) .map_err(Error::io) - .context(E::FailedUnnamedTzifRead)?; - let tz = - TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?; + .with_context(|| err!("failed to read {path:?} as TZif file"))?; + let tz = TimeZone::tzif_system(&data) + .with_context(|| err!("found invalid TZif data at {path:?}"))?; Ok(tz) } diff --git a/src/tz/system/windows/mod.rs b/src/tz/system/windows/mod.rs index 1711b31..ec23143 100644 --- a/src/tz/system/windows/mod.rs +++ b/src/tz/system/windows/mod.rs @@ -8,7 +8,7 @@ use windows_sys::Win32::System::Time::{ }; use crate::{ - error::{tz::system::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, tz::{TimeZone, TimeZoneDatabase}, util::utf8, }; @@ -79,12 +79,16 @@ fn windows_to_iana(tz_key_name: &str) -> Result<&'static str, Error> { utf8::cmp_ignore_ascii_case(win_name, &tz_key_name) }); let Ok(index) = result else { - return Err(Error::from(E::WindowsMissingIanaMapping)); + return Err(err!( + "found Windows time zone name {tz_key_name}, \ + but could not find a mapping for it to an \ + IANA time zone name", + )); }; let iana_name = WINDOWS_TO_IANA[index].1; trace!( - "found Windows time zone name `{tz_key_name}`, and \ - successfully mapped it to IANA time zone `{iana_name}`", + "found Windows time zone name {tz_key_name}, and \ + successfully mapped it to IANA time zone {iana_name}", ); Ok(iana_name) } @@ -103,19 +107,24 @@ fn get_tz_key_name() -> Result { // when `info` is properly initialized. let info = unsafe { info.assume_init() }; let tz_key_name = nul_terminated_utf16_to_string(&info.TimeZoneKeyName) - .context(E::WindowsTimeZoneKeyName)?; + .context( + "could not get TimeZoneKeyName from \ + winapi DYNAMIC_TIME_ZONE_INFORMATION", + )?; Ok(tz_key_name) } fn nul_terminated_utf16_to_string( code_units: &[u16], ) -> Result { - let nul = code_units - .iter() - .position(|&cu| cu == 0) - .ok_or(E::WindowsUtf16DecodeNul)?; + let nul = code_units.iter().position(|&cu| cu == 0).ok_or_else(|| { + err!("failed to convert u16 slice to UTF-8 (no NUL terminator found)") + })?; let string = String::from_utf16(&code_units[..nul]) - .map_err(|_| E::WindowsUtf16DecodeInvalid)?; + .map_err(Error::adhoc) + .with_context(|| { + err!("failed to convert u16 slice to UTF-8 (invalid UTF-16)") + })?; Ok(string) } diff --git a/src/tz/timezone.rs b/src/tz/timezone.rs index 297ecff..0664cb3 100644 --- a/src/tz/timezone.rs +++ b/src/tz/timezone.rs @@ -1,6 +1,6 @@ use crate::{ civil::DateTime, - error::{tz::timezone::Error as E, Error}, + error::{err, Error}, tz::{ ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned}, offset::{Dst, Offset}, @@ -392,8 +392,10 @@ impl TimeZone { pub fn try_system() -> Result { #[cfg(not(feature = "tz-system"))] { - Err(Error::from(crate::error::CrateFeatureError::TzSystem) - .context(E::FailedSystem)) + Err(err!( + "failed to get system time zone since 'tz-system' \ + crate feature is not enabled", + )) } #[cfg(feature = "tz-system")] { @@ -914,7 +916,7 @@ impl TimeZone { /// assert_eq!( /// tz.to_fixed_offset().unwrap_err().to_string(), /// "cannot convert non-fixed IANA time zone \ - /// to offset without a timestamp or civil datetime", + /// to offset without timestamp or civil datetime", /// ); /// /// let tz = TimeZone::UTC; @@ -933,7 +935,11 @@ impl TimeZone { #[inline] pub fn to_fixed_offset(&self) -> Result { let mkerr = || { - Error::from(E::ConvertNonFixed { kind: self.kind_description() }) + err!( + "cannot convert non-fixed {kind} time zone to offset \ + without timestamp or civil datetime", + kind = self.kind_description(), + ) }; repr::each! { &self.repr, @@ -1386,7 +1392,7 @@ impl TimeZone { /// Returns a short description about the kind of this time zone. /// /// This is useful in error messages. - fn kind_description(&self) -> &'static str { + fn kind_description(&self) -> &str { repr::each! { &self.repr, UTC => "UTC", @@ -1881,12 +1887,12 @@ impl<'a> core::fmt::Display for DiagnosticName<'a> { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { repr::each! { &self.0.repr, - UTC => f.write_str("UTC"), - UNKNOWN => f.write_str("Etc/Unknown"), - FIXED(offset) => offset.fmt(f), - STATIC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")), - ARC_TZIF(tzif) => f.write_str(tzif.name().unwrap_or("Local")), - ARC_POSIX(posix) => posix.fmt(f), + UTC => write!(f, "UTC"), + UNKNOWN => write!(f, "Etc/Unknown"), + FIXED(offset) => write!(f, "{offset}"), + STATIC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")), + ARC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")), + ARC_POSIX(posix) => write!(f, "{posix}"), } } } @@ -1933,10 +1939,6 @@ impl<'t> TimeZoneAbbreviation<'t> { /// /// This module exists to _encapsulate_ the representation rigorously and /// expose a safe and sound API. -// To squash warnings on older versions of Rust. Our polyfill below should -// match what std does on newer versions of Rust, so the confusability should -// be fine. ---AG -#[allow(unstable_name_collisions)] mod repr { use core::mem::ManuallyDrop; @@ -2269,9 +2271,9 @@ mod repr { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { each! { self, - UTC => f.write_str("UTC"), - UNKNOWN => f.write_str("Etc/Unknown"), - FIXED(offset) => core::fmt::Debug::fmt(&offset, f), + UTC => write!(f, "UTC"), + UNKNOWN => write!(f, "Etc/Unknown"), + FIXED(offset) => write!(f, "{offset:?}"), STATIC_TZIF(tzif) => { // The full debug output is a bit much, so constrain it. let field = tzif.name().unwrap_or("Local"); @@ -2282,11 +2284,7 @@ mod repr { let field = tzif.name().unwrap_or("Local"); f.debug_tuple("TZif").field(&field).finish() }, - ARC_POSIX(posix) => { - f.write_str("Posix(")?; - core::fmt::Display::fmt(&posix, f)?; - f.write_str(")") - }, + ARC_POSIX(posix) => write!(f, "Posix({posix})"), } } } diff --git a/src/tz/tzif.rs b/src/tz/tzif.rs index 4271ff5..6f2c815 100644 --- a/src/tz/tzif.rs +++ b/src/tz/tzif.rs @@ -157,7 +157,8 @@ impl TzifOwned { name: Option, bytes: &[u8], ) -> Result { - let sh = shared::TzifOwned::parse(name, bytes).map_err(Error::tzif)?; + let sh = + shared::TzifOwned::parse(name, bytes).map_err(Error::shared)?; Ok(TzifOwned::from_shared_owned(sh)) } @@ -570,9 +571,9 @@ impl shared::TzifLocalTimeType { impl core::fmt::Display for shared::TzifIndicator { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { - shared::TzifIndicator::LocalWall => f.write_str("local/wall"), - shared::TzifIndicator::LocalStandard => f.write_str("local/std"), - shared::TzifIndicator::UTStandard => f.write_str("ut/std"), + shared::TzifIndicator::LocalWall => write!(f, "local/wall"), + shared::TzifIndicator::LocalStandard => write!(f, "local/std"), + shared::TzifIndicator::UTStandard => write!(f, "ut/std"), } } } diff --git a/src/tz/zic.rs b/src/tz/zic.rs index dc702f3..28db5b0 100644 --- a/src/tz/zic.rs +++ b/src/tz/zic.rs @@ -111,10 +111,7 @@ use alloc::{ use crate::{ civil::{Date, DateTime, Time, Weekday}, - error::{ - tz::zic::{Error as E, MAX_LINE_LEN}, - Error, ErrorContext, - }, + error::{err, Error, ErrorContext}, span::{Span, SpanFieldwise, ToSpan}, timestamp::Timestamp, tz::{Dst, Offset}, @@ -208,12 +205,12 @@ impl Rules { "every name in rule group must be identical" ); let dst = Dst::from(r.save.suffix() == RuleSaveSuffixP::Dst); - let offset = r.save.to_offset().with_context(|| { - E::FailedRule { name: inner.name.as_str().into() } - })?; - let years = r.years().with_context(|| E::FailedRule { - name: inner.name.as_str().into(), + let offset = r.save.to_offset().map_err(|e| { + err!("SAVE value in rule {:?} is too big: {e}", inner.name) })?; + let years = r + .years() + .map_err(|e| e.context(err!("rule {:?}", inner.name)))?; let month = r.inn.month; let letters = r.letters.part; let day = r.on; @@ -263,12 +260,13 @@ impl ZicP { ) -> Result<(), Error> { while parser.read_next_fields()? { self.parse_one(&mut parser) - .context(E::Line { number: parser.line_number })?; + .map_err(|e| e.context(err!("line {}", parser.line_number)))?; } if let Some(ref name) = parser.continuation_zone_for { - return Err(Error::from(E::ExpectedContinuationZoneLine { - name: name.as_str().into(), - })); + return Err(err!( + "expected continuation zone line for {name:?}, \ + but found end of data instead", + )); } Ok(()) } @@ -279,8 +277,9 @@ impl ZicP { assert!(!p.fields.is_empty()); if let Some(name) = p.continuation_zone_for.take() { - let zone = ZoneContinuationP::parse(&p.fields) - .context(E::FailedContinuationZone)?; + let zone = ZoneContinuationP::parse(&p.fields).map_err(|e| { + e.context("failed to parse continuation 'Zone' line") + })?; let more_continuations = zone.until.is_some(); // OK because `p.continuation_zone_for` is only set when we have // seen a first zone with the corresponding name. @@ -294,45 +293,51 @@ impl ZicP { let (first, rest) = (&p.fields[0], &p.fields[1..]); if first.starts_with("R") && "Rule".starts_with(first) { - let rule = RuleP::parse(rest).context(E::FailedRuleLine)?; + let rule = RuleP::parse(rest) + .map_err(|e| e.context("failed to parse 'Rule' line"))?; let name = rule.name.name.clone(); self.rules.entry(name).or_default().push(rule); } else if first.starts_with("Z") && "Zone".starts_with(first) { - let first = ZoneFirstP::parse(rest).context(E::FailedZoneFirst)?; + let first = ZoneFirstP::parse(rest) + .map_err(|e| e.context("failed to parse first 'Zone' line"))?; let name = first.name.name.clone(); if first.until.is_some() { p.continuation_zone_for = Some(name.clone()); } let zone = ZoneP { first, continuations: vec![] }; if self.links.contains_key(&name) { - return Err(Error::from(E::DuplicateZoneLink { - name: name.into(), - })); + return Err(err!( + "found zone with name {name:?} that conflicts \ + with a link of the same name", + )); } if let Some(previous_zone) = self.zones.insert(name, zone) { - return Err(Error::from(E::DuplicateZone { - name: previous_zone.first.name.name.into(), - })); + return Err(err!( + "found duplicate zone for {:?}", + previous_zone.first.name.name, + )); } } else if first.starts_with("L") && "Link".starts_with(first) { - let link = LinkP::parse(rest).context(E::FailedLinkLine)?; + let link = LinkP::parse(rest) + .map_err(|e| e.context("failed to parse 'Link' line"))?; let name = link.name.name.clone(); if self.zones.contains_key(&name) { - return Err(Error::from(E::DuplicateLinkZone { - name: name.into(), - })); + return Err(err!( + "found link with name {name:?} that conflicts \ + with a zone of the same name", + )); } if let Some(previous_link) = self.links.insert(name, link) { - return Err(Error::from(E::DuplicateLink { - name: previous_link.name.name.into(), - })); + return Err(err!( + "found duplicate link for {:?}", + previous_link.name.name, + )); } // N.B. We don't check that the link's target name refers to some // other zone/link here, because the corresponding zone/link might // be defined later. } else { - return Err(Error::from(E::UnrecognizedZicLine) - .context(E::Line { number: p.line_number })); + return Err(err!("unrecognized zic line: {first:?}")); } Ok(()) } @@ -364,9 +369,10 @@ struct RuleP { impl RuleP { fn parse(fields: &[&str]) -> Result { if fields.len() != 9 { - return Err(Error::from(E::ExpectedRuleNineFields { - got: fields.len(), - })); + return Err(err!( + "expected exactly 9 fields for rule, but found {} fields", + fields.len(), + )); } let (name_field, fields) = (fields[0], &fields[1..]); let (from_field, fields) = (fields[0], &fields[1..]); @@ -380,21 +386,28 @@ impl RuleP { let name = name_field .parse::() - .context(E::FailedParseFieldName)?; + .map_err(|e| e.context("failed to parse NAME field"))?; let from = from_field .parse::() - .context(E::FailedParseFieldFrom)?; - let to = to_field.parse::().context(E::FailedParseFieldTo)?; - let inn = - in_field.parse::().context(E::FailedParseFieldIn)?; - let on = on_field.parse::().context(E::FailedParseFieldOn)?; - let at = at_field.parse::().context(E::FailedParseFieldAt)?; + .map_err(|e| e.context("failed to parse FROM field"))?; + let to = to_field + .parse::() + .map_err(|e| e.context("failed to parse TO field"))?; + let inn = in_field + .parse::() + .map_err(|e| e.context("failed to parse IN field"))?; + let on = on_field + .parse::() + .map_err(|e| e.context("failed to parse ON field"))?; + let at = at_field + .parse::() + .map_err(|e| e.context("failed to parse AT field"))?; let save = save_field .parse::() - .context(E::FailedParseFieldSave)?; + .map_err(|e| e.context("failed to parse SAVE field"))?; let letters = letters_field .parse::() - .context(E::FailedParseFieldLetters)?; + .map_err(|e| e.context("failed to parse LETTERS field"))?; Ok(RuleP { name, from, to, inn, on, at, save, letters }) } @@ -407,10 +420,9 @@ impl RuleP { RuleToP::Year { year } => year, }; if start > end { - return Err(Error::from(E::InvalidRuleYear { - start: start.get(), - end: end.get(), - })); + return Err(err!( + "found start year {start} to be greater than end year {end}" + )); } Ok(start..=end) } @@ -450,7 +462,7 @@ struct ZoneFirstP { impl ZoneFirstP { fn parse(fields: &[&str]) -> Result { if fields.len() < 4 { - return Err(Error::from(E::ExpectedFirstZoneFourFields)); + return Err(err!("first ZONE line must have at least 4 fields")); } let (name_field, fields) = (fields[0], &fields[1..]); let (stdoff_field, fields) = (fields[0], &fields[1..]); @@ -458,20 +470,23 @@ impl ZoneFirstP { let (format_field, fields) = (fields[0], &fields[1..]); let name = name_field .parse::() - .context(E::FailedParseFieldName)?; + .map_err(|e| e.context("failed to parse NAME field"))?; let stdoff = stdoff_field .parse::() - .context(E::FailedParseFieldStdOff)?; + .map_err(|e| e.context("failed to parse STDOFF field"))?; let rules = rules_field .parse::() - .context(E::FailedParseFieldRules)?; + .map_err(|e| e.context("failed to parse RULES field"))?; let format = format_field .parse::() - .context(E::FailedParseFieldFormat)?; + .map_err(|e| e.context("failed to parse FORMAT field"))?; let until = if fields.is_empty() { None } else { - Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?) + Some( + ZoneUntilP::parse(fields) + .map_err(|e| e.context("failed to parse UNTIL field"))?, + ) }; Ok(ZoneFirstP { name, stdoff, rules, format, until }) } @@ -497,24 +512,29 @@ struct ZoneContinuationP { impl ZoneContinuationP { fn parse(fields: &[&str]) -> Result { if fields.len() < 3 { - return Err(Error::from(E::ExpectedContinuationZoneThreeFields)); + return Err(err!( + "continuation ZONE line must have at least 3 fields" + )); } let (stdoff_field, fields) = (fields[0], &fields[1..]); let (rules_field, fields) = (fields[0], &fields[1..]); let (format_field, fields) = (fields[0], &fields[1..]); let stdoff = stdoff_field .parse::() - .context(E::FailedParseFieldStdOff)?; + .map_err(|e| e.context("failed to parse STDOFF field"))?; let rules = rules_field .parse::() - .context(E::FailedParseFieldRules)?; + .map_err(|e| e.context("failed to parse RULES field"))?; let format = format_field .parse::() - .context(E::FailedParseFieldFormat)?; + .map_err(|e| e.context("failed to parse FORMAT field"))?; let until = if fields.is_empty() { None } else { - Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?) + Some( + ZoneUntilP::parse(fields) + .map_err(|e| e.context("failed to parse UNTIL field"))?, + ) }; Ok(ZoneContinuationP { stdoff, rules, format, until }) } @@ -533,14 +553,17 @@ struct LinkP { impl LinkP { fn parse(fields: &[&str]) -> Result { if fields.len() != 2 { - return Err(Error::from(E::ExpectedLinkTwoFields)); + return Err(err!( + "expected exactly 2 fields after LINK, but found {}", + fields.len() + )); } let target = fields[0] .parse::() - .context(E::FailedParseFieldLinkTarget)?; + .map_err(|e| e.context("failed to parse LINK target"))?; let name = fields[1] .parse::() - .context(E::FailedParseFieldLinkName)?; + .map_err(|e| e.context("failed to parse LINK name"))?; Ok(LinkP { target, name }) } } @@ -569,9 +592,12 @@ impl FromStr for RuleNameP { // or not. We erase that information. We could rejigger things to keep // that information around, but... Meh. if name.is_empty() { - Err(Error::from(E::ExpectedNonEmptyName)) + Err(err!("NAME field for rule cannot be empty")) } else if name.starts_with(|ch| matches!(ch, '0'..='9' | '+' | '-')) { - Err(Error::from(E::ExpectedNameBegin)) + Err(err!( + "NAME field cannot begin with a digit, + or -, \ + but {name:?} begins with one of those", + )) } else { Ok(RuleNameP { name: name.to_string() }) } @@ -588,7 +614,8 @@ impl FromStr for RuleFromP { type Err = Error; fn from_str(from: &str) -> Result { - let year = parse_year(from).context(E::FailedParseFieldFrom)?; + let year = parse_year(from) + .map_err(|e| e.context("failed to parse FROM field"))?; Ok(RuleFromP { year }) } } @@ -615,7 +642,8 @@ impl FromStr for RuleToP { } else if to.starts_with("o") && "only".starts_with(to) { Ok(RuleToP::Only) } else { - let year = parse_year(to).context(E::FailedParseFieldTo)?; + let year = parse_year(to) + .map_err(|e| e.context("failed to parse TO field"))?; Ok(RuleToP::Year { year }) } } @@ -651,7 +679,7 @@ impl FromStr for RuleInP { return Ok(RuleInP { month }); } } - Err(Error::from(E::UnrecognizedMonthName)) + Err(err!("unrecognized month name: {field:?}")) } } @@ -719,7 +747,7 @@ impl FromStr for RuleOnP { // field. That gets checked at a higher level. Ok(RuleOnP::Day { day }) } else { - Err(Error::from(E::UnrecognizedDayOfMonthFormat)) + Err(err!("unrecognized format for day-of-month: {field:?}")) } } } @@ -754,7 +782,7 @@ impl FromStr for RuleAtP { fn from_str(at: &str) -> Result { if at.is_empty() { - return Err(Error::from(E::ExpectedNonEmptyAt)); + return Err(err!("empty field is not a valid AT value")); } let (span_string, suffix_string) = at.split_at(at.len() - 1); if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) { @@ -785,7 +813,7 @@ impl FromStr for RuleAtSuffixP { "w" => Ok(RuleAtSuffixP::Wall), "s" => Ok(RuleAtSuffixP::Standard), "u" | "g" | "z" => Ok(RuleAtSuffixP::Universal), - _ => Err(Error::from(E::UnrecognizedAtTimeSuffix)), + _ => Err(err!("unrecognized AT time suffix {suffix:?}")), } } } @@ -839,7 +867,7 @@ impl FromStr for RuleSaveP { fn from_str(at: &str) -> Result { if at.is_empty() { - return Err(Error::from(E::ExpectedNonEmptySave)); + return Err(err!("empty field is not a valid SAVE value")); } let (span_string, suffix_string) = at.split_at(at.len() - 1); if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) { @@ -874,7 +902,7 @@ impl FromStr for RuleSaveSuffixP { match suffix { "s" => Ok(RuleSaveSuffixP::Standard), "d" => Ok(RuleSaveSuffixP::Dst), - _ => Err(Error::from(E::UnrecognizedSaveTimeSuffix)), + _ => Err(err!("unrecognized SAVE time suffix {suffix:?}")), } } } @@ -911,13 +939,14 @@ impl FromStr for ZoneNameP { fn from_str(name: &str) -> Result { if name.is_empty() { - return Err(Error::from(E::ExpectedNonEmptyZoneName)); + return Err(err!("zone names cannot be empty")); } for component in name.split('/') { if component == "." || component == ".." { - return Err(Error::from(E::ExpectedZoneNameComponentNoDots { - component: component.into(), - })); + return Err(err!( + "component {component:?} in zone name {name:?} cannot \ + be \".\" or \"..\"", + )); } } Ok(ZoneNameP { name: name.to_string() }) @@ -1006,12 +1035,16 @@ impl FromStr for ZoneFormatP { fn from_str(format: &str) -> Result { fn check_abbrev(abbrev: &str) -> Result { if abbrev.is_empty() { - return Err(Error::from(E::ExpectedNonEmptyAbbreviation)); + return Err(err!("empty abbreviations are not allowed")); } let is_ok = |ch| matches!(ch, '+'|'-'|'0'..='9'|'A'..='Z'|'a'..='z'); if !abbrev.chars().all(is_ok) { - return Err(Error::from(E::InvalidAbbreviation)); + return Err(err!( + "abbreviation {abbrev:?} \ + contains invalid character; only \"+\", \"-\" and \ + ASCII alpha-numeric characters are allowed" + )); } Ok(abbrev.to_string()) } @@ -1065,24 +1098,28 @@ enum ZoneUntilP { impl ZoneUntilP { fn parse(fields: &[&str]) -> Result { if fields.is_empty() { - return Err(Error::from(E::ExpectedUntilYear)); + return Err(err!("expected at least a year")); } let (year_field, fields) = (fields[0], &fields[1..]); - let year = parse_year(year_field).context(E::FailedParseYear)?; + let year = parse_year(year_field) + .map_err(|e| e.context("failed to parse year"))?; if fields.is_empty() { return Ok(ZoneUntilP::Year { year }); } let (month_field, fields) = (fields[0], &fields[1..]); - let month = - month_field.parse::().context(E::FailedParseMonth)?; + let month = month_field + .parse::() + .map_err(|e| e.context("failed to parse month"))?; if fields.is_empty() { return Ok(ZoneUntilP::YearMonth { year, month }); } let (day_field, fields) = (fields[0], &fields[1..]); - let day = day_field.parse::().context(E::FailedParseDay)?; + let day = day_field + .parse::() + .map_err(|e| e.context("failed to parse day"))?; if fields.is_empty() { return Ok(ZoneUntilP::YearMonthDay { year, month, day }); } @@ -1090,9 +1127,13 @@ impl ZoneUntilP { let (duration_field, fields) = (fields[0], &fields[1..]); let duration = duration_field .parse::() - .context(E::FailedParseTimeDuration)?; + .map_err(|e| e.context("failed to parse time duration"))?; if !fields.is_empty() { - return Err(Error::from(E::ExpectedNothingAfterTime)); + return Err(err!( + "expected no more fields after time of day, \ + but found: {fields:?}", + fields = fields.join(" "), + )); } Ok(ZoneUntilP::YearMonthDayTime { year, month, day, duration }) } @@ -1159,8 +1200,10 @@ fn parse_year(year: &str) -> Result { } else { (t::Sign::N::<1>(), year) }; - let number = parse::i64(rest.as_bytes()).context(E::FailedParseYear)?; - let year = t::Year::try_new("year", number)?; + let number = parse::i64(rest.as_bytes()) + .map_err(|e| e.context("failed to parse year"))?; + let year = t::Year::new(number) + .ok_or_else(|| err!("year is out of range: {number}"))?; Ok(year * sign) } @@ -1196,28 +1239,36 @@ fn parse_span(span: &str) -> Result { let hour_len = rest.chars().take_while(|c| c.is_ascii_digit()).count(); let (hour_digits, rest) = rest.split_at(hour_len); if hour_digits.is_empty() { - return Err(Error::from(E::ExpectedTimeOneHour)); + return Err(err!( + "expected time duration to contain at least one hour digit" + )); } - let hours = - parse::i64(hour_digits.as_bytes()).context(E::FailedParseHour)?; - span = span.try_hours(hours.saturating_mul(i64::from(sign.get())))?; + let hours = parse::i64(hour_digits.as_bytes()) + .map_err(|e| e.context("failed to parse hours in time duration"))?; + span = span + .try_hours(hours.saturating_mul(i64::from(sign.get()))) + .map_err(|_| err!("duration hours '{hours:?}' is out of range"))?; if rest.is_empty() { return Ok(span); } // Now pluck out the minute component. if !rest.starts_with(":") { - return Err(Error::from(E::ExpectedColonAfterHour)); + return Err(err!("expected ':' after hours, but found {rest:?}")); } let rest = &rest[1..]; let minute_len = rest.chars().take_while(|c| c.is_ascii_digit()).count(); let (minute_digits, rest) = rest.split_at(minute_len); if minute_digits.is_empty() { - return Err(Error::from(E::ExpectedMinuteAfterHours)); + return Err(err!( + "expected minute digits after 'HH:', but found {rest:?} instead" + )); } - let minutes = - parse::i64(minute_digits.as_bytes()).context(E::FailedParseMinute)?; - let minutes_ranged = t::Minute::try_new("minutes", minutes)?; + let minutes = parse::i64(minute_digits.as_bytes()) + .map_err(|e| e.context("failed to parse minutes in time duration"))?; + let minutes_ranged = t::Minute::new(minutes).ok_or_else(|| { + err!("duration minutes '{minutes:?}' is out of range") + })?; span = span.minutes_ranged((minutes_ranged * sign).rinto()); if rest.is_empty() { return Ok(span); @@ -1225,17 +1276,21 @@ fn parse_span(span: &str) -> Result { // Now pluck out the second component. if !rest.starts_with(":") { - return Err(Error::from(E::ExpectedColonAfterMinute)); + return Err(err!("expected ':' after minutes, but found {rest:?}")); } let rest = &rest[1..]; let second_len = rest.chars().take_while(|c| c.is_ascii_digit()).count(); let (second_digits, rest) = rest.split_at(second_len); if second_digits.is_empty() { - return Err(Error::from(E::ExpectedSecondAfterMinutes)); + return Err(err!( + "expected second digits after 'MM:', but found {rest:?} instead" + )); } - let seconds = - parse::i64(second_digits.as_bytes()).context(E::FailedParseSecond)?; - let seconds_ranged = t::Second::try_new("seconds", seconds)?; + let seconds = parse::i64(second_digits.as_bytes()) + .map_err(|e| e.context("failed to parse seconds in time duration"))?; + let seconds_ranged = t::Second::new(seconds).ok_or_else(|| { + err!("duration seconds '{seconds:?}' is out of range") + })?; span = span.seconds_ranged((seconds_ranged * sign).rinto()); if rest.is_empty() { return Ok(span); @@ -1243,24 +1298,33 @@ fn parse_span(span: &str) -> Result { // Now look for the fractional nanosecond component. if !rest.starts_with(".") { - return Err(Error::from(E::ExpectedDotAfterSeconds)); + return Err(err!("expected '.' after seconds, but found {rest:?}")); } let rest = &rest[1..]; let nanosecond_len = rest.chars().take_while(|c| c.is_ascii_digit()).count(); let (nanosecond_digits, rest) = rest.split_at(nanosecond_len); if nanosecond_digits.is_empty() { - return Err(Error::from(E::ExpectedNanosecondDigits)); + return Err(err!( + "expected nanosecond digits after 'SS.', \ + but found {rest:?} instead" + )); } - let nanoseconds = parse::fraction(nanosecond_digits.as_bytes()) - .context(E::FailedParseNanosecond)?; - let nanoseconds_ranged = - t::FractionalNanosecond::try_new("nanoseconds", nanoseconds)?; + let nanoseconds = + parse::fraction(nanosecond_digits.as_bytes()).map_err(|e| { + e.context("failed to parse nanoseconds in time duration") + })?; + let nanoseconds_ranged = t::FractionalNanosecond::new(nanoseconds) + .ok_or_else(|| { + err!("duration nanoseconds '{nanoseconds:?}' is out of range") + })?; span = span.nanoseconds_ranged((nanoseconds_ranged * sign).rinto()); // We should have consumed everything at this point. if !rest.is_empty() { - return Err(Error::from(E::UnrecognizedTrailingTimeDuration)); + return Err(err!( + "found unrecognized trailing {rest:?} in time duration" + )); } span.rebalance(Unit::Hour) } @@ -1270,8 +1334,10 @@ fn parse_span(span: &str) -> Result { /// This checks that the day is in the range 1-31, but otherwise doesn't /// check that it is valid for a particular month. fn parse_day(string: &str) -> Result { - let number = parse::i64(string.as_bytes()).context(E::FailedParseDay)?; - let day = t::Day::try_new("day", number)?; + let number = parse::i64(string.as_bytes()) + .map_err(|e| e.context("failed to parse number for day"))?; + let day = t::Day::new(number) + .ok_or_else(|| err!("{number} is not a valid day"))?; Ok(day) } @@ -1291,7 +1357,7 @@ fn parse_weekday(string: &str) -> Result { return Ok(weekday); } } - Err(Error::from(E::UnrecognizedDayOfWeek)) + Err(err!("unrecognized day of the week: {string:?}")) } /// A parser that emits lines as sequences of fields. @@ -1327,7 +1393,7 @@ impl<'a> FieldParser<'a> { /// This returns an error if the given bytes are not valid UTF-8. fn from_bytes(src: &'a [u8]) -> Result { let src = core::str::from_utf8(src) - .map_err(|_| Error::from(E::InvalidUtf8))?; + .map_err(|e| err!("invalid UTF-8: {e}"))?; Ok(FieldParser::new(src)) } @@ -1343,10 +1409,12 @@ impl<'a> FieldParser<'a> { self.fields.clear(); loop { let Some(mut line) = self.lines.next() else { return Ok(false) }; - self.line_number = - self.line_number.checked_add(1).ok_or(E::LineOverflow)?; + self.line_number = self + .line_number + .checked_add(1) + .ok_or_else(|| err!("line count overflowed"))?; parse_fields(&line, &mut self.fields) - .context(E::Line { number: self.line_number })?; + .with_context(|| err!("line {}", self.line_number))?; if self.fields.is_empty() { continue; } @@ -1384,6 +1452,13 @@ fn parse_fields<'a>( matches!(ch, ' ' | '\x0C' | '\n' | '\r' | '\t' | '\x0B') } + // `man zic` says that the max line length including the line + // terminator is 2048. The `core::str::Lines` iterator doesn't include + // the terminator, so we subtract 1 to account for that. Note that this + // could potentially allow one extra byte in the case of a \r\n line + // terminator, but this seems fine. + const MAX_LINE_LEN: usize = 2047; + // The different possible states of the field parser below. enum State { Whitespace, @@ -1394,11 +1469,15 @@ fn parse_fields<'a>( fields.clear(); if line.len() > MAX_LINE_LEN { - return Err(Error::from(E::LineMaxLength)); + return Err(err!( + "line with length {} exceeds \ + max length of {MAX_LINE_LEN}", + line.len() + )); } // Do a quick scan for a NUL terminator. They are illegal in all cases. if line.contains('\x00') { - return Err(Error::from(E::LineNul)); + return Err(err!("found line with NUL byte, which isn't allowed")); } // The current state of the parser. We start at whitespace, since it also // means "before a field." @@ -1443,8 +1522,9 @@ fn parse_fields<'a>( } State::AfterQuote => { if !is_space(ch) { - return Err(Error::from( - E::ExpectedWhitespaceAfterQuotedField, + return Err(err!( + "expected whitespace after quoted field, \ + but found {ch:?} instead", )); } State::Whitespace @@ -1457,7 +1537,7 @@ fn parse_fields<'a>( fields.push(&line[start..]); } State::InQuote => { - return Err(Error::from(E::ExpectedCloseQuote)); + return Err(err!("found unclosed quote")); } } Ok(()) diff --git a/src/util/c.rs b/src/util/c.rs index bd054ab..468e5c0 100644 --- a/src/util/c.rs +++ b/src/util/c.rs @@ -49,7 +49,7 @@ impl Sign { impl core::fmt::Display for Sign { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { if self.is_negative() { - f.write_str("-") + write!(f, "-") } else { Ok(()) } diff --git a/src/util/cache.rs b/src/util/cache.rs index a6c1eee..bf1e7bf 100644 --- a/src/util/cache.rs +++ b/src/util/cache.rs @@ -36,13 +36,15 @@ impl Expiration { impl core::fmt::Display for Expiration { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - let maybe_duration = self.0.and_then(|instant| { - crate::now::monotonic_time() - .and_then(|now| instant.checked_duration_since(now)) - }); - match maybe_duration { - None => f.write_str("expired"), - Some(duration) => core::fmt::Debug::fmt(&duration, f), - } + let Some(instant) = self.0 else { + return write!(f, "expired"); + }; + let Some(now) = crate::now::monotonic_time() else { + return write!(f, "expired"); + }; + let Some(duration) = instant.checked_duration_since(now) else { + return write!(f, "expired"); + }; + write!(f, "{duration:?}") } } diff --git a/src/util/escape.rs b/src/util/escape.rs index 8aec114..488785d 100644 --- a/src/util/escape.rs +++ b/src/util/escape.rs @@ -4,121 +4,8 @@ Provides convenience routines for escaping raw bytes. This was copied from `regex-automata` with a few light edits. */ -use super::utf8; - -/// Provides a convenient `Debug` implementation for a `u8`. -/// -/// The `Debug` impl treats the byte as an ASCII, and emits a human -/// readable representation of it. If the byte isn't ASCII, then it's -/// emitted as a hex escape sequence. -#[derive(Clone, Copy)] -pub(crate) struct Byte(pub u8); - -impl core::fmt::Display for Byte { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - if self.0 == b' ' { - return f.write_str(" "); - } - // 10 bytes is enough for any output from ascii::escape_default. - let mut bytes = [0u8; 10]; - let mut len = 0; - for (i, mut b) in core::ascii::escape_default(self.0).enumerate() { - // capitalize \xab to \xAB - if i >= 2 && b'a' <= b && b <= b'f' { - b -= 32; - } - bytes[len] = b; - len += 1; - } - f.write_str(core::str::from_utf8(&bytes[..len]).unwrap()) - } -} - -impl core::fmt::Debug for Byte { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("\"")?; - core::fmt::Display::fmt(self, f)?; - f.write_str("\"")?; - Ok(()) - } -} - -/// Provides a convenient `Debug` implementation for `&[u8]`. -/// -/// This generally works best when the bytes are presumed to be mostly -/// UTF-8, but will work for anything. For any bytes that aren't UTF-8, -/// they are emitted as hex escape sequences. -#[derive(Clone, Copy)] -pub(crate) struct Bytes<'a>(pub &'a [u8]); - -impl<'a> core::fmt::Display for Bytes<'a> { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - // This is a sad re-implementation of a similar impl found in bstr. - let mut bytes = self.0; - while let Some(result) = utf8::decode(bytes) { - let ch = match result { - Ok(ch) => ch, - Err(err) => { - // The decode API guarantees `errant_bytes` is non-empty. - write!(f, r"\x{:02x}", err.as_slice()[0])?; - bytes = &bytes[1..]; - continue; - } - }; - bytes = &bytes[ch.len_utf8()..]; - match ch { - '\0' => f.write_str(r"\0")?, - '\x01'..='\x7f' => { - core::fmt::Display::fmt(&(ch as u8).escape_ascii(), f)?; - } - _ => { - core::fmt::Display::fmt(&ch.escape_debug(), f)?; - } - } - } - Ok(()) - } -} - -impl<'a> core::fmt::Debug for Bytes<'a> { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("\"")?; - core::fmt::Display::fmt(self, f)?; - f.write_str("\"")?; - Ok(()) - } -} - -/// A helper for repeating a single byte utilizing `Byte`. -/// -/// This is limited to repeating a byte up to `u8::MAX` times in order -/// to reduce its size overhead. And in practice, Jiff just doesn't -/// need more than this (at time of writing, 2025-11-29). -pub(crate) struct RepeatByte { - pub(crate) byte: u8, - pub(crate) count: u8, -} - -impl core::fmt::Display for RepeatByte { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - for _ in 0..self.count { - core::fmt::Display::fmt(&Byte(self.byte), f)?; - } - Ok(()) - } -} - -impl core::fmt::Debug for RepeatByte { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("\"")?; - core::fmt::Display::fmt(self, f)?; - f.write_str("\"")?; - Ok(()) - } -} +// These were originally defined here, but they got moved to +// shared since they're needed there. We re-export them here +// because this is really where they should live, but they're +// in shared because `jiff-tzdb-static` needs it. +pub(crate) use crate::shared::util::escape::{Byte, Bytes}; diff --git a/src/util/parse.rs b/src/util/parse.rs index a95d7ff..fced840 100644 --- a/src/util/parse.rs +++ b/src/util/parse.rs @@ -1,4 +1,7 @@ -use crate::error::util::{ParseFractionError, ParseIntError}; +use crate::{ + error::{err, Error}, + util::escape::{Byte, Bytes}, +}; /// Parses an `i64` number from the beginning to the end of the given slice of /// ASCII digit characters. @@ -10,20 +13,38 @@ use crate::error::util::{ParseFractionError, ParseIntError}; /// integers, and because a higher level routine might want to parse the sign /// and then apply it to the result of this routine.) #[cfg_attr(feature = "perf-inline", inline(always))] -pub(crate) fn i64(bytes: &[u8]) -> Result { +pub(crate) fn i64(bytes: &[u8]) -> Result { if bytes.is_empty() { - return Err(ParseIntError::NoDigitsFound); + return Err(err!("invalid number, no digits found")); } let mut n: i64 = 0; for &byte in bytes { - if !(b'0' <= byte && byte <= b'9') { - return Err(ParseIntError::InvalidDigit(byte)); - } - let digit = i64::from(byte - b'0'); - n = n - .checked_mul(10) - .and_then(|n| n.checked_add(digit)) - .ok_or(ParseIntError::TooBig)?; + let digit = match byte.checked_sub(b'0') { + None => { + return Err(err!( + "invalid digit, expected 0-9 but got {}", + Byte(byte), + )); + } + Some(digit) if digit > 9 => { + return Err(err!( + "invalid digit, expected 0-9 but got {}", + Byte(byte), + )) + } + Some(digit) => { + debug_assert!((0..=9).contains(&digit)); + i64::from(digit) + } + }; + n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else( + || { + err!( + "number '{}' too big to parse into 64-bit integer", + Bytes(bytes), + ) + }, + )?; } Ok(n) } @@ -44,9 +65,7 @@ pub(crate) fn i64(bytes: &[u8]) -> Result { /// /// When the parsed integer cannot fit into a `u64`. #[cfg_attr(feature = "perf-inline", inline(always))] -pub(crate) fn u64_prefix( - bytes: &[u8], -) -> Result<(Option, &[u8]), ParseIntError> { +pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option, &[u8]), Error> { // Discovered via `u64::MAX.to_string().len()`. const MAX_U64_DIGITS: usize = 20; @@ -60,10 +79,15 @@ pub(crate) fn u64_prefix( digit_count += 1; // OK because we confirmed `byte` is an ASCII digit. let digit = u64::from(byte - b'0'); - n = n - .checked_mul(10) - .and_then(|n| n.checked_add(digit)) - .ok_or(ParseIntError::TooBig)?; + n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else( + #[inline(never)] + || { + err!( + "number `{}` too big to parse into 64-bit integer", + Bytes(&bytes[..digit_count]), + ) + }, + )?; } if digit_count == 0 { return Ok((None, bytes)); @@ -81,33 +105,54 @@ pub(crate) fn u64_prefix( /// /// If any byte in the given slice is not `[0-9]`, then this returns an error. /// Notably, this routine does not permit parsing a negative integer. -pub(crate) fn fraction(bytes: &[u8]) -> Result { +pub(crate) fn fraction(bytes: &[u8]) -> Result { + const MAX_PRECISION: usize = 9; + if bytes.is_empty() { - return Err(ParseFractionError::NoDigitsFound); - } else if bytes.len() > ParseFractionError::MAX_PRECISION { - return Err(ParseFractionError::TooManyDigits); + return Err(err!("invalid fraction, no digits found")); + } else if bytes.len() > MAX_PRECISION { + return Err(err!( + "invalid fraction, too many digits \ + (at most {MAX_PRECISION} are allowed" + )); } let mut n: u32 = 0; for &byte in bytes { let digit = match byte.checked_sub(b'0') { None => { - return Err(ParseFractionError::InvalidDigit(byte)); + return Err(err!( + "invalid fractional digit, expected 0-9 but got {}", + Byte(byte), + )); } Some(digit) if digit > 9 => { - return Err(ParseFractionError::InvalidDigit(byte)); + return Err(err!( + "invalid fractional digit, expected 0-9 but got {}", + Byte(byte), + )) } Some(digit) => { debug_assert!((0..=9).contains(&digit)); u32::from(digit) } }; - n = n - .checked_mul(10) - .and_then(|n| n.checked_add(digit)) - .ok_or_else(|| ParseFractionError::TooBig)?; + n = n.checked_mul(10).and_then(|n| n.checked_add(digit)).ok_or_else( + || { + err!( + "fractional '{}' too big to parse into 64-bit integer", + Bytes(bytes), + ) + }, + )?; } - for _ in bytes.len()..ParseFractionError::MAX_PRECISION { - n = n.checked_mul(10).ok_or_else(|| ParseFractionError::TooBig)?; + for _ in bytes.len()..MAX_PRECISION { + n = n.checked_mul(10).ok_or_else(|| { + err!( + "fractional '{}' too big to parse into 64-bit integer \ + (too much precision supported)", + Bytes(bytes) + ) + })?; } Ok(n) } @@ -116,17 +161,15 @@ pub(crate) fn fraction(bytes: &[u8]) -> Result { /// /// This is effectively `OsStr::to_str`, but with a slightly better error /// message. -#[cfg(any(feature = "tz-system", feature = "tzdb-zoneinfo"))] -pub(crate) fn os_str_utf8<'o, O>( - os_str: &'o O, -) -> Result<&'o str, crate::error::util::OsStrUtf8Error> +#[cfg(feature = "tzdb-zoneinfo")] +pub(crate) fn os_str_utf8<'o, O>(os_str: &'o O) -> Result<&'o str, Error> where O: ?Sized + AsRef, { let os_str = os_str.as_ref(); os_str .to_str() - .ok_or_else(|| crate::error::util::OsStrUtf8Error::from(os_str)) + .ok_or_else(|| err!("environment value {os_str:?} is not valid UTF-8")) } /// Parses an `OsStr` into a `&str` when `&[u8]` isn't easily available. @@ -135,9 +178,7 @@ where /// be a zero-cost conversion on Unix platforms to `&[u8]`. On Windows, this /// will do UTF-8 validation and return an error if it's invalid UTF-8. #[cfg(feature = "tz-system")] -pub(crate) fn os_str_bytes<'o, O>( - os_str: &'o O, -) -> Result<&'o [u8], crate::error::util::OsStrUtf8Error> +pub(crate) fn os_str_bytes<'o, O>(os_str: &'o O) -> Result<&'o [u8], Error> where O: ?Sized + AsRef, { @@ -149,13 +190,16 @@ where } #[cfg(not(unix))] { + let string = os_str.to_str().ok_or_else(|| { + err!("environment value {os_str:?} is not valid UTF-8") + })?; // It is suspect that we're doing UTF-8 validation and then throwing // away the fact that we did UTF-8 validation. So this could lead // to an extra UTF-8 check if the caller ultimately needs UTF-8. If // that's important, we can add a new API that returns a `&str`. But it // probably won't matter because an `OsStr` in this crate is usually // just an environment variable. - Ok(os_str_utf8(os_str)?.as_bytes()) + Ok(string.as_bytes()) } } diff --git a/src/util/rangeint.rs b/src/util/rangeint.rs index 82bf3a4..a20ec34 100644 --- a/src/util/rangeint.rs +++ b/src/util/rangeint.rs @@ -164,15 +164,16 @@ macro_rules! define_ranged { val: impl Into, ) -> Result { let val = val.into(); - <$repr>::try_from(val).ok().and_then(Self::new).ok_or_else( - || { - Error::range( - what, - val, - Self::MIN_REPR, - Self::MAX_REPR, - ) - }) + #[allow(irrefutable_let_patterns)] + let Ok(val) = <$repr>::try_from(val) else { + return Err(Error::range( + what, + val, + Self::MIN_REPR, + Self::MAX_REPR, + )); + }; + Self::new(val).ok_or_else(|| Self::error(what, val)) } #[inline] @@ -181,15 +182,16 @@ macro_rules! define_ranged { val: impl Into, ) -> Result { let val = val.into(); - <$repr>::try_from(val).ok().and_then(Self::new).ok_or_else( - || { - Error::range( - what, - val, - Self::MIN_REPR, - Self::MAX_REPR, - ) - }) + #[allow(irrefutable_let_patterns)] + let Ok(val) = <$repr>::try_from(val) else { + return Err(Error::range( + what, + val, + Self::MIN_REPR, + Self::MAX_REPR, + )); + }; + Self::new(val).ok_or_else(|| Self::error(what, val)) } #[inline] @@ -377,7 +379,7 @@ macro_rules! define_ranged { /// dependent bounds. For example, when the day of the month is out /// of bounds. The maximum value can vary based on the month (and /// year). - #[inline(never)] + #[inline] pub(crate) fn to_error_with_bounds( self, what: &'static str, @@ -2093,7 +2095,7 @@ macro_rules! define_ranged { // its debug repr which should show some nice output. match self.checked_add(Self::N::<0>()) { Some(val) => core::fmt::Display::fmt(&val.get(), f), - None => core::fmt::Debug::fmt(self, f), + None => write!(f, "{:?}", self), } } } diff --git a/src/util/round/increment.rs b/src/util/round/increment.rs index 7f174ac..a66e2d2 100644 --- a/src/util/round/increment.rs +++ b/src/util/round/increment.rs @@ -9,7 +9,7 @@ for time units must divide evenly into 1 unit of the next highest unit. */ use crate::{ - error::{util::RoundingIncrementError as E, Error, ErrorContext}, + error::{err, Error}, util::{ rangeint::RFrom, t::{self, Constant, C}, @@ -45,7 +45,7 @@ pub(crate) fn for_span( // bounds of i64 and not i128. Ok(t::NoUnits128::rfrom(t::NoUnits::new_unchecked(increment))) } else { - get_with_limit(unit, increment, LIMIT).context(E::ForSpan) + get_with_limit(unit, increment, "span", LIMIT) } } @@ -67,7 +67,7 @@ pub(crate) fn for_datetime( t::HOURS_PER_CIVIL_DAY, Constant(2), ]; - get_with_limit(unit, increment, LIMIT).context(E::ForDateTime) + get_with_limit(unit, increment, "datetime", LIMIT) } /// Validates the given rounding increment for the given unit. @@ -87,7 +87,7 @@ pub(crate) fn for_time( t::MINUTES_PER_HOUR, t::HOURS_PER_CIVIL_DAY, ]; - get_with_limit(unit, increment, LIMIT).context(E::ForTime) + get_with_limit(unit, increment, "time", LIMIT) } /// Validates the given rounding increment for the given unit. @@ -107,25 +107,38 @@ pub(crate) fn for_timestamp( t::MINUTES_PER_CIVIL_DAY, t::HOURS_PER_CIVIL_DAY, ]; - get_with_max(unit, increment, MAX).context(E::ForTimestamp) + get_with_max(unit, increment, "timestamp", MAX) } fn get_with_limit( unit: Unit, increment: i64, + what: &'static str, limit: &[t::Constant], -) -> Result { +) -> Result { // OK because `NoUnits` specifically allows any `i64` value. let increment = t::NoUnits::new_unchecked(increment); if increment <= C(0) { - return Err(E::GreaterThanZero { unit }); + return Err(err!( + "rounding increment {increment} for {unit} must be \ + greater than zero", + unit = unit.plural(), + )); } let Some(must_divide) = limit.get(unit as usize) else { - return Err(E::Unsupported { unit }); + return Err(err!( + "{what} rounding does not support {unit}", + unit = unit.plural() + )); }; let must_divide = t::NoUnits::rfrom(*must_divide); if increment >= must_divide || must_divide % increment != C(0) { - Err(E::InvalidDivide { unit, must_divide: must_divide.get() }) + Err(err!( + "increment {increment} for rounding {what} to {unit} \ + must be 1) less than {must_divide}, 2) divide into \ + it evenly and 3) greater than zero", + unit = unit.plural(), + )) } else { Ok(t::NoUnits128::rfrom(increment)) } @@ -134,19 +147,32 @@ fn get_with_limit( fn get_with_max( unit: Unit, increment: i64, + what: &'static str, max: &[t::Constant], -) -> Result { +) -> Result { // OK because `NoUnits` specifically allows any `i64` value. let increment = t::NoUnits::new_unchecked(increment); if increment <= C(0) { - return Err(E::GreaterThanZero { unit }); + return Err(err!( + "rounding increment {increment} for {unit} must be \ + greater than zero", + unit = unit.plural(), + )); } let Some(must_divide) = max.get(unit as usize) else { - return Err(E::Unsupported { unit }); + return Err(err!( + "{what} rounding does not support {unit}", + unit = unit.plural() + )); }; let must_divide = t::NoUnits::rfrom(*must_divide); if increment > must_divide || must_divide % increment != C(0) { - Err(E::InvalidDivide { unit, must_divide: must_divide.get() }) + Err(err!( + "increment {increment} for rounding {what} to {unit} \ + must be 1) less than or equal to {must_divide}, \ + 2) divide into it evenly and 3) greater than zero", + unit = unit.plural(), + )) } else { Ok(t::NoUnits128::rfrom(increment)) } diff --git a/src/util/utf8.rs b/src/util/utf8.rs index bd5b462..0ea226c 100644 --- a/src/util/utf8.rs +++ b/src/util/utf8.rs @@ -1,59 +1,5 @@ use core::cmp::Ordering; -/// Represents an invalid UTF-8 sequence. -/// -/// This is an error returned by `decode`. It is guaranteed to -/// contain 1, 2 or 3 bytes. -pub(crate) struct Utf8Error { - bytes: [u8; 3], - len: u8, -} - -impl Utf8Error { - #[cold] - #[inline(never)] - fn new(original_bytes: &[u8], err: core::str::Utf8Error) -> Utf8Error { - let len = err.error_len().unwrap_or_else(|| original_bytes.len()); - // OK because the biggest invalid UTF-8 - // sequence possible is 3. - debug_assert!(1 <= len && len <= 3); - let mut bytes = [0; 3]; - bytes[..len].copy_from_slice(&original_bytes[..len]); - Utf8Error { - bytes, - // OK because the biggest invalid UTF-8 - // sequence possible is 3. - len: u8::try_from(len).unwrap(), - } - } - - /// Returns the slice of invalid UTF-8 bytes. - /// - /// The slice returned is guaranteed to have length equivalent - /// to `Utf8Error::len`. - pub(crate) fn as_slice(&self) -> &[u8] { - &self.bytes[..self.len()] - } - - /// Returns the length of the invalid UTF-8 sequence found. - /// - /// This is guaranteed to be 1, 2 or 3. - pub(crate) fn len(&self) -> usize { - usize::from(self.len) - } -} - -impl core::fmt::Display for Utf8Error { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!( - f, - "found invalid UTF-8 byte {errant_bytes:?} in format \ - string (format strings must be valid UTF-8)", - errant_bytes = crate::util::escape::Bytes(self.as_slice()), - ) - } -} - /// Decodes the next UTF-8 encoded codepoint from the given byte slice. /// /// If no valid encoding of a codepoint exists at the beginning of the @@ -66,28 +12,11 @@ impl core::fmt::Display for Utf8Error { /// /// This never panics. /// -/// *WARNING*: This is not designed for performance. If you're looking for -/// a fast UTF-8 decoder, this is not it. If you feel like you need one in -/// this crate, then please file an issue and discuss your use case. -pub(crate) fn decode(bytes: &[u8]) -> Option> { - if bytes.is_empty() { - return None; - } - let string = match core::str::from_utf8(&bytes[..bytes.len().min(4)]) { - Ok(s) => s, - Err(ref err) if err.valid_up_to() > 0 => { - // OK because we just verified we have at least some - // valid UTF-8. - core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap() - } - // In this case, we want to return 1-3 bytes that make up a prefix of - // a potentially valid codepoint. - Err(err) => return Some(Err(Utf8Error::new(bytes, err))), - }; - // OK because we guaranteed above that `string` - // must be non-empty. And thus, `str::chars` must - // yield at least one Unicode scalar value. - Some(Ok(string.chars().next().unwrap())) +/// *WARNING*: This is not designed for performance. If you're looking for a +/// fast UTF-8 decoder, this is not it. If you feel like you need one in this +/// crate, then please file an issue and discuss your use case. +pub(crate) fn decode(bytes: &[u8]) -> Option> { + crate::shared::util::utf8::decode(bytes) } /// Like std's `eq_ignore_ascii_case`, but returns a full `Ordering`. diff --git a/src/zoned.rs b/src/zoned.rs index 60c0c0d..8f37e78 100644 --- a/src/zoned.rs +++ b/src/zoned.rs @@ -6,7 +6,7 @@ use crate::{ Weekday, }, duration::{Duration, SDuration}, - error::{zoned::Error as E, Error, ErrorContext}, + error::{err, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -2216,20 +2216,41 @@ impl Zoned { .timestamp() .checked_add(span) .map(|ts| ts.to_zoned(self.time_zone().clone())) - .context(E::AddTimestamp); + .with_context(|| { + err!( + "failed to add span {span} to timestamp {timestamp} \ + from zoned datetime {zoned}", + timestamp = self.timestamp(), + zoned = self, + ) + }); } let span_time = span.only_time(); - let dt = self - .datetime() - .checked_add(span_calendar) - .context(E::AddDateTime)?; + let dt = + self.datetime().checked_add(span_calendar).with_context(|| { + err!( + "failed to add span {span_calendar} to datetime {dt} \ + from zoned datetime {zoned}", + dt = self.datetime(), + zoned = self, + ) + })?; let tz = self.time_zone(); - let mut ts = tz - .to_ambiguous_timestamp(dt) - .compatible() - .context(E::ConvertDateTimeToTimestamp)?; - ts = ts.checked_add(span_time).context(E::AddTimestamp)?; + let mut ts = + tz.to_ambiguous_timestamp(dt).compatible().with_context(|| { + err!( + "failed to convert civil datetime {dt} to timestamp \ + with time zone {tz}", + tz = self.time_zone().diagnostic_name(), + ) + })?; + ts = ts.checked_add(span_time).with_context(|| { + err!( + "failed to add span {span_time} to timestamp {ts} \ + (which was created from {dt})" + ) + })?; Ok(ts.to_zoned(tz.clone())) } @@ -4306,7 +4327,13 @@ impl<'a> ZonedDifference<'a> { return zdt1.timestamp().until((largest, zdt2.timestamp())); } if zdt1.time_zone() != zdt2.time_zone() { - return Err(Error::from(E::MismatchTimeZoneUntil { largest })); + return Err(err!( + "computing the span between zoned datetimes, with \ + {largest} units, requires that the time zones are \ + equivalent, but {zdt1} and {zdt2} have distinct \ + time zones", + largest = largest.singular(), + )); } let tz = zdt1.time_zone(); @@ -4320,27 +4347,43 @@ impl<'a> ZonedDifference<'a> { let mut mid = dt2 .date() .checked_add(Span::new().days_ranged(day_correct * -sign)) - .context(E::AddDays)? + .with_context(|| { + err!( + "failed to add {days} days to date in {dt2}", + days = day_correct * -sign, + ) + })? .to_datetime(dt1.time()); - let mut zmid: Zoned = mid - .to_zoned(tz.clone()) - .context(E::ConvertIntermediateDatetime)?; + let mut zmid: Zoned = mid.to_zoned(tz.clone()).with_context(|| { + err!( + "failed to convert intermediate datetime {mid} \ + to zoned timestamp in time zone {tz}", + tz = tz.diagnostic_name(), + ) + })?; if t::sign(zdt2, &zmid) == -sign { if sign == C(-1) { - // FIXME panic!("this should be an error"); } day_correct += C(1); mid = dt2 .date() .checked_add(Span::new().days_ranged(day_correct * -sign)) - .context(E::AddDays)? + .with_context(|| { + err!( + "failed to add {days} days to date in {dt2}", + days = day_correct * -sign, + ) + })? .to_datetime(dt1.time()); - zmid = mid - .to_zoned(tz.clone()) - .context(E::ConvertIntermediateDatetime)?; + zmid = mid.to_zoned(tz.clone()).with_context(|| { + err!( + "failed to convert intermediate datetime {mid} \ + to zoned timestamp in time zone {tz}", + tz = tz.diagnostic_name(), + ) + })?; if t::sign(zdt2, &zmid) == -sign { - // FIXME panic!("this should be an error too"); } } @@ -4592,18 +4635,32 @@ impl ZonedRound { // and a &TimeZone. Fixing just this should just be some minor annoying // work. The grander refactor is something like an `Unzoned` type, but // I'm not sure that's really worth it. ---AG - let start = zdt.start_of_day().context(E::FailedStartOfDay)?; + let start = zdt.start_of_day().with_context(move || { + err!("failed to find start of day for {zdt}") + })?; let end = start .checked_add(Span::new().days_ranged(C(1).rinto())) - .context(E::FailedLengthOfDay)?; + .with_context(|| { + err!("failed to add 1 day to {start} to find length of day") + })?; let span = start .timestamp() .until((Unit::Nanosecond, end.timestamp())) - .context(E::FailedSpanNanoseconds)?; + .with_context(|| { + err!( + "failed to compute span in nanoseconds \ + from {start} until {end}" + ) + })?; let nanos = span.get_nanoseconds_ranged(); let day_length = ZonedDayNanoseconds::try_rfrom("nanoseconds-per-zoned-day", nanos) - .context(E::FailedSpanNanoseconds)?; + .with_context(|| { + err!( + "failed to convert span between {start} until {end} \ + to nanoseconds", + ) + })?; let progress = zdt.timestamp().as_nanosecond_ranged() - start.timestamp().as_nanosecond_ranged(); let rounded = self.round.get_mode().round(progress, day_length); @@ -6021,21 +6078,21 @@ mod tests { insta::assert_snapshot!( zdt.round(Unit::Year).unwrap_err(), - @"failed rounding datetime: rounding to years is not supported" + @"datetime rounding does not support years" ); insta::assert_snapshot!( zdt.round(Unit::Month).unwrap_err(), - @"failed rounding datetime: rounding to months is not supported" + @"datetime rounding does not support months" ); insta::assert_snapshot!( zdt.round(Unit::Week).unwrap_err(), - @"failed rounding datetime: rounding to weeks is not supported" + @"datetime rounding does not support weeks" ); let options = ZonedRound::new().smallest(Unit::Day).increment(2); insta::assert_snapshot!( zdt.round(options).unwrap_err(), - @"failed rounding datetime: increment for rounding to days must be 1) less than 2, 2) divide into it evenly and 3) greater than zero" + @"increment 2 for rounding datetime to days must be 1) less than 2, 2) divide into it evenly and 3) greater than zero" ); } @@ -6067,12 +6124,12 @@ mod tests { insta::assert_snapshot!( "1970-06-01T00:00:00-00:44:40[Africa/Monrovia]".parse::().unwrap_err(), - @"datetime could not resolve to a timestamp since `reject` conflict resolution was chosen, and because datetime has offset `-00:44:40`, but the time zone `Africa/Monrovia` for the given datetime unambiguously has offset `-00:44:30`", + @r#"parsing "1970-06-01T00:00:00-00:44:40[Africa/Monrovia]" failed: datetime 1970-06-01T00:00:00 could not resolve to a timestamp since 'reject' conflict resolution was chosen, and because datetime has offset -00:44:40, but the time zone Africa/Monrovia for the given datetime unambiguously has offset -00:44:30"#, ); insta::assert_snapshot!( "1970-06-01T00:00:00-00:45:00[Africa/Monrovia]".parse::().unwrap_err(), - @"datetime could not resolve to a timestamp since `reject` conflict resolution was chosen, and because datetime has offset `-00:45`, but the time zone `Africa/Monrovia` for the given datetime unambiguously has offset `-00:44:30`", + @r#"parsing "1970-06-01T00:00:00-00:45:00[Africa/Monrovia]" failed: datetime 1970-06-01T00:00:00 could not resolve to a timestamp since 'reject' conflict resolution was chosen, and because datetime has offset -00:45, but the time zone Africa/Monrovia for the given datetime unambiguously has offset -00:44:30"#, ); } diff --git a/testprograms/invalid-tz-environment-variable/main.rs b/testprograms/invalid-tz-environment-variable/main.rs index 4dc6eaf..75544d2 100644 --- a/testprograms/invalid-tz-environment-variable/main.rs +++ b/testprograms/invalid-tz-environment-variable/main.rs @@ -7,9 +7,9 @@ fn main() { } assert_eq!( jiff::tz::TimeZone::try_system().unwrap_err().to_string(), - "`TZ` environment variable set, but failed to read value: \ - failed to read `TZ` environment variable value as a TZif file \ - after attempting (and failing) a tzdb lookup for that same value", + "TZ environment variable set, but failed to read value: \ + failed to read TZ=\"WAT5HUH\" as a TZif file \ + after attempting a tzdb lookup for `WAT5HUH`", ); // SAFETY: This is a single threaded program. @@ -18,9 +18,9 @@ fn main() { } assert_eq!( jiff::tz::TimeZone::try_system().unwrap_err().to_string(), - "`TZ` environment variable set, but failed to read value: \ - failed to read `TZ` environment variable value as a TZif file \ - after attempting (and failing) a tzdb lookup for that same value", + "TZ environment variable set, but failed to read value: \ + failed to read TZ=\"/usr/share/zoneinfo/WAT5HUH\" as a TZif file \ + after attempting a tzdb lookup for `WAT5HUH`", ); unsafe { diff --git a/tests/tc39_262/span/add.rs b/tests/tc39_262/span/add.rs index 95c8093..187895f 100644 --- a/tests/tc39_262/span/add.rs +++ b/tests/tc39_262/span/add.rs @@ -171,7 +171,7 @@ fn no_calendar_units() -> Result { ); insta::assert_snapshot!( 1.week().checked_add(blank).unwrap_err(), - @"using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); let ok = 1.day(); diff --git a/tests/tc39_262/span/round.rs b/tests/tc39_262/span/round.rs index 53807c2..24e7651 100644 --- a/tests/tc39_262/span/round.rs +++ b/tests/tc39_262/span/round.rs @@ -87,7 +87,7 @@ fn calendar_possibly_required() -> Result { let sp = 5.weeks(); insta::assert_snapshot!( sp.round(Unit::Hour).unwrap_err(), - @"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); span_eq!(sp.round(relative_years)?, 1.month().days(4)); span_eq!(sp.round(relative_months)?, 1.month().days(4)); @@ -98,7 +98,7 @@ fn calendar_possibly_required() -> Result { // We differ from Temporal in that we require opt-in for 24-hour days. insta::assert_snapshot!( sp.round(Unit::Hour).unwrap_err(), - @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); span_eq!(sp.round(relative_years)?, 1.month().days(11)); span_eq!(sp.round(relative_months)?, 1.month().days(11)); @@ -218,14 +218,14 @@ fn duration_out_of_range_added_to_relative() -> Result { let relative = SpanRound::new().relative(d); insta::assert_snapshot!( sp.round(relative.smallest(Unit::Year)).unwrap_err(), - @"failed to add overflowing span: parameter 'days' with value 7083333 is not in the required range of -4371587..=2932896", + @"failed to add P2000000DT170000000H to 2000-01-01T00:00:00: failed to add overflowing span, P7083333D, from adding PT170000000H to 00:00:00, to 7475-10-25: parameter 'days' with value 7083333 is not in the required range of -4371587..=2932896", ); let sp = -2_000_000.days().hours(170_000_000); let relative = SpanRound::new().relative(d); insta::assert_snapshot!( sp.round(relative.smallest(Unit::Year)).unwrap_err(), - @"failed to add overflowing span: parameter 'days' with value -7083334 is not in the required range of -4371587..=2932896", + @"failed to add -P2000000DT170000000H to 2000-01-01T00:00:00: failed to add overflowing span, -P7083334D, from adding -PT170000000H to 00:00:00, to -003476-03-09: parameter 'days' with value -7083334 is not in the required range of -4371587..=2932896", ); Ok(()) @@ -713,7 +713,7 @@ fn out_of_range_when_adjusting_rounded_days() -> Result { insta::assert_snapshot!( sp.round(options).unwrap_err(), // Kind of a brutal error message... - @"failed to add span to timestamp from zoned datetime: adding span overflowed timestamp: parameter 'span' with value 631107331200999999999 is not in the required range of -377705023201000000000..=253402207200999999999", + @"failed to add P1DT631107331200.999999999S to 1970-01-01T00:00:00+00:00[UTC]: failed to add span PT631107331200.999999999S to timestamp 1970-01-02T00:00:00Z (which was created from 1970-01-02T00:00:00): adding PT631107331200.999999999S to 1970-01-02T00:00:00Z overflowed: parameter 'span' with value 631107331200999999999 is not in the required range of -377705023201000000000..=253402207200999999999", ); Ok(()) @@ -728,7 +728,7 @@ fn out_of_range_when_converting_from_normalized_duration() -> Result { insta::assert_snapshot!( sp.round(options).unwrap_err(), // Kind of a brutal error message... - @"failed to convert rounded nanoseconds to span for largest unit set to 'nanoseconds': parameter 'nanoseconds' with value 631107417600999999999 is not in the required range of -9223372036854775807..=9223372036854775807", + @"failed to convert rounded nanoseconds 631107417600999999999 to span for largest unit as nanoseconds: parameter 'nanoseconds' with value 631107417600999999999 is not in the required range of -9223372036854775807..=9223372036854775807", ); Ok(()) @@ -759,15 +759,15 @@ fn precision_exact_in_round_duration() -> Result { fn relativeto_undefined_throw_on_calendar_units() -> Result { insta::assert_snapshot!( 1.day().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(), - @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.day().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(), - @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.week().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(), - @"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.month().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(), @@ -780,37 +780,37 @@ fn relativeto_undefined_throw_on_calendar_units() -> Result { insta::assert_snapshot!( 1.month().round(SpanRound::new().largest(Unit::Hour).days_are_24_hours()).unwrap_err(), - @"using unit 'month' in span or configuration requires that a relative reference time be given (`jiff::SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)", + @"using unit 'month' in span or configuration requires that a relative reference time be given (`SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)", ); insta::assert_snapshot!( 1.year().round(SpanRound::new().largest(Unit::Hour).days_are_24_hours()).unwrap_err(), - @"using unit 'year' in span or configuration requires that a relative reference time be given (`jiff::SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)", + @"using unit 'year' in span or configuration requires that a relative reference time be given (`SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)", ); insta::assert_snapshot!( 1.hour().round(SpanRound::new().largest(Unit::Day)).unwrap_err(), - @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.day().round(SpanRound::new().largest(Unit::Day)).unwrap_err(), - @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.week().round(SpanRound::new().largest(Unit::Day)).unwrap_err(), - @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.month().round(SpanRound::new().largest(Unit::Day)).unwrap_err(), - @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.year().round(SpanRound::new().largest(Unit::Day)).unwrap_err(), - @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.day().round(SpanRound::new().largest(Unit::Week)).unwrap_err(), - @"error with `largest` rounding option: using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"error with `largest` rounding option: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); insta::assert_snapshot!( 1.day().round(SpanRound::new().largest(Unit::Month)).unwrap_err(), @@ -842,7 +842,7 @@ fn result_out_of_range() -> Result { let sp = MAX_SPAN_SECONDS.seconds().nanoseconds(999_999_999); insta::assert_snapshot!( sp.round(Unit::Second).unwrap_err(), - @"failed to convert rounded nanoseconds to span for largest unit set to 'seconds': parameter 'seconds' with value 631107417601 is not in the required range of -631107417600..=631107417600", + @"failed to convert rounded nanoseconds 631107417601000000000 to span for largest unit as seconds: parameter 'seconds' with value 631107417601 is not in the required range of -631107417600..=631107417600", ); Ok(()) diff --git a/tests/tc39_262/span/total.rs b/tests/tc39_262/span/total.rs index 985581f..a3968ae 100644 --- a/tests/tc39_262/span/total.rs +++ b/tests/tc39_262/span/total.rs @@ -54,7 +54,7 @@ fn calendar_possibly_required() -> Result { insta::assert_snapshot!( week.total(Unit::Day).unwrap_err(), - @"using unit 'week' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); let result = week.total((Unit::Day, d))?; assert_eq!(result, 7.0); @@ -62,7 +62,7 @@ fn calendar_possibly_required() -> Result { // Differs from Temporal. We require explicit opt-in for 24-hour days. insta::assert_snapshot!( day.total(Unit::Day).unwrap_err(), - @"using unit 'day' in a span or configuration requires that either a relative reference time be given or `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", + @"using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided", ); let result = day.total((Unit::Day, DAY24))?; assert_eq!(result, 42.0);