diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec4df7e..cd96087 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -229,6 +229,23 @@ 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 fc1050d..24a7443 100644 --- a/.vim/coc-settings.json +++ b/.vim/coc-settings.json @@ -2,6 +2,7 @@ "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 705254a..914a7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # 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 be1783e..580d89a 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(&code)); + let code = remove_only_jiffs(&remove_cfg_alloc_or_std(&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")]` gates. +/// Removes all `#[cfg(feature = "alloc|std")]` gates. /// /// This is because the proc-macro always runs in a context where `alloc` /// (and `std`) are enabled. -fn remove_cfg_alloc(code: &str) -> String { +fn remove_cfg_alloc_or_std(code: &str) -> String { static RE: LazyLock = LazyLock::new(|| { - Regex::new(r###"#\[cfg\(feature = "alloc"\)\]\n"###).unwrap() + Regex::new(r###"#\[cfg\(feature = "(alloc|std)"\)\]\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 new file mode 100644 index 0000000..05f56a6 --- /dev/null +++ b/crates/jiff-static/src/shared/error/itime.rs @@ -0,0 +1,100 @@ +// 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 new file mode 100644 index 0000000..921da75 --- /dev/null +++ b/crates/jiff-static/src/shared/error/mod.rs @@ -0,0 +1,57 @@ +// 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 fef2428..6297171 100644 --- a/crates/jiff-static/src/shared/posix.rs +++ b/crates/jiff-static/src/shared/posix.rs @@ -5,8 +5,6 @@ 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, @@ -19,7 +17,9 @@ 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, Error> { + pub fn parse( + bytes: &[u8], + ) -> Result, PosixTimeZoneError> { // 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,12 +333,11 @@ impl + Debug> PosixTimeZone { impl> core::fmt::Display for PosixTimeZone { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!( + core::fmt::Display::fmt( + &AbbreviationDisplay(self.std_abbrev.as_ref()), 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)?; } @@ -352,22 +351,29 @@ impl> PosixDst { std_offset: &PosixOffset, f: &mut core::fmt::Formatter, ) -> core::fmt::Result { - write!(f, "{}", AbbreviationDisplay(self.abbrev.as_ref()))?; + core::fmt::Display::fmt( + &AbbreviationDisplay(self.abbrev.as_ref()), + f, + )?; // 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 { - write!(f, "{}", self.offset)?; + core::fmt::Display::fmt(&self.offset, f)?; } - write!(f, ",{}", self.rule)?; + f.write_str(",")?; + core::fmt::Display::fmt(&self.rule, f)?; Ok(()) } } impl core::fmt::Display for PosixRule { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{},{}", self.start, self.end) + core::fmt::Display::fmt(&self.start, f)?; + f.write_str(",")?; + core::fmt::Display::fmt(&self.end, f)?; + Ok(()) } } @@ -418,11 +424,12 @@ impl PosixDayTime { impl core::fmt::Display for PosixDayTime { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", self.date)?; + core::fmt::Display::fmt(&self.date, f)?; // This is the default time, so don't write it if we // don't need to. if self.time != PosixTime::DEFAULT { - write!(f, "/{}", self.time)?; + f.write_str("/")?; + core::fmt::Display::fmt(&self.time, f)?; } Ok(()) } @@ -490,10 +497,19 @@ impl PosixDay { impl core::fmt::Display for PosixDay { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { - PosixDay::JulianOne(n) => write!(f, "J{n}"), - PosixDay::JulianZero(n) => write!(f, "{n}"), + PosixDay::JulianOne(n) => { + f.write_str("J")?; + core::fmt::Display::fmt(&n, f) + } + PosixDay::JulianZero(n) => core::fmt::Display::fmt(&n, f), PosixDay::WeekdayOfMonth { month, week, weekday } => { - write!(f, "M{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(()) } } } @@ -506,7 +522,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() { - write!(f, "-")?; + f.write_str("-")?; // The default is positive, so when // positive, we write nothing. } @@ -514,7 +530,7 @@ impl core::fmt::Display for PosixTime { let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - write!(f, "{h}")?; + core::fmt::Display::fmt(&h, f)?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -537,13 +553,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 { - write!(f, "-")?; + f.write_str("-")?; } let second = self.second.unsigned_abs(); let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - write!(f, "{h}")?; + core::fmt::Display::fmt(&h, f)?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -565,9 +581,11 @@ 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 == '-') { - write!(f, "<{s}>") + f.write_str("<")?; + core::fmt::Display::fmt(&s, f)?; + f.write_str(">") } else { - write!(f, "{s}") + core::fmt::Display::fmt(&s, f) } } } @@ -661,15 +679,12 @@ 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, Error> { + fn parse( + &self, + ) -> Result, PosixTimeZoneError> { let (time_zone, remaining) = self.parse_prefix()?; if !remaining.is_empty() { - 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), - )); + return Err(ErrorKind::FoundRemaining.into()); } Ok(time_zone) } @@ -678,7 +693,8 @@ impl<'s> Parser<'s> { /// returns the remaining input. fn parse_prefix( &self, - ) -> Result<(PosixTimeZone, &'s [u8]), Error> { + ) -> Result<(PosixTimeZone, &'s [u8]), PosixTimeZoneError> + { let time_zone = self.parse_posix_time_zone()?; Ok((time_zone, self.remaining())) } @@ -689,18 +705,14 @@ impl<'s> Parser<'s> { /// TZ string. fn parse_posix_time_zone( &self, - ) -> Result, Error> { + ) -> Result, PosixTimeZoneError> { if self.is_done() { - return Err(err!( - "an empty string is not a valid POSIX time zone" - )); + return Err(ErrorKind::Empty.into()); } - 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 std_abbrev = + self.parse_abbreviation().map_err(ErrorKind::AbbreviationStd)?; + let std_offset = + self.parse_posix_offset().map_err(ErrorKind::OffsetStd)?; let mut dst = None; if !self.is_done() && (self.byte().is_ascii_alphabetic() || self.byte() == b'<') @@ -721,49 +733,30 @@ impl<'s> Parser<'s> { fn parse_posix_dst( &self, std_offset: &PosixOffset, - ) -> Result, Error> { - let abbrev = self - .parse_abbreviation() - .map_err(|e| err!("failed to parse DST abbreviation: {e}"))?; + ) -> Result, PosixTimeZoneError> { + let abbrev = + self.parse_abbreviation().map_err(ErrorKind::AbbreviationDst)?; if self.is_done() { - return Err(err!( - "found DST abbreviation `{abbrev}`, but no transition \ - rule (this is technically allowed by POSIX, but has \ - unspecified behavior)", - )); + return Err(ErrorKind::FoundDstNoRule.into()); } // 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(|e| err!("failed to parse DST offset: {e}"))?; + offset = + self.parse_posix_offset().map_err(ErrorKind::OffsetDst)?; if self.is_done() { - 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, - )); + return Err(ErrorKind::FoundDstNoRuleWithOffset.into()); } } if self.byte() != b',' { - return Err(err!( - "after parsing DST offset in POSIX time zone string, \ - found `{}` but expected a ','", - Byte(self.byte()), - )); + return Err(ErrorKind::ExpectedCommaAfterDst.into()); } if !self.bump() { - return Err(err!( - "after parsing DST offset in POSIX time zone string, \ - found end of string after a trailing ','", - )); + return Err(ErrorKind::FoundEndAfterComma.into()); } - let rule = self.parse_rule()?; + let rule = self.parse_rule().map_err(ErrorKind::Rule)?; Ok(PosixDst { abbrev, offset, rule }) } @@ -779,18 +772,17 @@ 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(err!( - "found opening '<' quote for abbreviation in \ - POSIX time zone string, and expected a name \ - following it, but found the end of string instead" + return Err(AbbreviationError::Quoted( + QuotedAbbreviationError::UnexpectedEndAfterOpening, )); } - self.parse_quoted_abbreviation() + self.parse_quoted_abbreviation().map_err(AbbreviationError::Quoted) } else { self.parse_unquoted_abbreviation() + .map_err(AbbreviationError::Unquoted) } } @@ -805,19 +797,16 @@ 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(err!( - "expected abbreviation with at most {} bytes, \ - but found a longer abbreviation beginning with `{}`", - Abbreviation::capacity(), - Bytes(&self.tz[start..][..i]), - )); + return Err(UnquotedAbbreviationError::TooLong); } if !self.bump() { break; @@ -832,18 +821,10 @@ 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. - err!( - "found abbreviation `{}`, but it is not valid UTF-8", - Bytes(&self.tz[start..end]), - ) + UnquotedAbbreviationError::InvalidUtf8 })?; if abbrev.len() < 3 { - return Err(err!( - "expected abbreviation with 3 or more bytes, but found \ - abbreviation {:?} with {} bytes", - abbrev, - abbrev.len(), - )); + return Err(UnquotedAbbreviationError::TooShort); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -861,7 +842,9 @@ 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() @@ -871,12 +854,7 @@ impl<'s> Parser<'s> { break; } if i >= Abbreviation::capacity() { - return Err(err!( - "expected abbreviation with at most {} bytes, \ - but found a longer abbreviation beginning with `{}`", - Abbreviation::capacity(), - Bytes(&self.tz[start..][..i]), - )); + return Err(QuotedAbbreviationError::TooLong); } if !self.bump() { break; @@ -891,33 +869,17 @@ 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. - err!( - "found abbreviation `{}`, but it is not valid UTF-8", - Bytes(&self.tz[start..end]), - ) + QuotedAbbreviationError::InvalidUtf8 })?; if self.is_done() { - return Err(err!( - "found non-empty quoted abbreviation {abbrev:?}, but \ - did not find expected end-of-quoted abbreviation \ - '>' character", - )); + return Err(QuotedAbbreviationError::UnexpectedEnd); } if self.byte() != b'>' { - return Err(err!( - "found non-empty quoted abbreviation {abbrev:?}, but \ - found `{}` instead of end-of-quoted abbreviation '>' \ - character", - Byte(self.byte()), - )); + return Err(QuotedAbbreviationError::UnexpectedLastByte); } self.bump(); if abbrev.len() < 3 { - return Err(err!( - "expected abbreviation with 3 or more bytes, but found \ - abbreviation {abbrev:?} with {} bytes", - abbrev.len(), - )); + return Err(QuotedAbbreviationError::TooShort); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -932,30 +894,18 @@ 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() - .map_err(|e| { - err!( - "failed to parse sign for time offset \ - in POSIX time zone string: {e}", - ) - })? - .unwrap_or(1); + fn parse_posix_offset(&self) -> Result { + let sign = self.parse_optional_sign()?.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(err!( - "incomplete time in POSIX timezone (missing minutes)", - )); + return Err(PosixOffsetError::IncompleteMinutes); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(err!( - "incomplete time in POSIX timezone (missing seconds)", - )); + return Err(PosixOffsetError::IncompleteSeconds); } second = self.parse_second()?; } @@ -986,19 +936,16 @@ 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(|e| { - err!("failed to parse start of DST transition rule: {e}") - })?; + fn parse_rule(&self) -> Result { + let start = self + .parse_posix_datetime() + .map_err(PosixRuleError::DateTimeStart)?; if self.maybe_byte() != Some(b',') || !self.bump() { - return Err(err!( - "expected end of DST rule after parsing the start \ - of the DST rule" - )); + return Err(PosixRuleError::ExpectedEnd); } - let end = self.parse_posix_datetime().map_err(|e| { - err!("failed to parse end of DST transition rule: {e}") - })?; + let end = self + .parse_posix_datetime() + .map_err(PosixRuleError::DateTimeEnd)?; Ok(PosixRule { start, end }) } @@ -1010,7 +957,9 @@ 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, @@ -1019,10 +968,7 @@ impl<'s> Parser<'s> { return Ok(daytime); } if !self.bump() { - return Err(err!( - "expected time specification after '/' following a date - specification in a POSIX time zone DST transition rule", - )); + return Err(PosixDateTimeError::ExpectedTime); } daytime.time = self.parse_posix_time()?; Ok(daytime) @@ -1041,16 +987,11 @@ 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(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" - )); + return Err(PosixDateError::ExpectedJulianNoLeap); } Ok(PosixDay::JulianOne(self.parse_posix_julian_day_no_leap()?)) } @@ -1059,22 +1000,12 @@ impl<'s> Parser<'s> { )), b'M' => { if !self.bump() { - 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" - )); + return Err(PosixDateError::ExpectedMonthWeekWeekday); } let (month, week, weekday) = self.parse_weekday_of_month()?; Ok(PosixDay::WeekdayOfMonth { month, week, weekday }) } - _ => 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()), - )), + _ => Err(PosixDateError::UnexpectedByte), } } @@ -1084,22 +1015,16 @@ 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(|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" - ) - })?; + .map_err(PosixJulianNoLeapError::Parse)?; + let number = i16::try_from(number) + .map_err(|_| PosixJulianNoLeapError::Range)?; if !(1 <= number && number <= 365) { - return Err(err!( - "parsed one based Julian day `{number}`, \ - but one based Julian day in POSIX time zone \ - must be in range 1..=365", - )); + return Err(PosixJulianNoLeapError::Range); } Ok(number) } @@ -1110,22 +1035,16 @@ 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(|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" - ) - })?; + .map_err(PosixJulianLeapError::Parse)?; + let number = + i16::try_from(number).map_err(|_| PosixJulianLeapError::Range)?; if !(0 <= number && number <= 365) { - return Err(err!( - "parsed zero based Julian day `{number}`, \ - but zero based Julian day in POSIX time zone \ - must be in range 0..=365", - )); + return Err(PosixJulianLeapError::Range); } Ok(number) } @@ -1139,31 +1058,22 @@ 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), Error> { + fn parse_weekday_of_month( + &self, + ) -> Result<(i8, i8, i8), WeekdayOfMonthError> { let month = self.parse_month()?; if self.maybe_byte() != Some(b'.') { - return Err(err!( - "expected '.' after month `{month}` in \ - POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedDotAfterMonth); } if !self.bump() { - return Err(err!( - "expected week after month `{month}` in \ - POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedWeekAfterMonth); } let week = self.parse_week()?; if self.maybe_byte() != Some(b'.') { - return Err(err!( - "expected '.' after week `{week}` in POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedDotAfterWeek); } if !self.bump() { - return Err(err!( - "expected day-of-week after week `{week}` in \ - POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedDayOfWeekAfterWeek); } let weekday = self.parse_weekday()?; Ok((month, week, weekday)) @@ -1175,17 +1085,9 @@ 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() - .map_err(|e| { - err!( - "failed to parse sign for transition time \ - in POSIX time zone string: {e}", - ) - })? - .unwrap_or(1); + let sign = self.parse_optional_sign()?.unwrap_or(1); let hour = self.parse_hour_ianav3plus()?; (sign, hour) } else { @@ -1194,18 +1096,12 @@ impl<'s> Parser<'s> { let (mut minute, mut second) = (0, 0); if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(err!( - "incomplete transition time in \ - POSIX time zone string (missing minutes)", - )); + return Err(PosixTimeError::IncompleteMinutes); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(err!( - "incomplete transition time in \ - POSIX time zone string (missing seconds)", - )); + return Err(PosixTimeError::IncompleteSeconds); } second = self.parse_second()?; } @@ -1230,19 +1126,13 @@ 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)?; - let number = i8::try_from(number).map_err(|_| { - err!( - "month `{number}` in POSIX time zone \ - does not fit into 8-bit integer" - ) - })?; + 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)?; if !(1 <= number && number <= 12) { - return Err(err!( - "parsed month `{number}`, but month in \ - POSIX time zone must be in range 1..=12", - )); + return Err(MonthError::Range); } Ok(number) } @@ -1251,19 +1141,14 @@ 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)?; - let number = i8::try_from(number).map_err(|_| { - err!( - "week `{number}` in POSIX time zone \ - does not fit into 8-bit integer" - ) - })?; + 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)?; if !(1 <= number && number <= 5) { - return Err(err!( - "parsed week `{number}`, but week in \ - POSIX time zone must be in range 1..=5" - )); + return Err(WeekOfMonthError::Range); } Ok(number) } @@ -1275,20 +1160,13 @@ 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)?; - let number = i8::try_from(number).map_err(|_| { - err!( - "weekday `{number}` in POSIX time zone \ - does not fit into 8-bit integer" - ) - })?; + 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)?; if !(0 <= number && number <= 6) { - return Err(err!( - "parsed weekday `{number}`, but weekday in \ - POSIX time zone must be in range `0..=6` \ - (with `0` corresponding to Sunday)", - )); + return Err(WeekdayError::Range); } Ok(number) } @@ -1304,27 +1182,20 @@ 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(|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" - ) - })?; + .map_err(HourIanaError::Parse)?; + let number = + i16::try_from(number).map_err(|_| HourIanaError::Range)?; 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(err!( - "parsed hour `{number}`, but hour in IANA v3+ \ - POSIX time zone must be in range `-167..=167`", - )); + return Err(HourIanaError::Range); } Ok(number) } @@ -1338,21 +1209,14 @@ 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(|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" - ) - })?; + .map_err(HourPosixError::Parse)?; + let number = + i8::try_from(number).map_err(|_| HourPosixError::Range)?; if !(0 <= number && number <= 24) { - return Err(err!( - "parsed hour `{number}`, but hour in \ - POSIX time zone must be in range `0..=24`", - )); + return Err(HourPosixError::Range); } Ok(number) } @@ -1364,21 +1228,13 @@ 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(|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" - ) - })?; + .map_err(MinuteError::Parse)?; + let number = i8::try_from(number).map_err(|_| MinuteError::Range)?; if !(0 <= number && number <= 59) { - return Err(err!( - "parsed minute `{number}`, but minute in \ - POSIX time zone must be in range `0..=59`", - )); + return Err(MinuteError::Range); } Ok(number) } @@ -1390,21 +1246,13 @@ 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(|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" - ) - })?; + .map_err(SecondError::Parse)?; + let number = i8::try_from(number).map_err(|_| SecondError::Range)?; if !(0 <= number && number <= 59) { - return Err(err!( - "parsed second `{number}`, but second in \ - POSIX time zone must be in range `0..=59`", - )); + return Err(SecondError::Range); } Ok(number) } @@ -1420,27 +1268,20 @@ 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 i in 0..n { + for _ in 0..n { if self.is_done() { - return Err(err!("expected {n} digits, but found {i}")); + return Err(NumberError::ExpectedLength); } let byte = self.byte(); let digit = match byte.checked_sub(b'0') { None => { - return Err(err!( - "invalid digit, expected 0-9 but got {}", - Byte(byte), - )); + return Err(NumberError::InvalidDigit); } Some(digit) if digit > 9 => { - return Err(err!( - "invalid digit, expected 0-9 but got {}", - Byte(byte), - )) + return Err(NumberError::InvalidDigit); } Some(digit) => { debug_assert!((0..=9).contains(&digit)); @@ -1450,12 +1291,7 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or_else(|| { - err!( - "number `{}` too big to parse into 64-bit integer", - Bytes(&self.tz[start..][..i]), - ) - })?; + .ok_or(NumberError::TooBig)?; self.bump(); } Ok(number) @@ -1467,14 +1303,16 @@ 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(err!("invalid number, no digits found")); + return Err(NumberError::Empty); } break; } @@ -1482,12 +1320,7 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or_else(|| { - err!( - "number `{}` too big to parse into 64-bit integer", - Bytes(&self.tz[start..][..i]), - ) - })?; + .ok_or(NumberError::TooBig)?; self.bump(); } Ok(number) @@ -1500,26 +1333,20 @@ 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, Error> { + fn parse_optional_sign(&self) -> Result, OptionalSignError> { if self.is_done() { return Ok(None); } Ok(match self.byte() { b'-' => { if !self.bump() { - return Err(err!( - "expected digit after '-' sign, \ - but got end of input", - )); + return Err(OptionalSignError::ExpectedDigitAfterMinus); } Some(-1) } b'+' => { if !self.bump() { - return Err(err!( - "expected digit after '+' sign, \ - but got end of input", - )); + return Err(OptionalSignError::ExpectedDigitAfterPlus); } Some(1) } @@ -1577,11 +1404,717 @@ impl<'s> Parser<'s> { } } -// Tests require parsing, and parsing requires alloc. +#[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", + ), + } + } +} + #[cfg(test)] mod tests { - use alloc::string::ToString; - use super::*; fn posix_time_zone( @@ -1589,21 +2122,25 @@ mod tests { ) -> PosixTimeZone { let input = input.as_ref(); let tz = PosixTimeZone::parse(input).unwrap(); - // 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()); + { + 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()); + } tz } diff --git a/crates/jiff-static/src/shared/tzif.rs b/crates/jiff-static/src/shared/tzif.rs index 36d69dd..1b60a78 100644 --- a/crates/jiff-static/src/shared/tzif.rs +++ b/crates/jiff-static/src/shared/tzif.rs @@ -5,8 +5,6 @@ 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, @@ -59,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 2036. See above.) +// (Although we won't go above `FATTEN_UP_TO_YEAR`. See above.) const FATTEN_MAX_TRANSITIONS: usize = 300; impl TzifOwned { @@ -80,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(|e| err!("failed to parse 32-bit header: {e}"))?; + let (header32, rest) = + Header::parse(4, bytes).map_err(TzifErrorKind::Header32)?; let (mut tzif, rest) = if header32.version == 0 { TzifOwned::parse32(name, header32, rest)? } else { @@ -117,7 +115,7 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> Result<(TzifOwned, &'b [u8]), Error> { + ) -> Result<(TzifOwned, &'b [u8]), TzifError> { let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -148,14 +146,11 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> 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}"))?; + ) -> 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)?; let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -190,9 +185,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TzifError> { let (bytes, rest) = try_split_at( - "transition times data block", + SplitAtError::TransitionTimes, bytes, header.transition_times_len()?, )?; @@ -242,21 +237,17 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TransitionTypeError> { let (bytes, rest) = try_split_at( - "transition types data block", + SplitAtError::TransitionTypes, 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(err!( - "found transition type index {type_index}, - but there are only {} local time types", - header.tzh_typecnt, - )); + return Err(TransitionTypeError::ExceedsLocalTimeTypes); } self.transitions.infos[transition_index].type_index = type_index; } @@ -267,9 +258,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TzifError> { let (bytes, rest) = try_split_at( - "local time types data block", + SplitAtError::LocalTimeTypes, bytes, header.local_time_types_len()?, )?; @@ -277,8 +268,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(err!( - "found local time type with out-of-bounds offset: {offset}" + return Err(TzifError::from( + LocalTimeTypeError::InvalidOffset { offset }, )); } let is_dst = chunk[4] == 1; @@ -298,49 +289,30 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TimeZoneDesignatorError> { let (bytes, rest) = try_split_at( - "time zone designations data block", + SplitAtError::TimeZoneDesignations, bytes, - header.time_zone_designations_len()?, + header.time_zone_designations_len(), )?; - self.fixed.designations = - String::from_utf8(bytes.to_vec()).map_err(|_| { - err!( - "time zone designations are not valid UTF-8: {:?}", - Bytes(bytes), - ) - })?; + self.fixed.designations = String::from_utf8(bytes.to_vec()) + .map_err(|_| TimeZoneDesignatorError::InvalidUtf8)?; // Holy hell, this is brutal. The boundary conditions are crazy. - for (i, typ) in self.types.iter_mut().enumerate() { + for typ in self.types.iter_mut() { let start = usize::from(typ.designation.0); - 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", - ) - })?; + 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)?; } Ok(rest) } @@ -353,9 +325,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TzifError> { let (bytes, rest) = try_split_at( - "leap seconds data block", + SplitAtError::LeapSeconds, bytes, header.leap_second_len()?, )?; @@ -381,16 +353,16 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], IndicatorError> { let (std_wall_bytes, rest) = try_split_at( - "standard/wall indicators data block", + SplitAtError::StandardWallIndicators, bytes, - header.standard_wall_len()?, + header.standard_wall_len(), )?; let (ut_local_bytes, rest) = try_split_at( - "UT/local indicators data block", + SplitAtError::UTLocalIndicators, 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 @@ -398,14 +370,8 @@ 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. - 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", - )); - } + if ut_local_bytes.iter().any(|&byte| byte != 0) { + return Err(IndicatorError::UtLocalNonZero); } } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() { for (i, &byte) in std_wall_bytes.iter().enumerate() { @@ -416,10 +382,7 @@ impl TzifOwned { } else if byte == 1 { TzifIndicator::LocalStandard } else { - return Err(err!( - "found invalid std/wall indicator '{byte}' for \ - local time type {i}, it must be 0 or 1", - )); + return Err(IndicatorError::InvalidStdWallIndicator); }; } } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() { @@ -433,18 +396,9 @@ impl TzifOwned { (1, 0) => TzifIndicator::LocalStandard, (1, 1) => TzifIndicator::UTStandard, (0, 1) => { - 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::InvalidUtWallCombination); } + _ => return Err(IndicatorError::InvalidCombination), }; } } else { @@ -461,42 +415,31 @@ impl TzifOwned { &mut self, _header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], FooterError> { if bytes.is_empty() { - return Err(err!( - "invalid V2+ TZif footer, expected \\n, \ - but found unexpected end of data", - )); + return Err(FooterError::UnexpectedEnd); } if bytes[0] != b'\n' { - return Err(err!( - "invalid V2+ TZif footer, expected {:?}, but found {:?}", - Byte(b'\n'), - Byte(bytes[0]), - )); + return Err(FooterError::MismatchEnd); } 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 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 nlat = toscan + .iter() + .position(|&b| b == b'\n') + .ok_or(FooterError::TerminatorNotFound)?; 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 the GNU tooling allow it via the `TZ` environment variable + // that GNU tooling allows 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(|e| err!("{e}"))?; + let posix_tz = PosixTimeZone::parse(bytes) + .map_err(FooterError::InvalidPosixTz)?; self.fixed.posix_tz = Some(posix_tz); } Ok(&rest[1..]) @@ -509,7 +452,9 @@ 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<(), Error> { + fn verify_posix_time_zone_consistency( + &self, + ) -> Result<(), InconsistentPosixTimeZoneError> { // 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 @@ -536,35 +481,13 @@ impl TzifOwned { let (ioff, abbrev, is_dst) = tz.to_offset_info(ITimestamp::from_second(*last)); if ioff.second != typ.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, - )); + return Err(InconsistentPosixTimeZoneError::Offset); } if is_dst != typ.is_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, - )); + return Err(InconsistentPosixTimeZoneError::Dst); } if abbrev != self.designation(&typ) { - 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, - )); + return Err(InconsistentPosixTimeZoneError::Designation); } Ok(()) } @@ -580,7 +503,6 @@ 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); @@ -846,14 +768,14 @@ impl Header { fn parse( time_size: usize, bytes: &[u8], - ) -> Result<(Header, &[u8]), Error> { + ) -> Result<(Header, &[u8]), HeaderError> { assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8"); if bytes.len() < 44 { - return Err(err!("invalid header: too short")); + return Err(HeaderError::TooShort); } let (magic, rest) = bytes.split_at(4); if magic != b"TZif" { - return Err(err!("invalid header: magic bytes mismatch")); + return Err(HeaderError::MismatchMagic); } let (version, rest) = rest.split_at(1); let (_reserved, rest) = rest.split_at(15); @@ -865,40 +787,42 @@ 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| 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}"))?; + 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 } + })?; if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt { - return Err(err!( - "expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \ - or equal to tzh_typecnt={tzh_typecnt}", - )); + return Err(HeaderError::MismatchUtType); } if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt { - return Err(err!( - "expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \ - or equal to tzh_typecnt={tzh_typecnt}", - )); + return Err(HeaderError::MismatchStdType); } if tzh_typecnt < 1 { - return Err(err!( - "expected tzh_typecnt={tzh_typecnt} to be at least 1", - )); + return Err(HeaderError::ZeroType); } if tzh_charcnt < 1 { - return Err(err!( - "expected tzh_charcnt={tzh_charcnt} to be at least 1", - )); + return Err(HeaderError::ZeroChar); } let header = Header { @@ -929,64 +853,513 @@ 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_else(|| { - err!( - "length of data block in V{} tzfile is too big", - self.version - ) - }) + .ok_or(HeaderError::InvalidDataBlock { version: self.version }) } - 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_times_len(&self) -> Result { + self.tzh_timecnt + .checked_mul(self.time_size) + .ok_or(HeaderError::InvalidTimeCount) } - fn transition_types_len(&self) -> Result { - Ok(self.tzh_timecnt) + fn transition_types_len(&self) -> usize { + self.tzh_timecnt } - 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 local_time_types_len(&self) -> Result { + self.tzh_typecnt.checked_mul(6).ok_or(HeaderError::InvalidTypeCount) } - fn time_zone_designations_len(&self) -> Result { - Ok(self.tzh_charcnt) + fn time_zone_designations_len(&self) -> usize { + 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_else(|| { - err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt) - }) + self.tzh_leapcnt + .checked_mul(record_len) + .ok_or(HeaderError::InvalidLeapSecondCount) } - fn standard_wall_len(&self) -> Result { - Ok(self.tzh_ttisstdcnt) + fn standard_wall_len(&self) -> usize { + self.tzh_ttisstdcnt } - fn ut_local_len(&self) -> Result { - Ok(self.tzh_ttisutcnt) + 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, + ) } } @@ -996,16 +1369,12 @@ impl Header { /// 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: &'static str, + what: SplitAtError, bytes: &'b [u8], at: usize, -) -> Result<(&'b [u8], &'b [u8]), Error> { +) -> Result<(&'b [u8], &'b [u8]), SplitAtError> { if at > bytes.len() { - Err(err!( - "expected at least {at} bytes for {what}, \ - but found only {} bytes", - bytes.len(), - )) + Err(what) } else { Ok(bytes.split_at(at)) } @@ -1022,14 +1391,9 @@ 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(|_| { - err!( - "failed to parse integer {n} (too big, max allowed is {}", - usize::MAX - ) - }) + usize::try_from(n).map_err(|_| U32UsizeError) } /// 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 5ed8cd1..e5b182b 100644 --- a/crates/jiff-static/src/shared/util/escape.rs +++ b/crates/jiff-static/src/shared/util/escape.rs @@ -17,6 +17,7 @@ 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, " "); @@ -37,6 +38,7 @@ 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)?; @@ -54,15 +56,16 @@ 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(errant_bytes) => { + Err(err) => { // The decode API guarantees `errant_bytes` is non-empty. - write!(f, r"\x{:02x}", errant_bytes[0])?; + write!(f, r"\x{:02x}", err.as_slice()[0])?; bytes = &bytes[1..]; continue; } @@ -81,6 +84,7 @@ 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)?; @@ -88,3 +92,34 @@ 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 ba012c6..c919a26 100644 --- a/crates/jiff-static/src/shared/util/itime.rs +++ b/crates/jiff-static/src/shared/util/itime.rs @@ -24,8 +24,6 @@ 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, @@ -143,11 +141,13 @@ 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( - || err!("adding `{seconds}s` to datetime overflowed"), - )?; + ) -> Result { + let day_second = self + .time + .to_second() + .second + .checked_add(seconds) + .ok_or_else(|| RangeError::DateTimeSeconds)?; 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 { - const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; - const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; + pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; + pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; /// Converts days since the Unix epoch to a Gregorian date. /// @@ -219,20 +219,17 @@ 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(|| { - err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32") - })?; + let sum = epoch_day + .checked_add(amount) + .ok_or_else(|| RangeError::EpochDayI32)?; let ret = IEpochDay { epoch_day: sum }; if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) { - 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, - )); + return Err(RangeError::EpochDayDays); } Ok(ret) } @@ -260,14 +257,11 @@ 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(err!( - "day={day} is out of range for year={year} \ - and month={month}, must be in range 1..={max_day}", - )); + return Err(RangeError::DateInvalidDays { year, month }); } } Ok(IDate { year, month, day }) @@ -283,37 +277,22 @@ impl IDate { pub(crate) fn from_day_of_year( year: i16, day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 366) { - 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), - )); + return Err(RangeError::DateInvalidDayOfYear { year }); } let start = IDate { year, month: 1, day: 1 }.to_epoch_day(); let end = start .checked_add(i32::from(day) - 1) - .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, - ) - })? + // This can only happen when `year=9999` and `day=366`. + .map_err(|_| RangeError::DayOfYear)? .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(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), - )); + return Err(RangeError::DateInvalidDayOfYear { year }); } Ok(end) } @@ -329,12 +308,9 @@ 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(err!( - "day-of-year={day} is out of range for year={year}, \ - must be in range 1..=365", - )); + return Err(RangeError::DateInvalidDayOfYearNoLeap); } if day >= 60 && is_leap_year(year) { day += 1; @@ -392,12 +368,9 @@ impl IDate { &self, nth: i8, weekday: IWeekday, - ) -> Result { + ) -> Result { if nth == 0 || !(-5 <= nth && nth <= 5) { - return Err(err!( - "got nth weekday of `{nth}`, but \ - must be non-zero and in range `-5..=5`", - )); + return Err(RangeError::NthWeekdayOfMonth); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -414,13 +387,10 @@ impl IDate { // of `Day`, we can't let this boundary condition escape. So we // check it here. if day < 1 { - 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), - )); + return Err(RangeError::DateInvalidDays { + year: self.year, + month: self.month, + }); } IDate::try_new(self.year, self.month, day) } @@ -428,16 +398,12 @@ 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(err!( - "returning yesterday for -9999-01-01 is not \ - possible because it is less than Jiff's supported - minimum date", - )); + return Err(RangeError::Yesterday); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -450,16 +416,12 @@ 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(err!( - "returning tomorrow for 9999-12-31 is not \ - possible because it is greater than Jiff's supported - maximum date", - )); + return Err(RangeError::Tomorrow); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -471,34 +433,20 @@ 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(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, - )); + return Err(RangeError::YearPrevious); } 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(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, - )); + return Err(RangeError::YearNext); } Ok(year) } @@ -508,7 +456,7 @@ impl IDate { pub(crate) fn checked_add_days( &self, amount: i32, - ) -> Result { + ) -> Result { match amount { 0 => Ok(*self), -1 => self.yesterday(), @@ -720,6 +668,84 @@ 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. @@ -922,4 +948,20 @@ 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 d245aed..3630e73 100644 --- a/crates/jiff-static/src/shared/util/mod.rs +++ b/crates/jiff-static/src/shared/util/mod.rs @@ -1,7 +1,4 @@ // 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 08d0c7a..1738a20 100644 --- a/crates/jiff-static/src/shared/util/utf8.rs +++ b/crates/jiff-static/src/shared/util/utf8.rs @@ -1,5 +1,59 @@ // 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 @@ -15,25 +69,24 @@ /// *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( - &bytes[..err.error_len().unwrap_or_else(|| bytes.len())] - )) - } + 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())) } + diff --git a/crates/jiff-tzdb/Cargo.toml b/crates/jiff-tzdb/Cargo.toml index 3374467..7330144 100644 --- a/crates/jiff-tzdb/Cargo.toml +++ b/crates/jiff-tzdb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jiff-tzdb" -version = "0.1.4" #:version +version = "0.1.5" #: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 8b5878d..8990c8c 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 f6fd342..ca4788f 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"2025b"); +pub(super) static VERSION: Option<&str> = Some(r"2025c"); 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", 163461..164770), - (r"Africa/Casablanca", 189897..191818), + (r"Africa/Cairo", 162382..163691), + (r"Africa/Casablanca", 190185..192106), (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", 186193..188019), + (r"Africa/El_Aaiun", 186481..188307), (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", 152557..153642), + (r"America/Asuncion", 151478..152563), (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", 184439..186193), + (r"America/Chicago", 184727..186481), (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", 167472..168834), + (r"America/Coyhaique", 167760..169122), (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", 149270..150349), - (r"America/Fort_Nelson", 171698..173146), + (r"America/Ensenada", 166393..167760), + (r"America/Fort_Nelson", 171986..173434), (r"America/Fort_Wayne", 36060..36591), (r"America/Fortaleza", 37559..38043), (r"America/Glace_Bay", 110783..111663), - (r"America/Godthab", 191818..192783), - (r"America/Goose_Bay", 176127..177707), + (r"America/Godthab", 192106..193071), + (r"America/Goose_Bay", 176415..177995), (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", 179306..180978), - (r"America/Havana", 153642..154759), + (r"America/Halifax", 179594..181266), + (r"America/Havana", 152563..153680), (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", 158397..159639), + (r"America/Kentucky/Louisville", 157318..158560), (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", 160873..162167), - (r"America/Louisville", 158397..159639), + (r"America/Los_Angeles", 159794..161088), + (r"America/Louisville", 157318..158560), (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", 174634..176127), + (r"America/Moncton", 174922..176415), (r"America/Monterrey", 74688..75397), (r"America/Montevideo", 128417..129386), - (r"America/Montreal", 180978..182695), + (r"America/Montreal", 181266..182983), (r"America/Montserrat", 9872..10049), - (r"America/Nassau", 180978..182695), - (r"America/New_York", 182695..184439), - (r"America/Nipigon", 180978..182695), + (r"America/Nassau", 181266..182983), + (r"America/New_York", 182983..184727), + (r"America/Nipigon", 181266..182983), (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", 191818..192783), + (r"America/Nuuk", 192106..193071), (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", 157179..158397), - (r"America/Rainy_River", 162167..163461), + (r"America/Punta_Arenas", 156100..157318), + (r"America/Rainy_River", 161088..162382), (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", 149270..150349), + (r"America/Santa_Isabel", 166393..167760), (r"America/Santarem", 27453..27862), - (r"America/Santiago", 196015..197369), + (r"America/Santiago", 196303..197657), (r"America/Santo_Domingo", 19436..19753), (r"America/Sao_Paulo", 137116..138068), - (r"America/Scoresbysund", 192783..193767), + (r"America/Scoresbysund", 193071..194055), (r"America/Shiprock", 147183..148225), (r"America/Sitka", 119844..120800), (r"America/St_Barthelemy", 9872..10049), - (r"America/St_Johns", 188019..189897), + (r"America/St_Johns", 188307..190185), (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", 180978..182695), - (r"America/Tijuana", 149270..150349), - (r"America/Toronto", 180978..182695), + (r"America/Thunder_Bay", 181266..182983), + (r"America/Tijuana", 166393..167760), + (r"America/Toronto", 181266..182983), (r"America/Tortola", 9872..10049), - (r"America/Vancouver", 164770..166100), + (r"America/Vancouver", 163691..165021), (r"America/Virgin", 9872..10049), (r"America/Whitehorse", 141012..142041), - (r"America/Winnipeg", 162167..163461), + (r"America/Winnipeg", 161088..162382), (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", 159639..160873), + (r"Asia/Damascus", 158560..159794), (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", 197369..200319), + (r"Asia/Gaza", 197657..200607), (r"Asia/Harbin", 25461..25854), - (r"Asia/Hebron", 200319..203287), + (r"Asia/Hebron", 200607..203575), (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", 154759..155959), + (r"Asia/Istanbul", 153680..154880), (r"Asia/Jakarta", 14856..15104), (r"Asia/Jayapura", 8480..8651), - (r"Asia/Jerusalem", 193767..194841), + (r"Asia/Jerusalem", 194055..195129), (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", 193767..194841), + (r"Asia/Tel_Aviv", 194055..195129), (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", 168834..170235), + (r"Atlantic/Azores", 169122..170523), (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", 166100..167472), + (r"Atlantic/Madeira", 165021..166393), (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", 179306..180978), - (r"Canada/Central", 162167..163461), - (r"Canada/Eastern", 180978..182695), + (r"Canada/Atlantic", 179594..181266), + (r"Canada/Central", 161088..162382), + (r"Canada/Eastern", 181266..182983), (r"Canada/Mountain", 133167..134137), - (r"Canada/Newfoundland", 188019..189897), - (r"Canada/Pacific", 164770..166100), + (r"Canada/Newfoundland", 188307..190185), + (r"Canada/Pacific", 163691..165021), (r"Canada/Saskatchewan", 54642..55280), (r"Canada/Yukon", 141012..142041), - (r"CET", 151454..152557), - (r"Chile/Continental", 196015..197369), - (r"Chile/EasterIsland", 194841..196015), - (r"CST6CDT", 184439..186193), - (r"Cuba", 153642..154759), + (r"CET", 150375..151478), + (r"Chile/Continental", 196303..197657), + (r"Chile/EasterIsland", 195129..196303), + (r"CST6CDT", 184727..186481), + (r"Cuba", 152563..153680), (r"EET", 57918..58600), - (r"Egypt", 163461..164770), - (r"Eire", 173146..174634), + (r"Egypt", 162382..163691), + (r"Eire", 173434..174922), (r"EST", 5647..5796), - (r"EST5EDT", 182695..184439), + (r"EST5EDT", 182983..184727), (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", 151454..152557), + (r"Europe/Amsterdam", 150375..151478), (r"Europe/Andorra", 23884..24273), (r"Europe/Astrakhan", 81920..82646), (r"Europe/Athens", 57918..58600), - (r"Europe/Belfast", 177707..179306), + (r"Europe/Belfast", 177995..179594), (r"Europe/Belgrade", 34563..35041), (r"Europe/Berlin", 63350..64055), (r"Europe/Bratislava", 69009..69732), - (r"Europe/Brussels", 151454..152557), + (r"Europe/Brussels", 150375..151478), (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", 173146..174634), - (r"Europe/Gibraltar", 155959..157179), - (r"Europe/Guernsey", 177707..179306), + (r"Europe/Dublin", 173434..174922), + (r"Europe/Gibraltar", 154880..156100), + (r"Europe/Guernsey", 177995..179594), (r"Europe/Helsinki", 32688..33169), - (r"Europe/Isle_of_Man", 177707..179306), - (r"Europe/Istanbul", 154759..155959), - (r"Europe/Jersey", 177707..179306), + (r"Europe/Isle_of_Man", 177995..179594), + (r"Europe/Istanbul", 153680..154880), + (r"Europe/Jersey", 177995..179594), (r"Europe/Kaliningrad", 113459..114363), (r"Europe/Kiev", 38043..38601), (r"Europe/Kirov", 78274..79009), (r"Europe/Kyiv", 38043..38601), - (r"Europe/Lisbon", 170235..171698), + (r"Europe/Lisbon", 170523..171986), (r"Europe/Ljubljana", 34563..35041), - (r"Europe/London", 177707..179306), - (r"Europe/Luxembourg", 151454..152557), + (r"Europe/London", 177995..179594), + (r"Europe/Luxembourg", 150375..151478), (r"Europe/Madrid", 111663..112560), (r"Europe/Malta", 126549..127477), (r"Europe/Mariehamn", 32688..33169), (r"Europe/Minsk", 99205..100013), - (r"Europe/Monaco", 150349..151454), + (r"Europe/Monaco", 149270..150375), (r"Europe/Moscow", 109875..110783), (r"Europe/Nicosia", 44735..45332), (r"Europe/Oslo", 63350..64055), - (r"Europe/Paris", 150349..151454), + (r"Europe/Paris", 149270..150375), (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", 177707..179306), - (r"GB-Eire", 177707..179306), + (r"GB", 177995..179594), + (r"GB-Eire", 177995..179594), (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", 193767..194841), + (r"Israel", 194055..195129), (r"Jamaica", 21737..22076), (r"Japan", 15360..15573), (r"Kwajalein", 12882..13101), (r"Libya", 29570..30001), - (r"MET", 151454..152557), - (r"Mexico/BajaNorte", 149270..150349), + (r"MET", 150375..151478), + (r"Mexico/BajaNorte", 166393..167760), (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", 194841..196015), + (r"Pacific/Easter", 195129..196303), (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", 170235..171698), + (r"Portugal", 170523..171986), (r"PRC", 25461..25854), - (r"PST8PDT", 160873..162167), + (r"PST8PDT", 159794..161088), (r"ROC", 39103..39614), (r"ROK", 27038..27453), (r"Singapore", 15104..15360), - (r"Turkey", 154759..155959), + (r"Turkey", 153680..154880), (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", 184439..186193), + (r"US/Central", 184727..186481), (r"US/East-Indiana", 36060..36591), - (r"US/Eastern", 182695..184439), + (r"US/Eastern", 182983..184727), (r"US/Hawaii", 13910..14131), (r"US/Indiana-Starke", 139996..141012), (r"US/Michigan", 112560..113459), (r"US/Mountain", 147183..148225), - (r"US/Pacific", 160873..162167), + (r"US/Pacific", 159794..161088), (r"US/Samoa", 5197..5343), (r"UTC", 224..335), (r"W-SU", 109875..110783), - (r"WET", 170235..171698), + (r"WET", 170523..171986), (r"Zulu", 224..335), ]; diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..5e5046c --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,244 @@ +# 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 new file mode 100644 index 0000000..7c2a71f --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,44 @@ +[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 new file mode 100644 index 0000000..d26a3b9 --- /dev/null +++ b/fuzz/fuzz_targets/rfc2822_parse.rs @@ -0,0 +1,50 @@ +#![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 new file mode 100644 index 0000000..5bb129d --- /dev/null +++ b/fuzz/fuzz_targets/shim.rs @@ -0,0 +1,48 @@ +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 new file mode 100644 index 0000000..887f6a0 --- /dev/null +++ b/fuzz/fuzz_targets/strtime_parse.rs @@ -0,0 +1,103 @@ +#![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 new file mode 100644 index 0000000..e339a13 --- /dev/null +++ b/fuzz/fuzz_targets/temporal_parse.rs @@ -0,0 +1,51 @@ +#![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 c4e113b..e56f66f 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::{err, Error, ErrorContext}, + error::{civil::Error as E, Error, ErrorContext}, fmt::{ self, temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, @@ -903,7 +903,9 @@ 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::shared)?, + idate + .nth_weekday_of_month(nth, weekday) + .map_err(Error::itime_range)?, )) } @@ -1057,7 +1059,7 @@ impl Date { let nth = t::SpanWeeks::try_new("nth weekday", nth)?; if nth == C(0) { - Err(err!("nth weekday cannot be `0`")) + Err(Error::slim_range("nth weekday")) } else if nth > C(0) { let nth = nth.max(C(1)); let weekday_diff = weekday.since_ranged(self.weekday().next()); @@ -1515,14 +1517,8 @@ impl Date { -1 => self.yesterday(), 1 => self.tomorrow(), days => { - let days = UnixEpochDay::try_new("days", days).with_context( - || { - err!( - "{days} computed from duration {duration:?} \ - overflows Jiff's datetime limits", - ) - }, - )?; + let days = UnixEpochDay::try_new("days", days) + .context(E::OverflowDaysDuration)?; let days = self.to_unix_epoch_day().try_checked_add("days", days)?; Ok(Date::from_unix_epoch_day(days)) @@ -2941,11 +2937,9 @@ impl DateDifference { // // NOTE: I take the above back. It's actually possible for the // months component to overflow when largest=month. - return Err(err!( - "rounding the span between two dates must use days \ - or bigger for its units, but found {units}", - units = largest.plural(), - )); + return Err(Error::from(E::RoundMustUseDaysOrBigger { + unit: largest, + })); } if largest <= Unit::Week { let mut weeks = t::SpanWeeks::rfrom(C(0)); @@ -3197,13 +3191,13 @@ impl DateWith { Some(DateWithDay::OfYear(day)) => { let year = year.get_unchecked(); let idate = IDate::from_day_of_year(year, day) - .map_err(Error::shared)?; + .map_err(Error::itime_range)?; 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::shared)?; + .map_err(Error::itime_range)?; return Ok(Date::from_idate_const(idate)); } }; diff --git a/src/civil/datetime.rs b/src/civil/datetime.rs index 1c6d6e5..f44d1ec 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::{err, Error, ErrorContext}, + error::{civil::Error as E, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -1695,25 +1695,18 @@ impl DateTime { { (true, true) => Ok(self), (false, true) => { - let new_date = - old_date.checked_add(span).with_context(|| { - err!("failed to add {span} to {old_date}") - })?; + let new_date = old_date + .checked_add(span) + .context(E::FailedAddSpanDate)?; Ok(DateTime::from_parts(new_date, old_time)) } (true, false) => { - 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}", - ) - })?; + let (new_time, leftovers) = old_time + .overflowing_add(span) + .context(E::FailedAddSpanTime)?; + let new_date = old_date + .checked_add(leftovers) + .context(E::FailedAddSpanOverflowing)?; Ok(DateTime::from_parts(new_date, new_time)) } (false, false) => self.checked_add_span_general(&span), @@ -1727,20 +1720,14 @@ 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).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}", - ) - })?; + 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)?; Ok(DateTime::from_parts(new_date, new_time)) } @@ -1751,13 +1738,9 @@ 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).with_context(|| { - err!( - "failed to add overflowing signed duration, {leftovers:?}, \ - from adding {duration:?} to {time}, - to {date}", - ) - })?; + let new_date = date + .checked_add(leftovers) + .context(E::FailedAddDurationOverflowing)?; Ok(DateTime::from_parts(new_date, new_time)) } @@ -3552,9 +3535,10 @@ impl DateTimeRound { // it for good reasons. match self.smallest { Unit::Year | Unit::Month | Unit::Week => { - return Err(err!( - "rounding datetimes does not support {unit}", - unit = self.smallest.plural() + return Err(Error::from( + crate::error::util::RoundingIncrementError::Unsupported { + unit: self.smallest, + }, )); } // We don't do any rounding in this case, so just bail now. @@ -3592,9 +3576,7 @@ impl DateTimeRound { // supported datetimes. let end = start .checked_add(Span::new().days_ranged(days_len)) - .with_context(|| { - err!("adding {days_len} days to {start} failed") - })?; + .context(E::FailedAddDays)?; Ok(DateTime::from_parts(end, time)) } diff --git a/src/civil/iso_week_date.rs b/src/civil/iso_week_date.rs index 7452546..bea792a 100644 --- a/src/civil/iso_week_date.rs +++ b/src/civil/iso_week_date.rs @@ -1,6 +1,7 @@ use crate::{ civil::{Date, DateTime, Weekday}, - error::{err, Error}, + error::{civil::Error as E, Error}, + fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER}, util::{ rangeint::RInto, t::{self, ISOWeek, ISOYear, C}, @@ -35,6 +36,51 @@ 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 @@ -665,9 +711,7 @@ 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(err!( - "ISO week number `{week}` is invalid for year `{year}`" - )); + return Err(Error::from(E::InvalidISOWeekNumber)); } // And also, the maximum Date constrains what we can utter with // ISOWeekDate so that we can preserve infallible conversions between @@ -747,6 +791,24 @@ 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 { @@ -808,6 +870,60 @@ 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 4dd651b..127a7d1 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::{err, Error, ErrorContext}, + error::{civil::Error as E, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -950,7 +950,6 @@ 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 @@ -958,15 +957,7 @@ 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) - .with_context(|| { - err!( - "adding signed duration {duration:?}, equal to - {nanos} nanoseconds, to {time} overflowed", - duration = original, - nanos = original.as_nanos(), - time = self, - ) - })?; + .context(E::OverflowTimeNanoseconds)?; Ok(Time::from_nanosecond(end)) } @@ -2603,11 +2594,9 @@ impl TimeDifference { } let largest = self.round.get_largest().unwrap_or(Unit::Hour); if largest > Unit::Hour { - return Err(err!( - "rounding the span between two times must use hours \ - or smaller for its units, but found {units}", - units = largest.plural(), - )); + return Err(Error::from(E::RoundMustUseHoursOrSmaller { + unit: largest, + })); } let start = t1.to_nanosecond(); let end = t2.to_nanosecond(); @@ -3012,22 +3001,13 @@ impl TimeWith { None => self.original.subsec_nanosecond_ranged(), Some(subsec_nanosecond) => { if self.millisecond.is_some() { - return Err(err!( - "cannot set both TimeWith::millisecond \ - and TimeWith::subsec_nanosecond", - )); + return Err(Error::from(E::IllegalTimeWithMillisecond)); } if self.microsecond.is_some() { - return Err(err!( - "cannot set both TimeWith::microsecond \ - and TimeWith::subsec_nanosecond", - )); + return Err(Error::from(E::IllegalTimeWithMicrosecond)); } if self.nanosecond.is_some() { - return Err(err!( - "cannot set both TimeWith::nanosecond \ - and TimeWith::subsec_nanosecond", - )); + return Err(Error::from(E::IllegalTimeWithNanosecond)); } SubsecNanosecond::try_new( "subsec_nanosecond", diff --git a/src/duration.rs b/src/duration.rs index 27ceae3..6d42792 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -1,7 +1,7 @@ use core::time::Duration as UnsignedDuration; use crate::{ - error::{err, ErrorContext}, + error::{duration::Error as E, ErrorContext}, Error, SignedDuration, Span, }; @@ -24,12 +24,8 @@ 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).with_context(|| { - err!( - "unsigned duration {udur:?} exceeds Jiff's limits" - ) - })?; + let sdur = SignedDuration::try_from(udur) + .context(E::RangeUnsignedDuration)?; Ok(SDuration::Absolute(sdur)) } } @@ -91,9 +87,8 @@ 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).with_context(|| { - err!("failed to negate unsigned duration {udur:?}") - })? + -SignedDuration::try_from(udur) + .context(E::FailedNegateUnsignedDuration)? }; Ok(Duration::Signed(sdur)) } diff --git a/src/error/civil.rs b/src/error/civil.rs new file mode 100644 index 0000000..40fce64 --- /dev/null +++ b/src/error/civil.rs @@ -0,0 +1,84 @@ +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 new file mode 100644 index 0000000..56a8a29 --- /dev/null +++ b/src/error/duration.rs @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..e8d0eed --- /dev/null +++ b/src/error/fmt/friendly.rs @@ -0,0 +1,82 @@ +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 new file mode 100644 index 0000000..14fadb9 --- /dev/null +++ b/src/error/fmt/mod.rs @@ -0,0 +1,87 @@ +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 new file mode 100644 index 0000000..af81804 --- /dev/null +++ b/src/error/fmt/offset.rs @@ -0,0 +1,153 @@ +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 new file mode 100644 index 0000000..764b267 --- /dev/null +++ b/src/error/fmt/rfc2822.rs @@ -0,0 +1,231 @@ +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 new file mode 100644 index 0000000..28a0ea6 --- /dev/null +++ b/src/error/fmt/rfc9557.rs @@ -0,0 +1,114 @@ +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 new file mode 100644 index 0000000..efa5d0e --- /dev/null +++ b/src/error/fmt/strtime.rs @@ -0,0 +1,517 @@ +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 new file mode 100644 index 0000000..4f0cd34 --- /dev/null +++ b/src/error/fmt/temporal.rs @@ -0,0 +1,354 @@ +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 new file mode 100644 index 0000000..ae0ef1a --- /dev/null +++ b/src/error/fmt/util.rs @@ -0,0 +1,115 @@ +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.rs b/src/error/mod.rs similarity index 54% rename from src/error.rs rename to src/error/mod.rs index 0675547..c320a6d 100644 --- a/src/error.rs +++ b/src/error/mod.rs @@ -1,16 +1,14 @@ -use crate::{shared::util::error::Error as SharedError, util::sync::Arc}; +use crate::util::sync::Arc; -/// 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; +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; /// An error that can occur in this crate. /// @@ -29,8 +27,11 @@ pub(crate) use err; /// /// 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 no -/// introspection capabilities. +/// [`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. /// /// # Design /// @@ -65,50 +66,6 @@ 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`. /// @@ -120,6 +77,14 @@ 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 /// /// ``` @@ -132,79 +97,114 @@ impl Error { Error::from(ErrorKind::Adhoc(AdhocError::from_args(message))) } - #[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 - } + /// 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(_)) } } 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,9 +220,36 @@ 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 shared(err: SharedError) -> Error { - Error::from(ErrorKind::Shared(err)) + 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)) } /// A convenience constructor for building an I/O error. @@ -258,7 +285,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 @@ -268,6 +295,83 @@ 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")] @@ -275,30 +379,14 @@ impl std::error::Error for Error {} impl core::fmt::Display for Error { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - #[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), + 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(": ")?; } } + Ok(()) } } @@ -328,14 +416,98 @@ 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 { - 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), + 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), } } } @@ -355,10 +527,10 @@ impl From for Error { /// A generic error 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. +/// 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. #[cfg_attr(not(feature = "alloc"), derive(Clone))] struct AdhocError { #[cfg(feature = "alloc")] @@ -368,18 +540,13 @@ 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")] { - AdhocError::from_display(message) + use alloc::string::ToString; + + let message = message.to_string().into_boxed_str(); + AdhocError { message } } #[cfg(not(feature = "alloc"))] { @@ -387,17 +554,6 @@ 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 } } } @@ -476,6 +632,75 @@ 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 @@ -581,21 +806,6 @@ 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`. @@ -603,7 +813,7 @@ impl IntoError for alloc::string::String { /// `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. /// @@ -612,7 +822,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) -> Self; + fn context(self, consequent: impl IntoError) -> Result; /// Like `context`, but hides error construction within a closure. /// @@ -623,39 +833,31 @@ 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() -> E, - ) -> Self; + consequent: impl FnOnce() -> C, + ) -> Result; } -impl ErrorContext for Error { - #[cfg_attr(feature = "perf-inline", inline(always))] - fn context(self, consequent: impl IntoError) -> Error { - self.context_impl(consequent.into_error()) - } - - #[cfg_attr(feature = "perf-inline", inline(always))] - fn with_context( - self, - consequent: impl FnOnce() -> E, - ) -> Error { - self.context_impl(consequent().into_error()) - } -} - -impl ErrorContext for Result { +impl ErrorContext for Result +where + E: IntoError, +{ #[cfg_attr(feature = "perf-inline", inline(always))] fn context(self, consequent: impl IntoError) -> Result { - self.map_err(|err| err.context_impl(consequent.into_error())) + self.map_err(|err| { + err.into_error().context_impl(consequent.into_error()) + }) } #[cfg_attr(feature = "perf-inline", inline(always))] - fn with_context( + fn with_context( self, - consequent: impl FnOnce() -> E, + consequent: impl FnOnce() -> C, ) -> Result { - self.map_err(|err| err.context_impl(consequent().into_error())) + self.map_err(|err| { + err.into_error().context_impl(consequent().into_error()) + }) } } @@ -690,7 +892,12 @@ 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. - expected_size *= 3; + // + // 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; } assert_eq!(expected_size, core::mem::size_of::()); } diff --git a/src/error/signed_duration.rs b/src/error/signed_duration.rs new file mode 100644 index 0000000..06191b9 --- /dev/null +++ b/src/error/signed_duration.rs @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000..3f8d1b0 --- /dev/null +++ b/src/error/span.rs @@ -0,0 +1,139 @@ +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 new file mode 100644 index 0000000..bdd2591 --- /dev/null +++ b/src/error/timestamp.rs @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..3346034 --- /dev/null +++ b/src/error/tz/ambiguous.rs @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..72c5d29 --- /dev/null +++ b/src/error/tz/concatenated.rs @@ -0,0 +1,119 @@ +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 new file mode 100644 index 0000000..70349c9 --- /dev/null +++ b/src/error/tz/db.rs @@ -0,0 +1,141 @@ +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 new file mode 100644 index 0000000..4c5991b --- /dev/null +++ b/src/error/tz/mod.rs @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..4ced302 --- /dev/null +++ b/src/error/tz/offset.rs @@ -0,0 +1,110 @@ +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 new file mode 100644 index 0000000..d9f10f2 --- /dev/null +++ b/src/error/tz/posix.rs @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..23b6471 --- /dev/null +++ b/src/error/tz/system.rs @@ -0,0 +1,104 @@ +#[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 new file mode 100644 index 0000000..f3d66b1 --- /dev/null +++ b/src/error/tz/timezone.rs @@ -0,0 +1,40 @@ +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 new file mode 100644 index 0000000..7273f70 --- /dev/null +++ b/src/error/tz/zic.rs @@ -0,0 +1,323 @@ +#[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 new file mode 100644 index 0000000..8009936 --- /dev/null +++ b/src/error/util.rs @@ -0,0 +1,194 @@ +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 new file mode 100644 index 0000000..c106e6c --- /dev/null +++ b/src/error/zoned.rs @@ -0,0 +1,71 @@ +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 cbdaf9b..57b3eaa 100644 --- a/src/fmt/friendly/mod.rs +++ b/src/fmt/friendly/mod.rs @@ -256,7 +256,9 @@ 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 \"1M\" in the \"friendly\" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with \"M\" instead", + "failed to parse input in the \"friendly\" duration format: \ + expected to find unit designator suffix \ + (e.g., `years` or `secs`) after parsing integer", ); # Ok::<(), Box>(()) @@ -335,7 +337,9 @@ assert_eq!( // Jiff is saving you from doing something wrong assert_eq!( "1 day".parse::().unwrap_err().to_string(), - "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)", + "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)", ); ``` diff --git a/src/fmt/friendly/parser.rs b/src/fmt/friendly/parser.rs index 9132c2d..af73977 100644 --- a/src/fmt/friendly/parser.rs +++ b/src/fmt/friendly/parser.rs @@ -1,11 +1,11 @@ use crate::{ - error::{err, ErrorContext}, + error::{fmt::friendly::Error as E, ErrorContext}, fmt::{ friendly::parser_label, util::{parse_temporal_fraction, DurationUnits}, Parsed, }, - util::{c::Sign, escape, parse}, + util::{c::Sign, parse}, Error, SignedDuration, Span, Unit, }; @@ -188,12 +188,7 @@ impl SpanParser { } let input = input.as_ref(); - imp(self, input).with_context(|| { - err!( - "failed to parse {input:?} in the \"friendly\" format", - input = escape::Bytes(input) - ) - }) + imp(self, input).context(E::Failed) } /// Run the parser on the given string (which may be plain bytes) and, @@ -248,12 +243,7 @@ impl SpanParser { } let input = input.as_ref(); - imp(self, input).with_context(|| { - err!( - "failed to parse {input:?} in the \"friendly\" format", - input = escape::Bytes(input) - ) - }) + imp(self, input).context(E::Failed) } /// Run the parser on the given string (which may be plain bytes) and, @@ -312,12 +302,7 @@ impl SpanParser { } let input = input.as_ref(); - imp(self, input).with_context(|| { - err!( - "failed to parse {input:?} in the \"friendly\" format", - input = escape::Bytes(input) - ) - }) + imp(self, input).context(E::Failed) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -327,7 +312,7 @@ impl SpanParser { builder: &mut DurationUnits, ) -> Result, Error> { if input.is_empty() { - return Err(err!("an empty string is not a valid duration")); + return Err(Error::from(E::Empty)); } // Guard prefix sign parsing to avoid the function call, which is // marked unlineable to keep the fast path tighter. @@ -342,11 +327,7 @@ impl SpanParser { let Parsed { value, input } = self.parse_unit_value(input)?; let Some(first_unit_value) = value else { - 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", - )); + return Err(Error::from(E::ExpectedIntegerAfterSign)); }; let Parsed { input, .. } = @@ -434,11 +415,7 @@ impl SpanParser { parsed_any_after_comma = true; } if !parsed_any_after_comma { - return Err(err!( - "found comma at the end of duration, \ - but a comma indicates at least one more \ - unit follows", - )); + return Err(Error::from(E::ExpectedOneMoreUnitAfterComma)); } Ok(Parsed { value: (), input }) } @@ -454,10 +431,13 @@ impl SpanParser { input: &'i [u8], hour: u64, ) -> Result>, Error> { - if !input.first().map_or(false, |&b| b == b':') { + let Some((&first, tail)) = input.split_first() else { + return Ok(Parsed { input, value: None }); + }; + if first != b':' { return Ok(Parsed { input, value: None }); } - let Parsed { input, value } = self.parse_hms(&input[1..], hour)?; + let Parsed { input, value } = self.parse_hms(tail, hour)?; Ok(Parsed { input, value: Some(value) }) } @@ -477,26 +457,16 @@ impl SpanParser { hour: u64, ) -> Result, Error> { let Parsed { input, value } = self.parse_unit_value(input)?; - 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 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 input = &input[1..]; + let Parsed { input, value } = self.parse_unit_value(input)?; - let Some(second) = value else { - return Err(err!( - "expected to parse second in 'HH:MM:SS' format \ - following parsed minute of {minute}", - )); - }; + let second = value.ok_or(E::ExpectedSecondAfterMinute)?; let (fraction, input) = if input.first().map_or(false, |&b| b == b'.' || b == b',') { let parsed = parse_temporal_fraction(input)?; @@ -540,22 +510,8 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - 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)]), - )); - } - }; + let (unit, len) = + parser_label::find(input).ok_or(E::ExpectedUnitSuffix)?; Ok(Parsed { value: unit, input: &input[len..] }) } @@ -606,17 +562,15 @@ 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 input.starts_with(b"ago") { - (Some(Sign::Negative), &input[3..]) - } else { - (None, input) - }; + let (suffix_sign, input) = + if let Some(tail) = input.strip_prefix(b"ago") { + (Some(Sign::Negative), tail) + } else { + (None, input) + }; let sign = match (prefix_sign, suffix_sign) { (Some(_), Some(_)) => { - return Err(err!( - "expected to find either a prefix sign (+/-) or \ - a suffix sign (ago), but found both", - )) + return Err(Error::from(E::ExpectedOneSign)); } (Some(sign), None) => sign, (None, Some(sign)) => sign, @@ -637,24 +591,24 @@ impl SpanParser { #[inline(never)] fn parse_optional_comma<'i>( &self, - mut input: &'i [u8], + input: &'i [u8], ) -> Result, Error> { - if !input.first().map_or(false, |&b| b == b',') { + let Some((&first, tail)) = input.split_first() else { + return Ok(Parsed { value: (), input }); + }; + if first != b',' { return Ok(Parsed { value: (), input }); } - input = &input[1..]; - if input.is_empty() { - return Err(err!( - "expected whitespace after comma, but found end of input" - )); + + let (second, input) = tail + .split_first() + .ok_or(E::ExpectedWhitespaceAfterCommaEndOfInput)?; + if !is_whitespace(second) { + return Err(Error::from(E::ExpectedWhitespaceAfterComma { + byte: *second, + })); } - 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..] }) + Ok(Parsed { value: (), input }) } /// Parses zero or more bytes of ASCII whitespace. @@ -776,35 +730,35 @@ mod tests { insta::assert_snapshot!( p(""), - @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###, + @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#, ); insta::assert_snapshot!( p(" "), - @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"###, + @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"#, ); insta::assert_snapshot!( p("a"), - @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"###, + @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"#, ); insta::assert_snapshot!( p("2 months 1 year"), - @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)"###, + @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)"#, ); insta::assert_snapshot!( p("1 year 1 mont"), - @r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"#, ); insta::assert_snapshot!( p("2 months,"), - @r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###, + @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#, ); insta::assert_snapshot!( p("2 months, "), - @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"#, + @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"#, ); insta::assert_snapshot!( p("2 months ,"), - @r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"#, ); } @@ -814,19 +768,19 @@ mod tests { insta::assert_snapshot!( p("1yago"), - @r###"failed to parse "1yago" in the "friendly" format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration 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 "1 year 1 monthago" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration 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 "+1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, + @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"#, ); insta::assert_snapshot!( p("-1 year 1 month ago"), - @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"###, + @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"#, ); } @@ -840,7 +794,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 "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"#, + @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"#, ); // one fewer is okay insta::assert_snapshot!( @@ -853,7 +807,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 "640330789636854775.808 micros" in the "friendly" format: failed to set nanosecond value 9223372036854775808 (it overflows `i64`) on span determined from 640330789636854775.808000000"#, + @r#"failed to parse input in the "friendly" duration format: failed to set nanosecond value from fractional component"#, ); // one fewer is okay insta::assert_snapshot!( @@ -868,47 +822,47 @@ mod tests { insta::assert_snapshot!( p("19999 years"), - @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"###, + @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"#, ); insta::assert_snapshot!( p("19999 years ago"), - @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"#, + @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"#, ); insta::assert_snapshot!( p("239977 months"), - @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"###, + @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"#, ); insta::assert_snapshot!( p("239977 months ago"), - @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"#, + @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"#, ); insta::assert_snapshot!( p("1043498 weeks"), - @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"###, + @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"#, ); insta::assert_snapshot!( p("1043498 weeks ago"), - @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"#, + @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"#, ); insta::assert_snapshot!( p("7304485 days"), - @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"###, + @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"#, ); insta::assert_snapshot!( p("7304485 days ago"), - @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"#, + @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"#, ); insta::assert_snapshot!( p("9223372036854775808 nanoseconds"), - @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"#, + @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"#, ); insta::assert_snapshot!( p("9223372036854775808 nanoseconds ago"), - @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"#, + @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"#, ); } @@ -918,11 +872,11 @@ mod tests { insta::assert_snapshot!( p("1.5 years"), - @r#"failed to parse "1.5 years" in the "friendly" format: fractional years are not supported"#, + @r#"failed to parse input in the "friendly" duration format: fractional years are not supported"#, ); insta::assert_snapshot!( p("1.5 nanos"), - @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#, + @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#, ); } @@ -932,19 +886,19 @@ mod tests { insta::assert_snapshot!( p("05:"), - @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#, ); insta::assert_snapshot!( p("05:06"), - @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"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#, ); insta::assert_snapshot!( p("05:06:"), - @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#, ); insta::assert_snapshot!( p("2 hours, 05:06:07"), - @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"#, + @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"#, ); } @@ -968,7 +922,7 @@ mod tests { ); insta::assert_snapshot!( perr("9223372036854775808s"), - @r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#, + @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"#, ); insta::assert_snapshot!( p("-9223372036854775808s"), @@ -1032,21 +986,21 @@ mod tests { insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H"); insta::assert_snapshot!( pe("2562047788015216hrs"), - @r#"failed to parse "2562047788015216hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 2562047788015216 of unit hour"#, + @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#, ); insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M"); insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M"); insta::assert_snapshot!( pe("153722867280912931mins"), - @r#"failed to parse "153722867280912931mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 153722867280912931 of unit minute"#, + @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#, ); insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S"); insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S"); insta::assert_snapshot!( pe("9223372036854775808s"), - @r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#, + @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"#, ); insta::assert_snapshot!( p("-9223372036854775808s"), @@ -1060,39 +1014,39 @@ mod tests { insta::assert_snapshot!( p(""), - @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###, + @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#, ); insta::assert_snapshot!( p(" "), - @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"###, + @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"#, ); insta::assert_snapshot!( p("5"), - @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"###, + @r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#, ); insta::assert_snapshot!( p("a"), - @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"###, + @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"#, ); insta::assert_snapshot!( p("2 minutes 1 hour"), - @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)"###, + @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)"#, ); insta::assert_snapshot!( p("1 hour 1 minut"), - @r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"#, ); insta::assert_snapshot!( p("2 minutes,"), - @r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###, + @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#, ); insta::assert_snapshot!( p("2 minutes, "), - @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"#, + @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"#, ); insta::assert_snapshot!( p("2 minutes ,"), - @r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"#, ); } @@ -1102,19 +1056,19 @@ mod tests { insta::assert_snapshot!( p("1hago"), - @r###"failed to parse "1hago" in the "friendly" format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration 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 "1 hour 1 minuteago" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"###, + @r#"failed to parse input in the "friendly" duration 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 "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, + @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"#, ); insta::assert_snapshot!( p("-1 hour 1 minute ago"), - @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"###, + @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"#, ); } @@ -1127,7 +1081,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 "9223372036854775808 micros" in the "friendly" format: `9223372036854775808` microseconds is too big (or small) to fit into a signed 64-bit integer"#, + @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"#, ); // one fewer is okay insta::assert_snapshot!( @@ -1142,7 +1096,7 @@ mod tests { insta::assert_snapshot!( p("1.5 nanos"), - @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#, + @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#, ); } @@ -1152,19 +1106,19 @@ mod tests { insta::assert_snapshot!( p("05:"), - @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#, ); insta::assert_snapshot!( p("05:06"), - @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"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#, ); insta::assert_snapshot!( p("05:06:"), - @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#, ); insta::assert_snapshot!( p("2 hours, 05:06:07"), - @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"#, + @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"#, ); } @@ -1205,11 +1159,11 @@ mod tests { ); insta::assert_snapshot!( perr("18446744073709551616s"), - @r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#, + @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#, ); insta::assert_snapshot!( perr("-1s"), - @r#"failed to parse "-1s" in the "friendly" format: cannot parse negative duration into unsigned `std::time::Duration`"#, + @r#"failed to parse input in the "friendly" duration format: cannot parse negative duration into unsigned `std::time::Duration`"#, ); } @@ -1263,19 +1217,19 @@ mod tests { insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H"); insta::assert_snapshot!( pe("5124095576030432hrs"), - @r#"failed to parse "5124095576030432hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 5124095576030432 of unit hour"#, + @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#, ); insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H"); insta::assert_snapshot!( pe("307445734561825861mins"), - @r#"failed to parse "307445734561825861mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 307445734561825861 of unit minute"#, + @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#, ); insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S"); insta::assert_snapshot!( pe("18446744073709551616s"), - @r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#, + @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#, ); } @@ -1287,39 +1241,39 @@ mod tests { insta::assert_snapshot!( p(""), - @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###, + @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#, ); insta::assert_snapshot!( p(" "), - @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"###, + @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"#, ); insta::assert_snapshot!( p("5"), - @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"###, + @r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#, ); insta::assert_snapshot!( p("a"), - @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"###, + @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"#, ); insta::assert_snapshot!( p("2 minutes 1 hour"), - @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)"###, + @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)"#, ); insta::assert_snapshot!( p("1 hour 1 minut"), - @r#"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#, + @r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#, ); insta::assert_snapshot!( p("2 minutes,"), - @r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###, + @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#, ); insta::assert_snapshot!( p("2 minutes, "), - @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"#, + @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"#, ); insta::assert_snapshot!( p("2 minutes ,"), - @r#"failed to parse "2 minutes ," in the "friendly" format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#, + @r#"failed to parse input in the "friendly" duration format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#, ); } @@ -1331,19 +1285,19 @@ mod tests { insta::assert_snapshot!( p("1hago"), - @r#"failed to parse "1hago" in the "friendly" format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#, + @r#"failed to parse input in the "friendly" duration 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 "1 hour 1 minuteago" in the "friendly" format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#, + @r#"failed to parse input in the "friendly" duration 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 "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###, + @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"#, ); insta::assert_snapshot!( p("-1 hour 1 minute ago"), - @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"###, + @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"#, ); } @@ -1362,7 +1316,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 "18446744073709551616 micros" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#, + @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#, ); // one fewer is okay insta::assert_snapshot!( @@ -1379,7 +1333,7 @@ mod tests { insta::assert_snapshot!( p("1.5 nanos"), - @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#, + @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#, ); } @@ -1391,19 +1345,19 @@ mod tests { insta::assert_snapshot!( p("05:"), - @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#, ); insta::assert_snapshot!( p("05:06"), - @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"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#, ); insta::assert_snapshot!( p("05:06:"), - @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###, + @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#, ); insta::assert_snapshot!( p("2 hours, 05:06:07"), - @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"#, + @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"#, ); } } diff --git a/src/fmt/friendly/printer.rs b/src/fmt/friendly/printer.rs index b3d5bd6..c2a9a07 100644 --- a/src/fmt/friendly/printer.rs +++ b/src/fmt/friendly/printer.rs @@ -1,6 +1,6 @@ use crate::{ fmt::{ - util::{DecimalFormatter, FractionalFormatter}, + util::{FractionalFormatter, IntegerFormatter}, 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())?; + wtr.write(Unit::Year, span.get_years().unsigned_abs().into())?; } if span.get_months() != 0 { - wtr.write(Unit::Month, span.get_months().unsigned_abs())?; + wtr.write(Unit::Month, span.get_months().unsigned_abs().into())?; } if span.get_weeks() != 0 { - wtr.write(Unit::Week, span.get_weeks().unsigned_abs())?; + wtr.write(Unit::Week, span.get_weeks().unsigned_abs().into())?; } if span.get_days() != 0 { - wtr.write(Unit::Day, span.get_days().unsigned_abs())?; + wtr.write(Unit::Day, span.get_days().unsigned_abs().into())?; } if span.get_hours() != 0 { - wtr.write(Unit::Hour, span.get_hours().unsigned_abs())?; + wtr.write(Unit::Hour, span.get_hours().unsigned_abs().into())?; } 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 = - DecimalFormatter::new().padding(self.padding.unwrap_or(2)); + IntegerFormatter::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,10 +1366,16 @@ 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)?; + wtr.write( + Unit::Millisecond, + (nanos / NANOS_PER_MILLI).into(), + )?; nanos %= NANOS_PER_MILLI; - wtr.write(Unit::Microsecond, nanos / NANOS_PER_MICRO)?; - wtr.write(Unit::Nanosecond, nanos % NANOS_PER_MICRO)?; + wtr.write( + Unit::Microsecond, + (nanos / NANOS_PER_MICRO).into(), + )?; + wtr.write(Unit::Nanosecond, (nanos % NANOS_PER_MICRO).into())?; } Some(FractionalUnit::Hour) => { wtr.write_fractional_duration(FractionalUnit::Hour, &dur)?; @@ -1421,7 +1427,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)?; + wtr.write( + Unit::Millisecond, + (nanos / NANOS_PER_MILLI).into(), + )?; nanos %= NANOS_PER_MILLI; let leftovers = core::time::Duration::new(0, nanos); @@ -1479,7 +1488,7 @@ impl SpanPrinter { // bigger. let fmtint = - DecimalFormatter::new().padding(self.padding.unwrap_or(2)); + IntegerFormatter::new().padding(self.padding.unwrap_or(2)); let fmtfraction = FractionalFormatter::new().precision(self.precision); let mut secs = udur.as_secs(); @@ -1609,7 +1618,7 @@ struct DesignatorWriter<'p, 'w, W> { wtr: &'w mut W, desig: Designators, sign: Option, - fmtint: DecimalFormatter, + fmtint: IntegerFormatter, fmtfraction: FractionalFormatter, written_non_zero_unit: bool, } @@ -1624,7 +1633,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 = - DecimalFormatter::new().padding(printer.padding.unwrap_or(0)); + IntegerFormatter::new().padding(printer.padding.unwrap_or(0)); let fmtfraction = FractionalFormatter::new().precision(printer.precision); DesignatorWriter { @@ -1670,12 +1679,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> { Ok(()) } - fn write( - &mut self, - unit: Unit, - value: impl Into, - ) -> Result<(), Error> { - let value = value.into(); + fn write(&mut self, unit: Unit, value: u64) -> Result<(), Error> { if value == 0 { return Ok(()); } @@ -1729,7 +1733,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> { struct FractionalPrinter { integer: u64, fraction: u32, - fmtint: DecimalFormatter, + fmtint: IntegerFormatter, fmtfraction: FractionalFormatter, } @@ -1746,7 +1750,7 @@ impl FractionalPrinter { fn from_span( span: &Span, unit: FractionalUnit, - fmtint: DecimalFormatter, + fmtint: IntegerFormatter, fmtfraction: FractionalFormatter, ) -> FractionalPrinter { debug_assert!(span.largest_unit() <= Unit::from(unit)); @@ -1758,7 +1762,7 @@ impl FractionalPrinter { fn from_duration( dur: &core::time::Duration, unit: FractionalUnit, - fmtint: DecimalFormatter, + fmtint: IntegerFormatter, fmtfraction: FractionalFormatter, ) -> FractionalPrinter { match unit { diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index b5886fa..ec247bb 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -166,11 +166,11 @@ and features.) */ use crate::{ - error::{err, Error}, + error::{fmt::Error as E, Error}, util::escape, }; -use self::util::{Decimal, DecimalFormatter, Fractional, FractionalFormatter}; +use self::util::{Fractional, FractionalFormatter, Integer, IntegerFormatter}; pub mod friendly; mod offset; @@ -218,12 +218,7 @@ impl<'i, V: core::fmt::Display> Parsed<'i, V> { if self.input.is_empty() { return Ok(self.value); } - Err(err!( - "parsed value '{value}', but unparsed input {unparsed:?} \ - remains (expected no unparsed input)", - value = self.value, - unparsed = escape::Bytes(self.input), - )) + Err(Error::from(E::into_full_error(&self.value, self.input))) } } @@ -244,12 +239,7 @@ impl<'i, V> Parsed<'i, V> { if self.input.is_empty() { return Ok(self.value); } - Err(err!( - "parsed value '{value}', but unparsed input {unparsed:?} \ - remains (expected no unparsed input)", - value = display, - unparsed = escape::Bytes(self.input), - )) + Err(Error::from(E::into_full_error(&display, self.input))) } } @@ -334,6 +324,17 @@ 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 @@ -368,7 +369,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::adhoc) + self.0.write_all(string.as_bytes()).map_err(Error::io) } } @@ -411,7 +412,7 @@ impl Write for StdFmtWrite { fn write_str(&mut self, string: &str) -> Result<(), Error> { self.0 .write_str(string) - .map_err(|_| err!("an error occurred when formatting an argument")) + .map_err(|_| Error::from(E::StdFmtWriteAdapter)) } } @@ -433,7 +434,7 @@ trait WriteExt: Write { #[inline] fn write_int( &mut self, - formatter: &DecimalFormatter, + formatter: &IntegerFormatter, n: impl Into, ) -> Result<(), Error> { self.write_decimal(&formatter.format_signed(n.into())) @@ -444,7 +445,7 @@ trait WriteExt: Write { #[inline] fn write_uint( &mut self, - formatter: &DecimalFormatter, + formatter: &IntegerFormatter, n: impl Into, ) -> Result<(), Error> { self.write_decimal(&formatter.format_unsigned(n.into())) @@ -463,7 +464,7 @@ trait WriteExt: Write { /// Write the given decimal number to this buffer. #[inline] - fn write_decimal(&mut self, decimal: &Decimal) -> Result<(), Error> { + fn write_decimal(&mut self, decimal: &Integer) -> Result<(), Error> { self.write_str(decimal.as_str()) } diff --git a/src/fmt/offset.rs b/src/fmt/offset.rs index f56e48a..f918098 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::{err, Error, ErrorContext}, + error::{fmt::offset::Error as E, Error, ErrorContext}, fmt::{ temporal::{PiecesNumericOffset, PiecesOffset}, util::{parse_temporal_fraction, FractionalFormatter}, @@ -110,7 +110,7 @@ use crate::{ }, tz::Offset, util::{ - escape, parse, + parse, rangeint::{ri8, RFrom}, t::{self, C}, }, @@ -237,13 +237,7 @@ impl Numeric { if part_nanoseconds >= C(500_000_000) { seconds = seconds .try_checked_add("offset-seconds", C(1)) - .with_context(|| { - err!( - "due to precision loss, UTC offset '{}' is \ - rounded to a value that is out of bounds", - self, - ) - })?; + .context(E::PrecisionLoss)?; } } Ok(Offset::from_seconds_ranged(seconds * self.sign)) @@ -254,11 +248,7 @@ impl Numeric { // `Offset` fails. impl core::fmt::Display for Numeric { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - if self.sign == C(-1) { - write!(f, "-")?; - } else { - write!(f, "+")?; - } + f.write_str(if self.sign == C(-1) { "-" } else { "+" })?; write!(f, "{:02}", self.hours)?; if let Some(minutes) = self.minutes { write!(f, ":{:02}", minutes)?; @@ -268,11 +258,8 @@ impl core::fmt::Display for Numeric { } if let Some(nanos) = self.nanoseconds { static FMT: FractionalFormatter = FractionalFormatter::new(); - write!( - f, - ".{}", - FMT.format(i32::from(nanos).unsigned_abs()).as_str() - )?; + f.write_str(".")?; + f.write_str(FMT.format(i32::from(nanos).unsigned_abs()).as_str())?; } Ok(()) } @@ -413,18 +400,14 @@ impl Parser { mut input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(err!("expected UTC offset, but found end of input")); + return Err(Error::from(E::EndOfInput)); } if input[0] == b'Z' || input[0] == b'z' { if !self.zulu { - 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), - )); + return Err(Error::from(E::UnexpectedLetterOffsetNoZulu( + input[0], + ))); } input = &input[1..]; let value = ParsedOffset { kind: ParsedOffsetKind::Zulu }; @@ -464,40 +447,24 @@ 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).with_context(|| { - err!("failed to parse sign in UTC numeric offset {original:?}") - })?; + self.parse_sign(input).context(E::InvalidSign)?; // Parse hours component. let Parsed { value: hours, input } = - self.parse_hours(input).with_context(|| { - err!( - "failed to parse hours in UTC numeric offset {original:?}" - ) - })?; + self.parse_hours(input).context(E::InvalidHours)?; let extended = match self.colon { Colon::Optional => input.starts_with(b":"), Colon::Required => { if !input.is_empty() && !input.starts_with(b":") { - return Err(err!( - "parsed hour component of time zone offset from \ - {original:?}, but could not find required colon \ - separator", - )); + return Err(Error::from(E::NoColonAfterHours)); } true } Colon::Absent => { if !input.is_empty() && input.starts_with(b":") { - return Err(err!( - "parsed hour component of time zone offset from \ - {original:?}, but found colon after hours which \ - is not allowed", - )); + return Err(Error::from(E::ColonAfterHours)); } false } @@ -513,32 +480,22 @@ impl Parser { }; // Parse optional separator after hours. - 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:?}" - ) - })?; + let Parsed { value: has_minutes, input } = self + .parse_separator(input, extended) + .context(E::SeparatorAfterHours)?; if !has_minutes { - 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 }); + return if self.require_minute + || (self.subminute && self.require_second) + { + Err(Error::from(E::MissingMinuteAfterHour)) + } else { + Ok(Parsed { value: numeric, input }) + }; } // Parse minutes component. let Parsed { value: minutes, input } = - self.parse_minutes(input).with_context(|| { - err!( - "failed to parse minutes in UTC numeric offset \ - {original:?}" - ) - })?; + self.parse_minutes(input).context(E::InvalidMinutes)?; numeric.minutes = Some(minutes); // If subminute resolution is not supported, then we're done here. @@ -549,65 +506,42 @@ 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). - 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 }); + return if input.get(0).map_or(false, |&b| b == b':') { + Err(Error::from(E::SubminutePrecisionNotEnabled)) + } else { + Ok(Parsed { value: numeric, input }) + }; } // Parse optional separator after minutes. - 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:?}" - ) - })?; + let Parsed { value: has_seconds, input } = self + .parse_separator(input, extended) + .context(E::SeparatorAfterMinutes)?; if !has_seconds { - 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 }); + return if self.require_second { + Err(Error::from(E::MissingSecondAfterMinute)) + } else { + Ok(Parsed { value: numeric, input }) + }; } // Parse seconds component. let Parsed { value: seconds, input } = - self.parse_seconds(input).with_context(|| { - err!( - "failed to parse seconds in UTC numeric offset \ - {original:?}" - ) - })?; + self.parse_seconds(input).context(E::InvalidSeconds)?; 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(err!( - "subsecond precision for UTC numeric offset {original:?} \ - is not enabled in this context (must provide only \ - integral minutes or seconds)", - )); + return Err(Error::from(E::SubsecondPrecisionNotEnabled)); } return Ok(Parsed { value: numeric, input }); } // Parse an optional fractional component. let Parsed { value: nanoseconds, input } = - parse_temporal_fraction(input).with_context(|| { - err!( - "failed to parse fractional nanoseconds in \ - UTC numeric offset {original:?}", - ) - })?; + parse_temporal_fraction(input) + .context(E::InvalidSecondsFractional)?; // OK because `parse_temporal_fraction` guarantees `0..=999_999_999`. numeric.nanoseconds = nanoseconds.map(|n| t::SubsecNanosecond::new(n).unwrap()); @@ -619,19 +553,13 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - let sign = input.get(0).copied().ok_or_else(|| { - err!("expected UTC numeric offset, but found end of input") - })?; + let sign = input.get(0).copied().ok_or(E::EndOfInputNumeric)?; let sign = if sign == b'+' { t::Sign::N::<1>() } else if sign == b'-' { t::Sign::N::<-1>() } else { - return Err(err!( - "expected '+' or '-' sign at start of UTC numeric offset, \ - but found {found:?} instead", - found = escape::Byte(sign), - )); + return Err(Error::from(E::InvalidSignPlusOrMinus)); }; Ok(Parsed { value: sign, input: &input[1..] }) } @@ -641,22 +569,16 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - 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), - ) - })?; + let (hours, input) = + parse::split(input, 2).ok_or(E::EndOfInputHour)?; + let hours = parse::i64(hours).context(E::ParseHours)?; // 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("offset hours are not valid")?; + .context(E::RangeHours)?; Ok(Parsed { value: hours, input }) } @@ -665,20 +587,11 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - 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, input) = + parse::split(input, 2).ok_or(E::EndOfInputMinute)?; + let minutes = parse::i64(minutes).context(E::ParseMinutes)?; let minutes = ParsedOffsetMinutes::try_new("minutes", minutes) - .context("minutes are not valid")?; + .context(E::RangeMinutes)?; Ok(Parsed { value: minutes, input }) } @@ -687,20 +600,11 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - 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, input) = + parse::split(input, 2).ok_or(E::EndOfInputSecond)?; + let seconds = parse::i64(seconds).context(E::ParseSeconds)?; let seconds = ParsedOffsetSeconds::try_new("seconds", seconds) - .context("time zone offset seconds are not valid")?; + .context(E::RangeSeconds)?; Ok(Parsed { value: seconds, input }) } @@ -941,7 +845,7 @@ mod tests { fn err_numeric_empty() { insta::assert_snapshot!( Parser::new().parse_numeric(b"").unwrap_err(), - @r###"failed to parse sign in UTC numeric offset "": expected UTC numeric offset, but found end of input"###, + @"failed to parse sign in UTC numeric offset: expected UTC numeric offset, but found end of input", ); } @@ -950,7 +854,7 @@ mod tests { fn err_numeric_notsign() { insta::assert_snapshot!( Parser::new().parse_numeric(b"*").unwrap_err(), - @r###"failed to parse sign in UTC numeric offset "*": expected '+' or '-' sign at start of UTC numeric offset, but found "*" instead"###, + @"failed to parse sign in UTC numeric offset: expected `+` or `-` sign at start of UTC numeric offset", ); } @@ -959,7 +863,7 @@ mod tests { fn err_numeric_hours_too_short() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+a").unwrap_err(), - @r###"failed to parse hours in UTC numeric offset "+a": expected two digit hour after sign, but found end of input"###, + @"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input", ); } @@ -968,7 +872,7 @@ mod tests { fn err_numeric_hours_invalid_digits() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+ab").unwrap_err(), - @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"###, + @"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", ); } @@ -977,7 +881,7 @@ mod tests { fn err_numeric_hours_out_of_range() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-26").unwrap_err(), - @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"###, + @"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", ); } @@ -986,7 +890,7 @@ mod tests { fn err_numeric_minutes_too_short() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:a").unwrap_err(), - @r###"failed to parse minutes in UTC numeric offset "+05:a": expected two digit minute after hours, but found end of input"###, + @"failed to parse minutes in UTC numeric offset: expected two digit minute after hours, but found end of input", ); } @@ -995,7 +899,7 @@ mod tests { fn err_numeric_minutes_invalid_digits() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:ab").unwrap_err(), - @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"###, + @"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", ); } @@ -1004,7 +908,7 @@ mod tests { fn err_numeric_minutes_out_of_range() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:60").unwrap_err(), - @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"###, + @"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", ); } @@ -1013,7 +917,7 @@ mod tests { fn err_numeric_seconds_too_short() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:30:a").unwrap_err(), - @r###"failed to parse seconds in UTC numeric offset "+05:30:a": expected two digit second after hours, but found end of input"###, + @"failed to parse seconds in UTC numeric offset: expected two digit second after minutes, but found end of input", ); } @@ -1022,7 +926,7 @@ mod tests { fn err_numeric_seconds_invalid_digits() { insta::assert_snapshot!( Parser::new().parse_numeric(b"+05:30:ab").unwrap_err(), - @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"###, + @"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", ); } @@ -1031,7 +935,7 @@ mod tests { fn err_numeric_seconds_out_of_range() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:60").unwrap_err(), - @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"###, + @"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", ); } @@ -1041,31 +945,31 @@ mod tests { fn err_numeric_fraction_non_empty() { insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:44.").unwrap_err(), - @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"###, + @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", ); insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:44,").unwrap_err(), - @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"###, + @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any 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(), - @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"###, + @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", ); insta::assert_snapshot!( Parser::new().parse_numeric(b"-05:30:44,a").unwrap_err(), - @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"###, + @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", ); // And also test basic format. insta::assert_snapshot!( Parser::new().parse_numeric(b"-053044.a").unwrap_err(), - @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"###, + @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", ); insta::assert_snapshot!( Parser::new().parse_numeric(b"-053044,a").unwrap_err(), - @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"###, + @"failed to parse fractional seconds in UTC numeric offset: found decimal after seconds component, but did not find any digits after decimal", ); } @@ -1076,7 +980,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(), - @r###"subminute precision for UTC numeric offset "-05:59:32" is not enabled in this context (must provide only integral minutes)"###, + @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)", ); } @@ -1086,11 +990,11 @@ mod tests { fn err_zulu_disabled_but_desired() { insta::assert_snapshot!( Parser::new().zulu(false).parse(b"Z").unwrap_err(), - @r###"found "Z" in "Z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###, + @"found `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(), - @r###"found "z" in "z" where a numeric UTC offset was expected (this context does not permit the Zulu offset)"###, + @"found `z` where a numeric UTC offset was expected (this context does not permit the Zulu offset)", ); } @@ -1118,7 +1022,7 @@ mod tests { }; insta::assert_snapshot!( numeric.to_offset().unwrap_err(), - @"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", + @"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", ); } @@ -1143,7 +1047,7 @@ mod tests { }; insta::assert_snapshot!( numeric.to_offset().unwrap_err(), - @"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", + @"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", ); } } diff --git a/src/fmt/rfc2822.rs b/src/fmt/rfc2822.rs index 7c986d9..d199a81 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::{err, ErrorContext}, - fmt::{util::DecimalFormatter, Parsed, Write, WriteExt}, + error::{fmt::rfc2822::Error as E, ErrorContext}, + fmt::{util::IntegerFormatter, Parsed, Write, WriteExt}, tz::{Offset, TimeZone}, util::{ - escape, parse, + parse, rangeint::{ri8, RFrom}, t::{self, C}, }, @@ -313,9 +313,7 @@ impl DateTimeParser { let input = input.as_ref(); let zdt = self .parse_zoned_internal(input) - .context( - "failed to parse RFC 2822 datetime into Jiff zoned datetime", - )? + .context(E::FailedZoned)? .into_full()?; Ok(zdt) } @@ -351,7 +349,7 @@ impl DateTimeParser { let input = input.as_ref(); let ts = self .parse_timestamp_internal(input) - .context("failed to parse RFC 2822 datetime into Jiff timestamp")? + .context(E::FailedTimestamp)? .into_full()?; Ok(ts) } @@ -367,9 +365,7 @@ impl DateTimeParser { ) -> Result, Error> { let Parsed { value: (dt, offset), input } = self.parse_datetime_offset(input)?; - let ts = offset - .to_timestamp(dt) - .context("RFC 2822 datetime out of Jiff's range")?; + let ts = offset.to_timestamp(dt)?; let zdt = ts.to_zoned(TimeZone::fixed(offset)); Ok(Parsed { value: zdt, input }) } @@ -385,9 +381,7 @@ impl DateTimeParser { ) -> Result, Error> { let Parsed { value: (dt, offset), input } = self.parse_datetime_offset(input)?; - let ts = offset - .to_timestamp(dt) - .context("RFC 2822 datetime out of Jiff's range")?; + let ts = offset.to_timestamp(dt)?; Ok(Parsed { value: ts, input }) } @@ -425,16 +419,11 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(err!( - "expected RFC 2822 datetime, but got empty string" - )); + return Err(Error::from(E::Empty)); } let Parsed { input, .. } = self.skip_whitespace(input); if input.is_empty() { - return Err(err!( - "expected RFC 2822 datetime, but got empty string after \ - trimming whitespace", - )); + return Err(Error::from(E::EmptyAfterWhitespace)); } let Parsed { value: wd, input } = self.parse_weekday(input)?; let Parsed { value: day, input } = self.parse_day(input)?; @@ -451,26 +440,19 @@ impl DateTimeParser { self.skip_whitespace(input); let (second, input) = if !input.starts_with(b":") { if !whitespace_after_minute { - return Err(err!( - "expected whitespace after parsing time: \ - expected at least one whitespace character \ - (space or tab), but found none", - )); + return Err(Error::from(E::WhitespaceAfterTime)); } (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).with_context(|| { - err!("expected whitespace after parsing time") - })?; + let Parsed { input, .. } = self.parse_whitespace(input)?; (second, input) }; let date = - Date::new_ranged(year, month, day).context("invalid date")?; + Date::new_ranged(year, month, day).context(E::InvalidDate)?; let time = Time::new_ranged( hour, minute, @@ -480,13 +462,10 @@ impl DateTimeParser { let dt = DateTime::from_parts(date, time); if let Some(wd) = wd { if !self.relaxed_weekday && wd != 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()), - )); + return Err(Error::from(E::InconsistentWeekday { + parsed: wd, + from_date: dt.weekday(), + })); } } Ok(Parsed { value: dt, input }) @@ -517,15 +496,13 @@ impl DateTimeParser { if matches!(input[0], b'0'..=b'9') { return Ok(Parsed { value: None, input }); } - 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(), - )); + if let Ok(len) = u8::try_from(input.len()) { + if len < 4 { + return Err(Error::from(E::TooShortWeekday { + got_non_digit: input[0], + len, + })); + } } let b1 = input[0]; let b2 = input[1]; @@ -543,31 +520,19 @@ impl DateTimeParser { b"fri" => Weekday::Friday, b"sat" => Weekday::Saturday, _ => { - 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]), - )); + return Err(Error::from(E::InvalidWeekday { + got_non_digit: input[0], + })); } }; let Parsed { input, .. } = self.skip_whitespace(&input[3..]); let Some(should_be_comma) = input.get(0).copied() else { - 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]), - )); + return Err(Error::from(E::EndOfInputComma)); }; if should_be_comma != b',' { - 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), - )); + return Err(Error::from(E::UnexpectedByteComma { + byte: should_be_comma, + })); } let Parsed { input, .. } = self.skip_whitespace(&input[1..]); Ok(Parsed { value: Some(wd), input }) @@ -586,21 +551,17 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(err!("expected day, but found end of input")); + return Err(Error::from(E::EndOfInputDay)); } 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).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 day = parse::i64(day).context(E::ParseDay)?; + let day = t::Day::try_new("day", day).context(E::ParseDay)?; let Parsed { input, .. } = - self.parse_whitespace(input).with_context(|| { - err!("expected whitespace after parsing day {day}") - })?; + self.parse_whitespace(input).context(E::WhitespaceAfterDay)?; Ok(Parsed { value: day, input }) } @@ -617,16 +578,12 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(err!( - "expected abbreviated month name, but found end of input" - )); + return Err(Error::from(E::EndOfInputMonth)); } - if input.len() < 3 { - return Err(err!( - "expected abbreviated month name, but remaining input \ - is too short (remaining bytes is {length})", - length = input.len(), - )); + if let Ok(len) = u8::try_from(input.len()) { + if len < 3 { + return Err(Error::from(E::TooShortMonth { len })); + } } let b1 = input[0].to_ascii_lowercase(); let b2 = input[1].to_ascii_lowercase(); @@ -644,22 +601,14 @@ impl DateTimeParser { b"oct" => 10, b"nov" => 11, b"dec" => 12, - _ => { - return Err(err!( - "expected abbreviated month name, \ - but did not recognize {got:?} \ - as a valid month", - got = escape::Bytes(&input[..3]), - )); - } + _ => return Err(Error::from(E::InvalidMonth)), }; // 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..]).with_context(|| { - err!("expected whitespace after parsing month name") - })?; + let Parsed { input, .. } = self + .parse_whitespace(&input[3..]) + .context(E::WhitespaceAfterMonth)?; Ok(Parsed { value: month, input }) } @@ -692,31 +641,22 @@ impl DateTimeParser { { digits += 1; } - if digits <= 1 { - return Err(err!( - "expected at least two ASCII digits for parsing \ - a year, but only found {digits}", - )); + if let Ok(len) = u8::try_from(digits) { + if len <= 1 { + return Err(Error::from(E::TooShortYear { len })); + } } let (year, input) = input.split_at(digits); - 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 = parse::i64(year).context(E::ParseYear)?; 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("year is not valid")?; - let Parsed { input, .. } = self - .parse_whitespace(input) - .with_context(|| err!("expected whitespace after parsing year"))?; + let year = t::Year::try_new("year", year).context(E::InvalidYear)?; + let Parsed { input, .. } = + self.parse_whitespace(input).context(E::WhitespaceAfterYear)?; Ok(Parsed { value: year, input }) } @@ -730,17 +670,9 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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")?; + 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)?; Ok(Parsed { value: hour, input }) } @@ -751,17 +683,11 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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")?; + 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)?; Ok(Parsed { value: minute, input }) } @@ -772,20 +698,14 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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), - ) - })?; + let (second, input) = + parse::split(input, 2).ok_or(E::EndOfInputSecond)?; + let mut second = parse::i64(second).context(E::ParseSecond)?; if second == 60 { second = 59; } - let second = t::Second::try_new("second", second) - .context("second is not valid")?; + let second = + t::Second::try_new("second", second).context(E::InvalidSecond)?; Ok(Parsed { value: second, input }) } @@ -801,13 +721,7 @@ impl DateTimeParser { type ParsedOffsetHours = ri8<0, { t::SpanZoneOffsetHours::MAX }>; type ParsedOffsetMinutes = ri8<0, { t::SpanZoneOffsetMinutes::MAX }>; - 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 = input.get(0).copied().ok_or(E::EndOfInputOffset)?; let sign = if sign == b'+' { t::Sign::N::<1>() } else if sign == b'-' { @@ -816,32 +730,16 @@ impl DateTimeParser { return self.parse_offset_obsolete(input); }; let input = &input[1..]; - 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 (hhmm, input) = parse::split(input, 4).ok_or(E::TooShortOffset)?; - 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 = parse::i64(&hhmm[0..2]).context(E::ParseOffsetHour)?; let hh = ParsedOffsetHours::try_new("zone-offset-hours", hh) - .context("time zone offset hours are not valid")?; + .context(E::InvalidOffsetHour)?; let hh = t::SpanZoneOffset::rfrom(hh); - 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 = parse::i64(&hhmm[2..4]).context(E::ParseOffsetMinute)?; let mm = ParsedOffsetMinutes::try_new("zone-offset-minutes", mm) - .context("time zone offset minutes are not valid")?; + .context(E::InvalidOffsetMinute)?; let mm = t::SpanZoneOffset::rfrom(mm); let seconds = hh * C(3_600) + mm * C(60); @@ -865,11 +763,7 @@ impl DateTimeParser { len += 1; } if len == 0 { - return Err(err!( - "expected obsolete RFC 2822 time zone abbreviation, \ - but found no remaining non-whitespace characters \ - after time", - )); + return Err(Error::from(E::WhitespaceAfterTimeForObsoleteOffset)); } let offset = match &letters[..len] { b"ut" | b"gmt" | b"z" => Offset::UTC, @@ -917,11 +811,7 @@ impl DateTimeParser { Offset::UTC } else { // But anything else we throw our hands up I guess. - return Err(err!( - "expected obsolete RFC 2822 time zone abbreviation, \ - but found {found:?}", - found = escape::Bytes(&input[..len]), - )); + return Err(Error::from(E::InvalidObsoleteOffset)); } } }; @@ -936,15 +826,12 @@ impl DateTimeParser { input: &'i [u8], ) -> Result, Error> { if input.is_empty() { - return Err(err!( - "expected time separator of ':', but found end of input", - )); + return Err(Error::from(E::EndOfInputTimeSeparator)); } if input[0] != b':' { - return Err(err!( - "expected time separator of ':', but found {got}", - got = escape::Byte(input[0]), - )); + return Err(Error::from(E::UnexpectedByteTimeSeparator { + byte: input[0], + })); } Ok(Parsed { value: (), input: &input[1..] }) } @@ -959,10 +846,7 @@ impl DateTimeParser { let Parsed { input, value: had_whitespace } = self.skip_whitespace(input); if !had_whitespace { - return Err(err!( - "expected at least one whitespace character (space or tab), \ - but found none", - )); + return Err(Error::from(E::WhitespaceAfterTime)); } Ok(Parsed { value: (), input }) } @@ -1012,26 +896,20 @@ 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_else(|| { - err!( - "found closing parenthesis in comment with \ - no matching opening parenthesis" - ) - })?; + depth = depth + .checked_sub(1) + .ok_or(E::CommentClosingParenWithoutOpen)?; if depth == 0 { break; } } else if byte == b'(' { - depth = depth.checked_add(1).ok_or_else(|| { - err!("found too many nested parenthesis in comment") - })?; + depth = depth + .checked_add(1) + .ok_or(E::CommentTooManyNestedParens)?; } } if depth > 0 { - return Err(err!( - "found opening parenthesis in comment with \ - no matching closing parenthesis" - )); + return Err(Error::from(E::CommentOpeningParenWithoutClose)); } let Parsed { input, .. } = self.skip_whitespace(input); Ok(Parsed { value: (), input }) @@ -1414,19 +1292,16 @@ impl DateTimePrinter { offset: Option, mut wtr: W, ) -> Result<(), Error> { - static FMT_DAY: DecimalFormatter = DecimalFormatter::new(); - static FMT_YEAR: DecimalFormatter = DecimalFormatter::new().padding(4); - static FMT_TIME_UNIT: DecimalFormatter = - DecimalFormatter::new().padding(2); + static FMT_DAY: IntegerFormatter = IntegerFormatter::new(); + static FMT_YEAR: IntegerFormatter = IntegerFormatter::new().padding(4); + static FMT_TIME_UNIT: IntegerFormatter = + IntegerFormatter::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(err!( - "datetime {dt} has negative year, \ - which cannot be formatted with RFC 2822", - )); + return Err(Error::from(E::NegativeYear)); } wtr.write_str(weekday_abbrev(dt.weekday()))?; @@ -1474,20 +1349,17 @@ impl DateTimePrinter { timestamp: &Timestamp, mut wtr: W, ) -> Result<(), Error> { - 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); + 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); 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(err!( - "datetime {dt} has negative year, \ - which cannot be formatted with RFC 2822", - )); + return Err(Error::from(E::NegativeYear)); } wtr.write_str(weekday_abbrev(dt.weekday()))?; @@ -1743,7 +1615,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 Thu, but parsed datetime of 2024-01-10T05:34:45 has weekday Wed", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: found parsed weekday of `Thursday`, but parsed datetime has weekday `Wednesday`", ); insta::assert_snapshot!( p("Wed, 29 Feb 2023 05:34:45 -0500"), @@ -1755,11 +1627,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: day is not valid: parameter 'day' with value 32 is not in the required range of 1..=31", + @"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", ); insta::assert_snapshot!( p("Sun, 30 Jun 2024 24:00:00 -0500"), - @"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", + @"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", ); // No whitespace after time insta::assert_snapshot!( @@ -1780,43 +1652,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 whitespace", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected RFC 2822 datetime, but got empty string after trimming leading whitespace", ); insta::assert_snapshot!( p("Wat"), - @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)"###, + @"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"), - @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)"###, + @"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 `Wed` in RFC 2822 datetime, but found end of string instead", + @"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", ); insta::assert_snapshot!( p("Wed ,"), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input", ); insta::assert_snapshot!( p("Wed , "), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric day, but found end of input", ); insta::assert_snapshot!( p("Wat, "), - @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"###, + @"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", ); insta::assert_snapshot!( p("Wed, "), - @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected day, but found end of input", + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected numeric 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 1: 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: expected whitespace after parsing time: 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 10: 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: expected whitespace after parsing time: expected at least one whitespace character (space or tab), but found none", ); insta::assert_snapshot!( p("Wed, 10 J"), @@ -1824,11 +1696,11 @@ mod tests { ); insta::assert_snapshot!( p("Wed, 10 Wat"), - @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize "Wat" as a valid month"###, + @"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected abbreviated month name, but did not recognize a valid abbreviated month name", ); insta::assert_snapshot!( p("Wed, 10 Jan"), - @"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", + @"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", ); insta::assert_snapshot!( p("Wed, 10 Jan 2"), @@ -1836,15 +1708,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 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 whitespace after parsing time: 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"), @@ -1860,7 +1732,7 @@ mod tests { ); insta::assert_snapshot!( p("Wed, 10 Jan 2024 05:34:45 J"), - @r###"failed to parse RFC 2822 datetime into Jiff zoned datetime: expected obsolete RFC 2822 time zone abbreviation, but found "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", ); } @@ -2040,7 +1912,7 @@ mod tests { .at(5, 34, 45, 0) .in_tz("America/New_York") .unwrap(); - insta::assert_snapshot!(p(&zdt), @"datetime -000001-01-10T05:34:45 has negative year, which cannot be formatted with RFC 2822"); + insta::assert_snapshot!(p(&zdt), @"datetime has negative year, which cannot be formatted with RFC 2822"); } #[test] @@ -2062,6 +1934,6 @@ mod tests { .in_tz("America/New_York") .unwrap() .timestamp(); - insta::assert_snapshot!(p(ts), @"datetime -000001-01-10T10:30:47 has negative year, which cannot be formatted with RFC 2822"); + insta::assert_snapshot!(p(ts), @"datetime has negative year, which cannot be formatted with RFC 2822"); } } diff --git a/src/fmt/rfc9557.rs b/src/fmt/rfc9557.rs index 259e92c..0c24148 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::{err, Error}, + error::{fmt::rfc9557::Error as E, Error}, fmt::{ offset::{self, ParsedOffset}, temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind}, Parsed, }, - util::{escape, parse}, + util::parse, }; /// The result of parsing RFC 9557 annotations. @@ -112,11 +112,6 @@ 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 @@ -127,7 +122,7 @@ pub(crate) struct ParsedAnnotations<'i> { impl<'i> ParsedAnnotations<'i> { /// Return an empty parsed annotations. pub(crate) fn none() -> ParsedAnnotations<'static> { - ParsedAnnotations { input: escape::Bytes(&[]), time_zone: None } + ParsedAnnotations { time_zone: None } } /// Turns this parsed time zone into a structured time zone annotation, @@ -212,8 +207,6 @@ 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 { @@ -229,10 +222,7 @@ impl Parser { input = unconsumed; } - let value = ParsedAnnotations { - input: escape::Bytes(mkslice(input)), - time_zone, - }; + let value = ParsedAnnotations { time_zone }; Ok(Parsed { value, input }) } @@ -241,14 +231,18 @@ impl Parser { mut input: &'i [u8], ) -> Result>>, Error> { let unconsumed = input; - if input.is_empty() || input[0] != b'[' { + let Some((&first, tail)) = input.split_first() else { + return Ok(Parsed { value: None, input: unconsumed }); + }; + if first != b'[' { return Ok(Parsed { value: None, input: unconsumed }); } - input = &input[1..]; + input = tail; - let critical = input.starts_with(b"!"); - if critical { - input = &input[1..]; + let mut critical = false; + if let Some(tail) = input.strip_prefix(b"!") { + critical = true; + input = tail; } // If we're starting with a `+` or `-`, then we know we MUST have a @@ -284,8 +278,8 @@ impl Parser { // a generic key/value annotation. return Ok(Parsed { value: None, input: unconsumed }); } - while input.starts_with(b"/") { - input = &input[1..]; + while let Some(tail) = input.strip_prefix(b"/") { + input = tail; let Parsed { input: unconsumed, .. } = self.parse_tz_annotation_iana_name(input)?; input = unconsumed; @@ -306,17 +300,21 @@ impl Parser { &self, mut input: &'i [u8], ) -> Result, Error> { - if input.is_empty() || input[0] != b'[' { + let Some((&first, tail)) = input.split_first() else { + return Ok(Parsed { value: false, input }); + }; + if first != b'[' { return Ok(Parsed { value: false, input }); } - input = &input[1..]; + input = tail; - let critical = input.starts_with(b"!"); - if critical { - input = &input[1..]; + let mut critical = false; + if let Some(tail) = input.strip_prefix(b"!") { + critical = true; + input = tail; } - let Parsed { value: key, input } = self.parse_annotation_key(input)?; + let Parsed { 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)?; @@ -326,11 +324,7 @@ 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(err!( - "found unsupported RFC 9557 annotation with key {key:?} \ - with the critical flag ('!') set", - key = escape::Bytes(key), - )); + return Err(Error::from(E::UnsupportedAnnotationCritical)); } Ok(Parsed { value: true, input }) @@ -381,8 +375,8 @@ impl Parser { input: &'i [u8], ) -> Result, Error> { let Parsed { mut input, .. } = self.parse_annotation_value(input)?; - while input.starts_with(b"-") { - input = &input[1..]; + while let Some(tail) = input.strip_prefix(b"-") { + input = tail; let Parsed { input: unconsumed, .. } = self.parse_annotation_value(input)?; input = unconsumed; @@ -413,173 +407,137 @@ impl Parser { &self, input: &'i [u8], ) -> Result, Error> { - 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", - )); + 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 !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..] }) + Ok(Parsed { value: (), input: tail }) } fn parse_tz_annotation_char<'i>( &self, input: &'i [u8], ) -> Parsed<'i, bool> { - let is_tz_annotation_char = |byte| { - matches!( - byte, - b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', - ) + let Some((&first, tail)) = input.split_first() else { + return Parsed { value: false, input }; }; - if input.is_empty() || !is_tz_annotation_char(input[0]) { + + if !matches!( + first, + b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', + ) { return Parsed { value: false, input }; } - Parsed { value: true, input: &input[1..] } + Parsed { value: true, input: tail } } fn parse_annotation_key_leading_char<'i>( &self, input: &'i [u8], ) -> Result, Error> { - if input.is_empty() { - return Err(err!( - "expected the start of an RFC 9557 annotation key, \ - but found end of input instead", - )); + 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 !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..] }) + Ok(Parsed { value: (), input: tail }) } fn parse_annotation_key_char<'i>( &self, input: &'i [u8], ) -> Parsed<'i, bool> { - 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]) { + 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') { return Parsed { value: false, input }; } - Parsed { value: true, input: &input[1..] } + Parsed { value: true, input: tail } } fn parse_annotation_value_leading_char<'i>( &self, input: &'i [u8], ) -> Result, Error> { - if input.is_empty() { - return Err(err!( - "expected the start of an RFC 9557 annotation value, \ - but found end of input instead", - )); + 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 !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..] }) + Ok(Parsed { value: (), input: tail }) } fn parse_annotation_value_char<'i>( &self, input: &'i [u8], ) -> Parsed<'i, bool> { - 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]) { + 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') { return Parsed { value: false, input }; } - Parsed { value: true, input: &input[1..] } + Parsed { value: true, input: tail } } fn parse_annotation_separator<'i>( &self, input: &'i [u8], ) -> Result, Error> { - 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'=' { + let Some((&first, tail)) = input.split_first() else { + return Err(Error::from(E::EndOfInputAnnotationSeparator)); + }; + if first != 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(if input[0] == b'/' { - err!( - "expected an '=' after parsing an RFC 9557 annotation \ - key, but found / instead (time zone annotations must \ - come first)", - ) + return Err(Error::from(if first == b'/' { + E::UnexpectedSlashAnnotationSeparator } else { - err!( - "expected an '=' after parsing an RFC 9557 annotation \ - key, but found {:?} instead", - escape::Byte(input[0]), - ) - }); + E::UnexpectedByteAnnotationSeparator { byte: first } + })); } - Ok(Parsed { value: (), input: &input[1..] }) + Ok(Parsed { value: (), input: tail }) } fn parse_annotation_close<'i>( &self, input: &'i [u8], ) -> Result, Error> { - if input.is_empty() { - return Err(err!( - "expected an ']' after parsing an RFC 9557 annotation key \ - and value, but found end of input instead", - )); + 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[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..] }) + Ok(Parsed { value: (), input: tail }) } fn parse_tz_annotation_close<'i>( &self, input: &'i [u8], ) -> Result, Error> { - if input.is_empty() { - return Err(err!( - "expected an ']' after parsing an RFC 9557 time zone \ - annotation, but found end of input instead", - )); + 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[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..] }) + Ok(Parsed { value: (), input: tail }) } } @@ -665,24 +623,22 @@ 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] @@ -691,39 +647,36 @@ 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: "", } - "###, + "#, ); } @@ -731,10 +684,9 @@ 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, @@ -744,11 +696,10 @@ 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, @@ -758,11 +709,10 @@ 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, @@ -772,11 +722,10 @@ 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, @@ -786,17 +735,16 @@ 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, @@ -810,11 +758,10 @@ 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, @@ -828,11 +775,10 @@ 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, @@ -846,11 +792,10 @@ 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, @@ -864,7 +809,7 @@ mod tests { }, input: "", } - "###); + "#); } #[test] @@ -873,10 +818,9 @@ 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, @@ -886,7 +830,7 @@ mod tests { }, input: "", } - "###, + "#, ); } @@ -894,11 +838,11 @@ mod tests { fn err_iana() { insta::assert_snapshot!( Parser::new().parse(b"[0/Foo]").unwrap_err(), - @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"###, + @"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(), - @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"###, + @"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", ); } @@ -906,23 +850,23 @@ mod tests { fn err_offset() { insta::assert_snapshot!( Parser::new().parse(b"[+").unwrap_err(), - @r###"failed to parse hours in UTC numeric offset "+": expected two digit hour after sign, but found end of input"###, + @"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(), - @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"###, + @"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", ); insta::assert_snapshot!( Parser::new().parse(b"[-26]").unwrap_err(), - @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"###, + @"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", ); insta::assert_snapshot!( Parser::new().parse(b"[+05:12:34]").unwrap_err(), - @r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###, + @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)", ); insta::assert_snapshot!( Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(), - @r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###, + @"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)", ); } @@ -930,7 +874,7 @@ mod tests { fn err_critical_unsupported() { insta::assert_snapshot!( Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(), - @r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###, + @"found unsupported RFC 9557 annotation with the critical flag (`!`) set", ); } @@ -942,7 +886,7 @@ mod tests { ); insta::assert_snapshot!( Parser::new().parse(b"[&").unwrap_err(), - @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"###, + @"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(), @@ -950,7 +894,7 @@ mod tests { ); insta::assert_snapshot!( Parser::new().parse(b"[Foo][&").unwrap_err(), - @r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###, + @"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found `&` instead", ); } @@ -958,27 +902,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(), - @r###"expected an ']' after parsing an RFC 9557 time zone annotation, but found "^" instead"###, + @"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(), - @r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###, + @"expected an `=` after parsing an RFC 9557 annotation key, but found `^` instead", ); } @@ -994,11 +938,11 @@ mod tests { ); insta::assert_snapshot!( Parser::new().parse(b"[abc=^").unwrap_err(), - @r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "^" instead"###, + @"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(), - @r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###, + @"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `]` instead", ); } @@ -1006,11 +950,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(), - @r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###, + @"expected an `]` after parsing an RFC 9557 annotation key and value, but found `*` instead", ); } @@ -1046,7 +990,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 dc0078f..f8fe871 100644 --- a/src/fmt/serde.rs +++ b/src/fmt/serde.rs @@ -1778,28 +1778,24 @@ pub mod unsigned_duration { fn parse_iso_or_friendly( bytes: &[u8], ) -> Result { - 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 Some((&byte, tail)) = bytes.split_first() else { + return Err(crate::Error::from( + crate::error::fmt::Error::HybridDurationEmpty, )); - } - let mut first = bytes[0]; + }; + 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. if first == b'+' || first == b'-' { - 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), + let Some(&byte) = tail.first() else { + return Err(crate::Error::from( + crate::error::fmt::Error::HybridDurationPrefix { + sign: first, + }, )); - } - first = bytes[1]; + }; + first = byte; } 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 5d5d9a5..74cb1a8 100644 --- a/src/fmt/strtime/format.rs +++ b/src/fmt/strtime/format.rs @@ -1,16 +1,19 @@ use crate::{ - error::{err, ErrorContext}, + error::{ + fmt::strtime::{Error as E, FormatError as FE}, + ErrorContext, + }, fmt::{ strtime::{ month_name_abbrev, month_name_full, weekday_name_abbrev, weekday_name_full, BrokenDownTime, Config, Custom, Extension, Flag, }, - util::{DecimalFormatter, FractionalFormatter}, + util::{FractionalFormatter, IntegerFormatter}, Write, WriteExt, }, tz::Offset, - util::{escape, utf8}, + util::utf8, Error, }; @@ -39,10 +42,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { self.wtr.write_str("%")?; break; } - return Err(err!( - "invalid format string, expected byte after '%', \ - but found end of format string", - )); + return Err(E::UnexpectedEndAfterPercent.into()); } let orig = self.fmt; if let Err(err) = self.format_one() { @@ -61,100 +61,92 @@ 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("%% 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'%' => 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'Q' => match ext.colons { - 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" - )) - } + 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()), }, - 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'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'z' => match ext.colons { - 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" - )) - } + 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()), }, b'.' => { if !self.bump_fmt() { - return Err(err!( - "invalid format string, expected directive after '%.'", - )); + return Err(E::UnexpectedEndAfterDot.into()); } // 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("%.f failed")? - } + b'f' => self + .fmt_dot_fractional(&ext) + .context(E::DirectiveFailureDot { directive: b'f' }), unk => { - return Err(err!( - "found unrecognized directive %{unk} following %.", - unk = escape::Byte(unk), + return Err(Error::from( + E::UnknownDirectiveAfterDot { directive: unk }, )); } } } unk => { - return Err(err!( - "found unrecognized specifier directive %{unk}", - unk = escape::Byte(unk), - )); + return Err(Error::from(E::UnknownDirective { + directive: unk, + })) } - } + }?; self.bump_fmt(); Ok(()) } @@ -200,21 +192,17 @@ 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(errant_bytes) if self.config.lenient => { - self.fmt = &self.fmt[errant_bytes.len()..]; + Err(err) if self.config.lenient => { + self.fmt = &self.fmt[err.len()..]; return Ok(char::REPLACEMENT_CHARACTER); } - 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), - )), + Err(_) => Err(FE::InvalidUtf8), } } @@ -270,11 +258,7 @@ 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_else(|| err!("requires time to format AM/PM"))? - .get(); + let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); ext.write_str( Case::AsIs, if hour < 12 { "am" } else { "pm" }, @@ -284,11 +268,7 @@ 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_else(|| err!("requires time to format AM/PM"))? - .get(); + let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); // Manually specialize this case to avoid hitting `write_str_cold`. let s = if matches!(ext.flag, Some(Flag::Swapcase)) { if hour < 12 { @@ -339,8 +319,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let day = self .tm .day - .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged())) - .ok_or_else(|| err!("requires date to format day"))? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_ranged()), + ) + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(2), day, self.wtr) } @@ -350,19 +333,18 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let day = self .tm .day - .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged())) - .ok_or_else(|| err!("requires date to format day"))? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_ranged()), + ) + .ok_or(FE::RequiresDate)? .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_else(|| err!("requires time to format hour"))? - .get(); + let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); if hour == 0 { hour = 12; } else if hour > 12 { @@ -373,21 +355,13 @@ 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_else(|| err!("requires time to format hour"))? - .get(); + let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.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_else(|| err!("requires time to format hour"))? - .get(); + let mut hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); if hour == 0 { hour = 12; } else if hour > 12 { @@ -398,11 +372,7 @@ 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_else(|| err!("requires time to format hour"))? - .get(); + let hour = self.tm.hour_ranged().ok_or(FE::RequiresTime)?.get(); ext.write_int(b' ', Some(2), hour, self.wtr) } @@ -418,11 +388,7 @@ 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_else(|| err!("requires time to format minute"))? - .get(); + let minute = self.tm.minute.ok_or(FE::RequiresTime)?.get(); ext.write_int(b'0', Some(2), minute, self.wtr) } @@ -431,8 +397,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) - .ok_or_else(|| err!("requires date to format month"))? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(2), month, self.wtr) } @@ -442,8 +411,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) - .ok_or_else(|| err!("requires date to format month"))?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) + .ok_or(FE::RequiresDate)?; ext.write_str(Case::AsIs, month_name_full(month), self.wtr) } @@ -452,20 +424,18 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) - .ok_or_else(|| err!("requires date to format month"))?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) + .ok_or(FE::RequiresDate)?; 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_else(|| { - err!( - "requires IANA time zone identifier or time \ - zone offset, but none were present" - ) - })?; + let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?; return write_offset(offset, false, true, false, &mut self.wtr); }; self.wtr.write_str(iana)?; @@ -475,12 +445,7 @@ 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_else(|| { - err!( - "requires IANA time zone identifier or time \ - zone offset, but none were present" - ) - })?; + let offset = self.tm.offset.ok_or(FE::RequiresTimeZoneOrOffset)?; return write_offset(offset, true, true, false, &mut self.wtr); }; self.wtr.write_str(iana)?; @@ -489,62 +454,44 @@ 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_else(|| { - err!("requires offset to format time zone offset") - })?; + let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; write_offset(offset, false, true, false, self.wtr) } /// %:z fn fmt_offset_colon(&mut self) -> Result<(), Error> { - let offset = self.tm.offset.ok_or_else(|| { - err!("requires offset to format time zone offset") - })?; + let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; write_offset(offset, true, true, false, self.wtr) } /// %::z fn fmt_offset_colon2(&mut self) -> Result<(), Error> { - let offset = self.tm.offset.ok_or_else(|| { - err!("requires offset to format time zone offset") - })?; + let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; write_offset(offset, true, true, true, self.wtr) } /// %:::z fn fmt_offset_colon3(&mut self) -> Result<(), Error> { - let offset = self.tm.offset.ok_or_else(|| { - err!("requires offset to format time zone offset") - })?; + let offset = self.tm.offset.ok_or(FE::RequiresOffset)?; 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_else(|| err!("requires time to format second"))? - .get(); + let second = self.tm.second.ok_or(FE::RequiresTime)?.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(|_| { - err!( - "requires instant (a date, time and offset) \ - to format Unix timestamp", - ) - })?; + let timestamp = + self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?; 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_else(|| { - err!("requires time to format subsecond nanoseconds") - })?; + let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?; 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 @@ -553,7 +500,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(err!("zero precision with %f is not allowed")); + return Err(Error::from(FE::ZeroPrecisionFloat)); } if subsec == 0 && ext.width.is_none() { self.wtr.write_str("0")?; @@ -577,11 +524,9 @@ 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_else(|| { - err!("requires time to format subsecond nanoseconds") - })?; + let subsec = self.tm.subsec.ok_or(FE::RequiresTime)?; if ext.width == Some(0) { - return Err(err!("zero precision with %N is not allowed")); + return Err(Error::from(FE::ZeroPrecisionNano)); } let subsec = i32::from(subsec).unsigned_abs(); // Since `%N` is actually an alias for `%9f`, when the precision @@ -596,14 +541,8 @@ 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_else(|| { - err!("requires time zone in broken down time") - })?; - let ts = self - .tm - .to_timestamp() - .context("requires timestamp in broken down time")?; + let tz = self.tm.tz.as_ref().ok_or(FE::RequiresTimeZone)?; + let ts = self.tm.to_timestamp().map_err(|_| FE::RequiresInstant)?; let oinfo = tz.to_offset_info(ts); ext.write_str(Case::Upper, oinfo.abbreviation(), self.wtr) } @@ -613,8 +552,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) - .ok_or_else(|| err!("requires date to format weekday"))?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) + .ok_or(FE::RequiresDate)?; ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr) } @@ -623,8 +565,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) - .ok_or_else(|| err!("requires date to format weekday"))?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) + .ok_or(FE::RequiresDate)?; ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr) } @@ -633,8 +578,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) - .ok_or_else(|| err!("requires date to format weekday number"))?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) + .ok_or(FE::RequiresDate)?; ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr) } @@ -643,8 +591,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) - .ok_or_else(|| err!("requires date to format weekday number"))?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) + .ok_or(FE::RequiresDate)?; ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr) } @@ -658,17 +609,19 @@ 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(|| self.tm.to_date().ok().map(|d| d.day_of_year())) - .ok_or_else(|| { - err!("requires date to format Sunday-based week number") - })?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_of_year()), + ) + .ok_or(FE::RequiresDate)?; let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) - .ok_or_else(|| { - err!("requires date to format Sunday-based week number") - })? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) + .ok_or(FE::RequiresDate)? .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. @@ -684,12 +637,16 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let weeknum = self .tm .iso_week - .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") - })?; + .or_else( + #[inline(never)] + || { + self.tm + .to_date() + .ok() + .map(|d| d.iso_week_date().week_ranged()) + }, + ) + .ok_or(FE::RequiresDate)?; ext.write_int(b'0', Some(2), weeknum, self.wtr) } @@ -703,17 +660,19 @@ 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(|| self.tm.to_date().ok().map(|d| d.day_of_year())) - .ok_or_else(|| { - err!("requires date to format Monday-based week number") - })?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_of_year()), + ) + .ok_or(FE::RequiresDate)?; let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) - .ok_or_else(|| { - err!("requires date to format Monday-based week number") - })? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) + .ok_or(FE::RequiresDate)? .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. @@ -729,8 +688,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .year - .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) - .ok_or_else(|| err!("requires date to format year"))? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.year_ranged()), + ) + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(4), year, self.wtr) } @@ -740,8 +702,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .year - .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) - .ok_or_else(|| err!("requires date to format year (2-digit)"))? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.year_ranged()), + ) + .ok_or(FE::RequiresDate)? .get(); let year = year % 100; ext.write_int(b'0', Some(2), year, self.wtr) @@ -752,8 +717,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .year - .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged())) - .ok_or_else(|| err!("requires date to format century (2-digit)"))? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.year_ranged()), + ) + .ok_or(FE::RequiresDate)? .get(); let century = year / 100; ext.write_int(b' ', None, century, self.wtr) @@ -764,12 +732,16 @@ 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(|| { - 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") - })? + .or_else( + #[inline(never)] + || { + self.tm + .to_date() + .ok() + .map(|d| d.iso_week_date().year_ranged()) + }, + ) + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(4), year, self.wtr) } @@ -779,15 +751,16 @@ 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(|| { - 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)" - ) - })? + .or_else( + #[inline(never)] + || { + self.tm + .to_date() + .ok() + .map(|d| d.iso_week_date().year_ranged()) + }, + ) + .ok_or(FE::RequiresDate)? .get(); let year = year % 100; ext.write_int(b'0', Some(2), year, self.wtr) @@ -798,8 +771,11 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let month = self .tm .month - .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged())) - .ok_or_else(|| err!("requires date to format quarter"))? + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) + .ok_or(FE::RequiresDate)? .get(); let quarter = match month { 1..=3 => 1, @@ -817,8 +793,11 @@ 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(|| self.tm.to_date().ok().map(|d| d.day_of_year())) - .ok_or_else(|| err!("requires date to format day of year"))?; + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_of_year()), + ) + .ok_or(FE::RequiresDate)?; ext.write_int(b'0', Some(3), day, self.wtr) } @@ -871,7 +850,7 @@ fn write_offset( second: bool, wtr: &mut W, ) -> Result<(), Error> { - static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); + static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); let hours = offset.part_hours_ranged().abs().get(); let minutes = offset.part_minutes_ranged().abs().get(); @@ -967,7 +946,7 @@ impl Extension { self.width.or(pad_width) }; - let mut formatter = DecimalFormatter::new().padding_byte(pad_byte); + let mut formatter = IntegerFormatter::new().padding_byte(pad_byte); if let Some(width) = pad_width { formatter = formatter.padding(width); } @@ -1508,7 +1487,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 date, time and offset) to format Unix timestamp", + @"strftime formatting failed: %s failed: requires instant (a timestamp or a date, time and offset)", ); } @@ -1521,7 +1500,7 @@ mod tests { ); insta::assert_snapshot!( format(b"abc %F \xFFxyz", d).unwrap_err(), - @r#"strftime formatting failed: found invalid UTF-8 byte "\xff" in format string (format strings must be valid UTF-8)"#, + @"strftime formatting failed: invalid format string, it must be valid UTF-8", ); } diff --git a/src/fmt/strtime/mod.rs b/src/fmt/strtime/mod.rs index 9455dd0..6a2d1f6 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 year", + "strftime formatting failed: %Y failed: requires date to format", ); ``` @@ -275,7 +275,7 @@ The following things are currently unsupported: use crate::{ civil::{Date, DateTime, ISOWeekDate, Time, Weekday}, - error::{err, ErrorContext}, + error::{fmt::strtime::Error as E, 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 offset to format time zone offset", + /// requires time zone offset", /// ); /// /// // Now enable lenient mode: @@ -946,13 +946,9 @@ 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("strptime parsing failed")?; + p.parse().context(E::FailedStrptime)?; if !p.inp.is_empty() { - return Err(err!( - "strptime expects to consume the entire input, but \ - {remaining:?} remains unparsed", - remaining = escape::Bytes(p.inp), - )); + return Err(Error::from(E::unconsumed(p.inp))); } Ok(pieces) } @@ -1055,7 +1051,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("strptime parsing failed")?; + p.parse().context(E::FailedStrptime)?; let remainder = mkoffset(p.inp); Ok((pieces, remainder)) } @@ -1158,7 +1154,7 @@ impl BrokenDownTime { ) -> Result<(), Error> { let fmt = format.as_ref(); let mut formatter = Formatter { config, fmt, tm: self, wtr }; - formatter.format().context("strftime formatting failed")?; + formatter.format().context(E::FailedStrftime)?; Ok(()) } @@ -1337,10 +1333,11 @@ impl BrokenDownTime { /// )?.to_zoned(); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "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", + /// "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`", /// ); /// /// # Ok::<(), Box>(()) @@ -1435,26 +1432,18 @@ impl BrokenDownTime { if let Some(ts) = self.timestamp { return Ok(ts.to_zoned(TimeZone::unknown())); } - Err(err!( - "either offset (from %z) or IANA time zone identifier \ - (from %Q) is required for parsing zoned datetime", - )) + Err(Error::from(E::ZonedOffsetOrTz)) } (Some(offset), None) => { let ts = match self.timestamp { Some(ts) => ts, None => { - 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", - ) - })?; + let dt = self + .to_datetime() + .context(E::RequiredDateTimeForZoned)?; + let ts = offset + .to_timestamp(dt) + .context(E::RangeTimestamp)?; ts } }; @@ -1465,9 +1454,9 @@ impl BrokenDownTime { match self.timestamp { Some(ts) => Ok(ts.to_zoned(tz)), None => { - let dt = self.to_datetime().context( - "datetime required to parse zoned datetime", - )?; + let dt = self + .to_datetime() + .context(E::RequiredDateTimeForZoned)?; Ok(tz.to_zoned(dt)?) } } @@ -1478,19 +1467,17 @@ impl BrokenDownTime { Some(ts) => { let zdt = ts.to_zoned(tz); if zdt.offset() != offset { - return Err(err!( - "parsed time zone offset `{offset}`, but \ - offset from timestamp `{ts}` for time zone \ - `{iana}` is `{got}`", - got = zdt.offset(), - )); + return Err(Error::from(E::MismatchOffset { + parsed: offset, + got: zdt.offset(), + })); } Ok(zdt) } None => { - let dt = self.to_datetime().context( - "datetime required to parse zoned datetime", - )?; + let dt = self + .to_datetime() + .context(E::RequiredDateTimeForZoned)?; let azdt = OffsetConflict::Reject.resolve(dt, offset, tz)?; // Guaranteed that if OffsetConflict::Reject doesn't @@ -1584,31 +1571,20 @@ 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("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) + let dt = + self.to_datetime().context(E::RequiredDateTimeForTimestamp)?; + let offset = self.offset.ok_or(E::RequiredOffsetForTimestamp)?; + offset.to_timestamp(dt).context(E::RangeTimestamp) } /// Extracts a civil datetime from this broken down time. @@ -1638,10 +1614,8 @@ impl BrokenDownTime { /// ``` #[inline] pub fn to_datetime(&self) -> Result { - let date = - self.to_date().context("date required to parse datetime")?; - let time = - self.to_time().context("time required to parse datetime")?; + let date = self.to_date().context(E::RequiredDateForDateTime)?; + let time = self.to_time().context(E::RequiredTimeForDateTime)?; Ok(DateTime::from_parts(date, time)) } @@ -1660,8 +1634,12 @@ impl BrokenDownTime { /// # Errors /// /// This returns an error if there weren't enough components to construct - /// a civil date. This means there must be at least a year and a way to - /// determine the day of the year. + /// 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. /// /// 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, @@ -1681,42 +1659,62 @@ impl BrokenDownTime { /// ``` #[inline] pub fn to_date(&self) -> Result { - 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); + #[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()?; } - 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", - )); + 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); }; + let date = + Date::new_ranged(year, month, day).context(E::InvalidDate)?; if let Some(weekday) = self.weekday { if weekday != 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()), - )); + return Err(Error::from(E::MismatchWeekday { + parsed: weekday, + got: date.weekday(), + })); } } Ok(date) @@ -1730,7 +1728,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("invalid date")?)) + Ok(Some(Date::new_ranged(year, month, day).context(E::InvalidDate)?)) } #[inline] @@ -1746,7 +1744,7 @@ impl BrokenDownTime { .with() .day_of_year(doy.get()) .build() - .context("invalid date")? + .context(E::InvalidDate)? })) } @@ -1757,8 +1755,8 @@ impl BrokenDownTime { else { return Ok(None); }; - let wd = ISOWeekDate::new_ranged(y, w, d) - .context("invalid ISO 8601 week date")?; + let wd = + ISOWeekDate::new_ranged(y, w, d).context(E::InvalidISOWeekDate)?; Ok(Some(wd.date())) } @@ -1773,28 +1771,20 @@ 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("invalid date")?; + .context(E::InvalidDate)?; let first_sunday = first_of_year .nth_weekday_of_month(1, Weekday::Sunday) .map(|d| d.day_of_year()) - .context("invalid date")?; + .context(E::InvalidDate)?; let doy = if week == 0 { let days_before_first_sunday = 7 - wday; let doy = first_sunday .checked_sub(days_before_first_sunday) - .ok_or_else(|| { - err!( - "weekday `{weekday:?}` is not valid for \ - Sunday based week number `{week}` \ - in year `{year}`", - ) - })?; + .ok_or(E::InvalidWeekdaySunday { got: weekday })?; if doy == 0 { - return Err(err!( - "weekday `{weekday:?}` is not valid for \ - Sunday based week number `{week}` \ - in year `{year}`", - )); + return Err(Error::from(E::InvalidWeekdaySunday { + got: weekday, + })); } doy } else { @@ -1806,7 +1796,7 @@ impl BrokenDownTime { .with() .day_of_year(doy) .build() - .context("invalid date")?; + .context(E::InvalidDate)?; Ok(Some(date)) } @@ -1821,28 +1811,20 @@ 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("invalid date")?; + .context(E::InvalidDate)?; let first_monday = first_of_year .nth_weekday_of_month(1, Weekday::Monday) .map(|d| d.day_of_year()) - .context("invalid date")?; + .context(E::InvalidDate)?; let doy = if week == 0 { let days_before_first_monday = 7 - wday; let doy = first_monday .checked_sub(days_before_first_monday) - .ok_or_else(|| { - err!( - "weekday `{weekday:?}` is not valid for \ - Monday based week number `{week}` \ - in year `{year}`", - ) - })?; + .ok_or(E::InvalidWeekdayMonday { got: weekday })?; if doy == 0 { - return Err(err!( - "weekday `{weekday:?}` is not valid for \ - Monday based week number `{week}` \ - in year `{year}`", - )); + return Err(Error::from(E::InvalidWeekdayMonday { + got: weekday, + })); } doy } else { @@ -1854,7 +1836,7 @@ impl BrokenDownTime { .with() .day_of_year(doy) .build() - .context("invalid date")?; + .context(E::InvalidDate)?; Ok(Some(date)) } @@ -1936,52 +1918,28 @@ impl BrokenDownTime { pub fn to_time(&self) -> Result { let Some(hour) = self.hour_ranged() else { if self.minute.is_some() { - 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)", - )); + return Err(Error::from(E::MissingTimeHourForMinute)); } if self.second.is_some() { - 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)", - )); + return Err(Error::from(E::MissingTimeHourForSecond)); } if self.subsec.is_some() { - 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 Err(Error::from(E::MissingTimeHourForFractional)); } return Ok(Time::midnight()); }; let Some(minute) = self.minute else { if self.second.is_some() { - 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)", - )); + return Err(Error::from(E::MissingTimeMinuteForSecond)); } if self.subsec.is_some() { - 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 Err(Error::from(E::MissingTimeMinuteForFractional)); } 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(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 Err(Error::from(E::MissingTimeSecondForFractional)); } return Ok(Time::new_ranged(hour, minute, C(0), C(0))); }; @@ -2121,8 +2079,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: day-of-year=366 is out of range \ - /// for year=2023, must be in range 1..=365", + /// "invalid date: number of days for `2023` is invalid, \ + /// must be in range `1..=365`", /// ); /// // But parsing a value that is always illegal will /// // result in an error: @@ -3479,7 +3437,7 @@ impl Extension { fn parse_flag<'i>( fmt: &'i [u8], ) -> Result<(Option, &'i [u8]), Error> { - let byte = fmt[0]; + let (&byte, tail) = fmt.split_first().unwrap(); let flag = match byte { b'_' => Flag::PadSpace, b'0' => Flag::PadZero, @@ -3488,15 +3446,12 @@ impl Extension { b'#' => Flag::Swapcase, _ => return Ok((None, fmt)), }; - 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), - )); + if tail.is_empty() { + return Err(Error::from(E::ExpectedDirectiveAfterFlag { + flag: byte, + })); } - Ok((Some(flag), fmt)) + Ok((Some(flag), tail)) } /// Parses an optional width that comes after a (possibly absent) flag and @@ -3520,16 +3475,10 @@ impl Extension { return Ok((None, fmt)); } let (digits, fmt) = util::parse::split(fmt, digits).unwrap(); - 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) - })?; + let width = util::parse::i64(digits).context(E::FailedWidth)?; + let width = u8::try_from(width).map_err(|_| E::RangeWidth)?; if fmt.is_empty() { - return Err(err!( - "expected to find specifier directive after width \ - {width}, but found end of format string", - )); + return Err(Error::from(E::ExpectedDirectiveAfterWidth)); } Ok((Some(width), fmt)) } @@ -3549,10 +3498,7 @@ impl Extension { } let fmt = &fmt[usize::from(colons)..]; if colons > 0 && fmt.is_empty() { - return Err(err!( - "expected to find specifier directive after {colons} colons, \ - but found end of format string", - )); + return Err(Error::from(E::ExpectedDirectiveAfterColons)); } Ok((u8::try_from(colons).unwrap(), fmt)) } diff --git a/src/fmt/strtime/parse.rs b/src/fmt/strtime/parse.rs index cecaeff..a66dc0d 100644 --- a/src/fmt/strtime/parse.rs +++ b/src/fmt/strtime/parse.rs @@ -1,15 +1,17 @@ -use core::fmt::Write; - use crate::{ civil::Weekday, - error::{err, ErrorContext}, + error::{ + fmt::strtime::{Error as E, ParseError as PE}, + util::ParseIntError, + ErrorContext, + }, fmt::{ offset, strtime::{BrokenDownTime, Extension, Flag, Meridiem}, Parsed, }, util::{ - escape, parse, + parse, rangeint::{ri8, RFrom}, t::{self, C}, }, @@ -24,109 +26,97 @@ 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(err!( - "invalid format string, expected byte after '%', \ - but found end of format string", - )); + return Err(Error::from(E::UnexpectedEndAfterPercent)); } // 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(err!( - "expected non-empty input for directive %{directive}, \ - but found end of input", - directive = escape::Byte(self.f()), - )); + return Err(Error::from(PE::ExpectedNonEmpty { + directive: self.f(), + })); } // Parse extensions like padding/case options and padding width. let ext = self.parse_extension()?; match self.f() { - 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'%' => 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'Q' => match ext.colons { - 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" - )) - } + 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()), }, - 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'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'z' => match ext.colons { - 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" - )) - } + 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()), }, b'c' => { - return Err(err!("cannot parse locale date and time")); + return Err(Error::from(PE::NotAllowedLocaleDateAndTime)) } b'r' => { - return Err(err!( - "cannot parse locale 12-hour clock time" - )); + return Err(Error::from( + PE::NotAllowedLocaleTwelveHourClockTime, + )) } b'X' => { - return Err(err!("cannot parse locale clock time")); - } - b'x' => { - return Err(err!("cannot parse locale date")); + return Err(Error::from(PE::NotAllowedLocaleClockTime)) } + b'x' => return Err(Error::from(PE::NotAllowedLocaleDate)), b'Z' => { - return Err(err!("cannot parse time zone abbreviations")); + return Err(Error::from( + PE::NotAllowedTimeZoneAbbreviation, + )) } b'.' => { if !self.bump_fmt() { - return Err(err!( - "invalid format string, expected directive \ - after '%.'", - )); + return Err(E::UnexpectedEndAfterDot.into()); } // Skip over any precision settings that might be here. // This is a specific special format supported by `%.f`. @@ -134,23 +124,20 @@ 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("%.f failed")?, + b'f' => self.parse_dot_fractional(ext).context( + E::DirectiveFailureDot { directive: b'f' }, + )?, unk => { - return Err(err!( - "found unrecognized directive %{unk} \ - following %.", - unk = escape::Byte(unk), + return Err(Error::from( + E::UnknownDirectiveAfterDot { directive: unk }, )); } } } unk => { - return Err(err!( - "found unrecognized directive %{unk}", - unk = escape::Byte(unk), - )); + return Err(Error::from(E::UnknownDirective { + directive: unk, + })); } } } @@ -221,18 +208,14 @@ 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(err!( - "expected to match literal byte {byte:?} from \ - format string, but found end of input", - byte = escape::Byte(self.fmt[0]), - )); + return Err(Error::from(PE::ExpectedMatchLiteralEndOfInput { + expected: self.f(), + })); } else if self.f() != 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()), - )); + return Err(Error::from(PE::ExpectedMatchLiteralByte { + expected: self.fmt[0], + got: self.i(), + })); } else { self.bump_input(); } @@ -254,11 +237,10 @@ 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(err!( - "expected '%' due to '%%' in format string, \ - but found {byte:?} in input", - byte = escape::Byte(self.inp[0]), - )); + return Err(Error::from(PE::ExpectedMatchLiteralByte { + expected: b'%', + got: self.i(), + })); } self.bump_fmt(); self.bump_input(); @@ -287,7 +269,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { 0 => Meridiem::AM, 1 => Meridiem::PM, // OK because 0 <= index <= 1. - index => unreachable!("unknown AM/PM index {index}"), + _ => unreachable!("unknown AM/PM index"), }); self.bump_fmt(); Ok(()) @@ -317,11 +299,10 @@ 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("failed to parse day")?; + .context(PE::ParseDay)?; self.inp = inp; - let day = - t::Day::try_new("day", day).context("day number is invalid")?; + let day = t::Day::try_new("day", day).context(PE::ParseDay)?; self.tm.day = Some(day); self.bump_fmt(); Ok(()) @@ -333,11 +314,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("failed to parse day of year")?; + .context(PE::ParseDayOfYear)?; self.inp = inp; let day = t::DayOfYear::try_new("day-of-year", day) - .context("day of year number is invalid")?; + .context(PE::ParseDayOfYear)?; self.tm.day_of_year = Some(day); self.bump_fmt(); Ok(()) @@ -347,11 +328,10 @@ 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("failed to parse hour")?; + .context(PE::ParseHour)?; self.inp = inp; - let hour = t::Hour::try_new("hour", hour) - .context("hour number is invalid")?; + let hour = t::Hour::try_new("hour", hour).context(PE::ParseHour)?; self.tm.hour = Some(hour); self.bump_fmt(); Ok(()) @@ -363,11 +343,10 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (hour, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context("failed to parse hour")?; + .context(PE::ParseHour)?; self.inp = inp; - let hour = - Hour12::try_new("hour", hour).context("hour number is invalid")?; + let hour = Hour12::try_new("hour", hour).context(PE::ParseHour)?; self.tm.hour = Some(t::Hour::rfrom(hour)); self.bump_fmt(); Ok(()) @@ -386,11 +365,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("failed to parse minute")?; + .context(PE::ParseMinute)?; self.inp = inp; - let minute = t::Minute::try_new("minute", minute) - .context("minute number is invalid")?; + let minute = + t::Minute::try_new("minute", minute).context(PE::ParseMinute)?; self.tm.minute = Some(minute); self.bump_fmt(); Ok(()) @@ -401,9 +380,10 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_iana_nocolon(&mut self) -> Result<(), Error> { #[cfg(not(feature = "alloc"))] { - Err(err!( - "cannot parse `%Q` without Jiff's `alloc` feature enabled" - )) + Err(Error::from(PE::NotAllowedAlloc { + directive: b'Q', + colons: 0, + })) } #[cfg(feature = "alloc")] { @@ -425,9 +405,10 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { fn parse_iana_colon(&mut self) -> Result<(), Error> { #[cfg(not(feature = "alloc"))] { - Err(err!( - "cannot parse `%:Q` without Jiff's `alloc` feature enabled" - )) + Err(Error::from(PE::NotAllowedAlloc { + directive: b'Q', + colons: 1, + })) } #[cfg(feature = "alloc")] { @@ -523,7 +504,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("failed to parse second")?; + .context(PE::ParseSecond)?; self.inp = inp; // As with other parses in Jiff, and like Temporal, @@ -532,8 +513,8 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { if second == 60 { second = 59; } - let second = t::Second::try_new("second", second) - .context("second number is invalid")?; + let second = + t::Second::try_new("second", second).context(PE::ParseSecond)?; self.tm.second = Some(second); self.bump_fmt(); Ok(()) @@ -545,23 +526,14 @@ 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("failed to parse Unix timestamp (in seconds)")?; + .context(PE::ParseTimestamp)?; // 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::from_second(timestamp).with_context(|| { - err!( - "parsed Unix timestamp `{timestamp}`, \ - but out of range of valid Jiff `Timestamp`", - ) - })?; + timestamp.checked_mul(sign).ok_or(PE::ParseTimestamp)?; + let timestamp = + Timestamp::from_second(timestamp).context(PE::ParseTimestamp)?; self.inp = inp; self.tm.timestamp = Some(timestamp); @@ -585,29 +557,20 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { } let digits = mkdigits(self.inp); if digits.is_empty() { - return Err(err!( - "expected at least one fractional decimal digit, \ - but did not find any", - )); + return Err(Error::from(PE::ExpectedFractionalDigit)); } // 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).map_err(|err| { - err!( - "failed to parse {digits:?} as fractional second component \ - (up to 9 digits, nanosecond precision): {err}", - digits = escape::Bytes(digits), - ) - })?; + let nanoseconds = + parse::fraction(digits).context(PE::ParseFractionalSeconds)?; // 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).map_err( - |err| err!("fractional nanoseconds are not valid: {err}"), - )?; + t::SubsecNanosecond::try_new("nanoseconds", nanoseconds) + .context(PE::ParseFractionalSeconds)?; self.tm.subsec = Some(nanoseconds); self.bump_fmt(); Ok(()) @@ -629,11 +592,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("failed to parse month")?; + .context(PE::ParseMonth)?; self.inp = inp; - let month = t::Month::try_new("month", month) - .context("month number is invalid")?; + let month = + t::Month::try_new("month", month).context(PE::ParseMonth)?; self.tm.month = Some(month); self.bump_fmt(); Ok(()) @@ -668,8 +631,8 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { b"December", ]; - let (index, inp) = parse_choice(self.inp, CHOICES) - .context("unrecognized month name")?; + let (index, inp) = + parse_choice(self.inp, CHOICES).context(PE::UnknownMonthName)?; self.inp = inp; // Both are OK because 0 <= index <= 11. @@ -705,7 +668,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { ]; let (index, inp) = parse_choice(self.inp, CHOICES) - .context("unrecognized weekday abbreviation")?; + .context(PE::UnknownWeekdayAbbreviation)?; self.inp = inp; // Both are OK because 0 <= index <= 6. @@ -721,14 +684,13 @@ 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("failed to parse weekday number")?; + .context(PE::ParseWeekdayNumber)?; self.inp = inp; - let weekday = i8::try_from(weekday).map_err(|_| { - err!("parsed weekday number `{weekday}` is invalid") - })?; + let weekday = + i8::try_from(weekday).map_err(|_| PE::ParseWeekdayNumber)?; let weekday = Weekday::from_monday_one_offset(weekday) - .context("weekday number is invalid")?; + .context(PE::ParseWeekdayNumber)?; self.tm.weekday = Some(weekday); self.bump_fmt(); Ok(()) @@ -738,14 +700,13 @@ 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("failed to parse weekday number")?; + .context(PE::ParseWeekdayNumber)?; self.inp = inp; - let weekday = i8::try_from(weekday).map_err(|_| { - err!("parsed weekday number `{weekday}` is invalid") - })?; + let weekday = + i8::try_from(weekday).map_err(|_| PE::ParseWeekdayNumber)?; let weekday = Weekday::from_sunday_zero_offset(weekday) - .context("weekday number is invalid")?; + .context(PE::ParseWeekdayNumber)?; self.tm.weekday = Some(weekday); self.bump_fmt(); Ok(()) @@ -756,11 +717,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("failed to parse Sunday-based week number")?; + .context(PE::ParseSundayWeekNumber)?; self.inp = inp; let week = t::WeekNum::try_new("week", week) - .context("Sunday-based week number is invalid")?; + .context(PE::ParseSundayWeekNumber)?; self.tm.week_sun = Some(week); self.bump_fmt(); Ok(()) @@ -770,11 +731,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("failed to parse ISO 8601 week number")?; + .context(PE::ParseIsoWeekNumber)?; self.inp = inp; let week = t::ISOWeek::try_new("week", week) - .context("ISO 8601 week number is invalid")?; + .context(PE::ParseIsoWeekNumber)?; self.tm.iso_week = Some(week); self.bump_fmt(); Ok(()) @@ -785,11 +746,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("failed to parse Monday-based week number")?; + .context(PE::ParseMondayWeekNumber)?; self.inp = inp; let week = t::WeekNum::try_new("week", week) - .context("Monday-based week number is invalid")?; + .context(PE::ParseMondayWeekNumber)?; self.tm.week_mon = Some(week); self.bump_fmt(); Ok(()) @@ -798,16 +759,14 @@ 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("failed to parse year")?; + let (year, inp) = + ext.parse_number(4, Flag::PadZero, inp).context(PE::ParseYear)?; 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("year number is invalid")?; + let year = t::Year::try_new("year", year).context(PE::ParseYear)?; self.tm.year = Some(year); self.bump_fmt(); Ok(()) @@ -821,11 +780,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (year, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context("failed to parse 2-digit year")?; + .context(PE::ParseYearTwoDigit)?; self.inp = inp; let year = Year2Digit::try_new("year (2 digits)", year) - .context("year number is invalid")?; + .context(PE::ParseYearTwoDigit)?; let mut year = t::Year::rfrom(year); if year <= C(68) { year += C(2000); @@ -841,15 +800,12 @@ 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("failed to parse century")?; + let (century, inp) = + ext.parse_number(2, Flag::NoPad, inp).context(PE::ParseCentury)?; self.inp = inp; if !(0 <= century && century <= 99) { - return Err(err!( - "century `{century}` is too big, must be in range 0-99", - )); + return Err(Error::range("century", century, 0, 99)); } // OK because sign=={1,-1} and century can't be bigger than 2 digits @@ -859,8 +815,7 @@ 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("year number (from century) is invalid")?; + let year = t::Year::try_new("year", year).context(PE::ParseCentury)?; self.tm.year = Some(year); self.bump_fmt(); Ok(()) @@ -871,14 +826,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("failed to parse ISO 8601 week-based year")?; + .context(PE::ParseIsoWeekYear)?; 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("ISO 8601 week-based year number is invalid")?; + let year = + t::ISOYear::try_new("year", year).context(PE::ParseIsoWeekYear)?; self.tm.iso_week_year = Some(year); self.bump_fmt(); Ok(()) @@ -892,11 +847,11 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> { let (year, inp) = ext .parse_number(2, Flag::PadZero, self.inp) - .context("failed to parse 2-digit ISO 8601 week-based year")?; + .context(PE::ParseIsoWeekYearTwoDigit)?; self.inp = inp; let year = Year2Digit::try_new("year (2 digits)", year) - .context("ISO 8601 week-based year number is invalid")?; + .context(PE::ParseIsoWeekYearTwoDigit)?; let mut year = t::ISOYear::rfrom(year); if year <= C(68) { year += C(2000); @@ -964,15 +919,10 @@ impl Extension { 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", - escape::Bytes(&inp[..digits]), - ) - })?; + .ok_or(ParseIntError::TooBig)?; } if digits == 0 { - return Err(err!("invalid number, no digits found")); + return Err(Error::from(ParseIntError::NoDigitsFound)); } Ok((n, &inp[digits..])) } @@ -1003,7 +953,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 [u8]], + choices: &'static [&'static [u8]], ) -> Result<(usize, &'i [u8]), Error> { for (i, choice) in choices.into_iter().enumerate() { if input.len() < choice.len() { @@ -1014,27 +964,7 @@ fn parse_choice<'i>( return Ok((i, input)); } } - #[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" - )) - } + Err(Error::from(PE::ExpectedChoice { available: choices })) } /// Like `parse_choice`, but specialized for AM/PM. @@ -1044,25 +974,14 @@ 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(err!( - "expected to find AM or PM, \ - but the remaining input, {input:?}, is too short \ - to contain one", - input = escape::Bytes(input), - )); + return Err(Error::from(PE::ExpectedAmPmTooShort)); } 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(err!( - "expected to find AM or PM, but found \ - {candidate:?} instead", - candidate = escape::Bytes(x), - )) - } + _ => return Err(Error::from(PE::ExpectedAmPm)), }; Ok((index, input)) } @@ -1076,12 +995,7 @@ fn parse_weekday_abbrev<'i>( input: &'i [u8], ) -> Result<(usize, &'i [u8]), Error> { if input.len() < 3 { - return Err(err!( - "expected to find a weekday abbreviation, \ - but the remaining input, {input:?}, is too short \ - to contain one", - input = escape::Bytes(input), - )); + return Err(Error::from(PE::ExpectedWeekdayAbbreviationTooShort)); } let (x, input) = input.split_at(3); let candidate = &[ @@ -1097,13 +1011,7 @@ fn parse_weekday_abbrev<'i>( b"thu" => 4, b"fri" => 5, b"sat" => 6, - _ => { - return Err(err!( - "expected to find weekday abbreviation, but found \ - {candidate:?} instead", - candidate = escape::Bytes(x), - )) - } + _ => return Err(Error::from(PE::ExpectedWeekdayAbbreviation)), }; Ok((index, input)) } @@ -1117,12 +1025,7 @@ fn parse_month_name_abbrev<'i>( input: &'i [u8], ) -> Result<(usize, &'i [u8]), Error> { if input.len() < 3 { - 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), - )); + return Err(Error::from(PE::ExpectedMonthAbbreviationTooShort)); } let (x, input) = input.split_at(3); let candidate = &[ @@ -1143,13 +1046,7 @@ fn parse_month_name_abbrev<'i>( b"oct" => 9, b"nov" => 10, b"dec" => 11, - _ => { - return Err(err!( - "expected to find month name abbreviation, but found \ - {candidate:?} instead", - candidate = escape::Bytes(x), - )) - } + _ => return Err(Error::from(PE::ExpectedMonthAbbreviation)), }; Ok((index, input)) } @@ -1158,8 +1055,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 input.starts_with(b"/") { - input = &input[1..]; + while let Some(tail) = input.strip_prefix(b"/") { + input = tail; let (_, unconsumed) = parse_iana_component(input)?; input = unconsumed; } @@ -1180,17 +1077,10 @@ fn parse_iana_component<'i>( ) -> Result<(&'i [u8], &'i [u8]), Error> { let mkname = parse::slicer(input); if input.is_empty() { - return Err(err!( - "expected the start of an IANA time zone identifier \ - name or component, but found end of input instead", - )); + return Err(Error::from(PE::ExpectedIanaTzEndOfInput)); } if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') { - return Err(err!( - "expected the start of an IANA time zone identifier \ - name or component, but found {:?} instead", - escape::Byte(input[0]), - )); + return Err(Error::from(PE::ExpectedIanaTz)); } input = &input[1..]; @@ -1200,8 +1090,12 @@ fn parse_iana_component<'i>( b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', ) }; - while !input.is_empty() && is_iana_char(input[0]) { - input = &input[1..]; + loop { + let Some((&first, tail)) = input.split_first() else { break }; + if !is_iana_char(first) { + break; + } + input = tail; } Ok((mkname(input), input)) } @@ -1701,7 +1595,7 @@ mod tests { let p = |fmt: &str, input: &str| { BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes()) .unwrap() - .to_offset() + .offset .unwrap() }; @@ -1816,116 +1710,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: invalid number, no digits found", + @"strptime parsing failed: %M failed: failed to parse minute number: 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: invalid number, no digits found", + @"strptime parsing failed: %M failed: failed to parse minute number: invalid number, no digits found", ); insta::assert_snapshot!( p("%y", "999"), - @r###"strptime expects to consume the entire input, but "9" remains unparsed"###, + @"strptime expects to consume the entire input, but `9` remains unparsed", ); insta::assert_snapshot!( p("%Y", "-10000"), - @r###"strptime expects to consume the entire input, but "0" remains unparsed"###, + @"strptime expects to consume the entire input, but `0` remains unparsed", ); insta::assert_snapshot!( p("%Y", "10000"), - @r###"strptime expects to consume the entire input, but "0" remains unparsed"###, + @"strptime expects to consume the entire input, but `0` remains unparsed", ); insta::assert_snapshot!( p("%A %m/%d/%y", "Mon 7/14/24"), - @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"#, + @"strptime parsing failed: %A failed: unrecognized weekday abbreviation: failed to find expected value, available choices are: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday", ); insta::assert_snapshot!( p("%b", "Bad"), - @r###"strptime parsing failed: %b failed: expected to find month name abbreviation, but found "Bad" instead"###, + @"strptime parsing failed: %b failed: expected to find month name abbreviation", ); insta::assert_snapshot!( p("%h", "July"), - @r###"strptime expects to consume the entire input, but "y" remains unparsed"###, + @"strptime expects to consume the entire input, but `y` remains unparsed", ); insta::assert_snapshot!( p("%B", "Jul"), - @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"###, + @"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", ); insta::assert_snapshot!( p("%H", "24"), - @"strptime parsing failed: %H failed: hour number is invalid: parameter 'hour' with value 24 is not in the required range of 0..=23", + @"strptime parsing failed: %H failed: failed to parse hour number: 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: minute number is invalid: parameter 'minute' with value 60 is not in the required range of 0..=59", + @"strptime parsing failed: %M failed: failed to parse minute number: 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: second number is invalid: parameter 'second' with value 61 is not in the required range of 0..=59", + @"strptime parsing failed: %S failed: failed to parse second number: 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: hour number is invalid: parameter 'hour' with value 0 is not in the required range of 1..=12", + @"strptime parsing failed: %I failed: failed to parse hour number: 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: hour number is invalid: parameter 'hour' with value 13 is not in the required range of 1..=12", + @"strptime parsing failed: %I failed: failed to parse hour number: parameter 'hour' with value 13 is not in the required range of 1..=12", ); insta::assert_snapshot!( p("%p", "aa"), - @r###"strptime parsing failed: %p failed: expected to find AM or PM, but found "aa" instead"###, + @"strptime parsing failed: %p failed: expected to find `AM` or `PM`", ); insta::assert_snapshot!( p("%_", " "), - @r###"strptime parsing failed: expected to find specifier directive after flag "_", but found end of format string"###, + @"strptime parsing failed: expected to find specifier directive after flag `_`, but found end of format string", ); insta::assert_snapshot!( p("%-", " "), - @r###"strptime parsing failed: expected to find specifier directive after flag "-", but found end of format string"###, + @"strptime parsing failed: expected to find specifier directive after flag `-`, but found end of format string", ); insta::assert_snapshot!( p("%0", " "), - @r###"strptime parsing failed: expected to find specifier directive after flag "0", but found end of format string"###, + @"strptime parsing failed: expected to find specifier directive after flag `0`, but found end of format string", ); insta::assert_snapshot!( p("%^", " "), - @r###"strptime parsing failed: expected to find specifier directive after flag "^", but found end of format string"###, + @"strptime parsing failed: expected to find specifier directive after flag `^`, but found end of format string", ); insta::assert_snapshot!( p("%#", " "), - @r###"strptime parsing failed: expected to find specifier directive after flag "#", but found end of format string"###, + @"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 width 1, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after parsed width, but found end of format string", ); insta::assert_snapshot!( p("%_23", " "), - @"strptime parsing failed: expected to find specifier directive after width 23, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after parsed width, but found end of format string", ); insta::assert_snapshot!( p("%:", " "), - @"strptime parsing failed: expected to find specifier directive after 1 colons, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after colons, but found end of format string", ); insta::assert_snapshot!( p("%::", " "), - @"strptime parsing failed: expected to find specifier directive after 2 colons, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after colons, but found end of format string", ); insta::assert_snapshot!( p("%:::", " "), - @"strptime parsing failed: expected to find specifier directive after 3 colons, but found end of format string", + @"strptime parsing failed: expected to find specifier directive after colons, but found end of format string", ); insta::assert_snapshot!( @@ -1938,15 +1832,15 @@ mod tests { ); insta::assert_snapshot!( p("%H:%M:%S%.f", "15:59:01.1234567891"), - @r###"strptime expects to consume the entire input, but "1" remains unparsed"###, + @"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"), - @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###, + @"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"), @@ -1954,11 +1848,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"), - @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###, + @"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"), @@ -1967,61 +1861,61 @@ mod tests { insta::assert_snapshot!( p("%Q", "+America/New_York"), - @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"#, + @"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", ); insta::assert_snapshot!( p("%Q", "-America/New_York"), - @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"#, + @"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", ); insta::assert_snapshot!( p("%:Q", "+0400"), - @r#"strptime parsing failed: %:Q failed: parsed hour component of time zone offset from "+0400", but could not find required colon separator"#, + @"strptime parsing failed: %:Q failed: parsed hour component of time zone offset, but could not find required colon separator", ); insta::assert_snapshot!( p("%Q", "+04:00"), - @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"#, + @"strptime parsing failed: %Q failed: parsed hour component of time zone offset, but found colon after hours which is not allowed", ); insta::assert_snapshot!( p("%Q", "America/"), - @"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found end of input instead", + @"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", ); insta::assert_snapshot!( p("%Q", "America/+"), - @r###"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found "+" instead"###, + @"strptime parsing failed: %Q failed: expected to find the start of an IANA time zone identifier name or component", ); insta::assert_snapshot!( p("%s", "-377705023202"), - @"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", + @"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", ); insta::assert_snapshot!( p("%s", "253402207201"), - @"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", + @"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", ); insta::assert_snapshot!( p("%s", "-9999999999999999999"), - @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer", + @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number 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 '9999999999999999999' too big to parse into 64-bit integer", + @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number too big to parse into 64-bit integer", ); insta::assert_snapshot!( p("%u", "0"), - @"strptime parsing failed: %u failed: weekday number is invalid: parameter 'weekday' with value 0 is not in the required range of 1..=7", + @"strptime parsing failed: %u failed: failed to parse weekday number: 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: weekday number is invalid: parameter 'weekday' with value 7 is not in the required range of 0..=6", + @"strptime parsing failed: %w failed: failed to parse weekday number: parameter 'weekday' with value 7 is not in the required range of 0..=6", ); insta::assert_snapshot!( p("%u", "128"), - @r###"strptime expects to consume the entire input, but "28" remains unparsed"###, + @"strptime expects to consume the entire input, but `28` remains unparsed", ); insta::assert_snapshot!( p("%w", "128"), - @r###"strptime expects to consume the entire input, but "28" remains unparsed"###, + @"strptime expects to consume the entire input, but `28` remains unparsed", ); } @@ -2041,11 +1935,11 @@ mod tests { ); insta::assert_snapshot!( p("%m", "7"), - @"missing year, date cannot be created", + @"year required to parse date", ); insta::assert_snapshot!( p("%d", "25"), - @"missing year, date cannot be created", + @"year required to parse date", ); insta::assert_snapshot!( p("%Y-%m", "2024-7"), @@ -2057,7 +1951,7 @@ mod tests { ); insta::assert_snapshot!( p("%m-%d", "7-25"), - @"missing year, date cannot be created", + @"year required to parse date", ); insta::assert_snapshot!( @@ -2070,20 +1964,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 2024-07-14", + @"parsed weekday `Monday` does not match weekday `Sunday` from parsed date", ); insta::assert_snapshot!( p("%A %m/%d/%y", "Monday 7/14/24"), - @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14", + @"parsed weekday `Monday` does not match weekday `Sunday` from parsed date", ); insta::assert_snapshot!( p("%Y-%U-%u", "2025-00-2"), - @"weekday `Tuesday` is not valid for Sunday based week number `0` in year `2025`", + @"weekday `Tuesday` is not valid for Sunday based week number", ); insta::assert_snapshot!( p("%Y-%W-%u", "2025-00-2"), - @"weekday `Tuesday` is not valid for Monday based week number `0` in year `2025`", + @"weekday `Tuesday` is not valid for Monday based week number", ); } @@ -2125,57 +2019,57 @@ mod tests { insta::assert_snapshot!( p("%z", "+05:30"), - @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"#, + @"strptime parsing failed: %z failed: parsed hour component of time zone offset, but found colon after hours which is not allowed", ); insta::assert_snapshot!( p("%:z", "+0530"), - @r#"strptime parsing failed: %:z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#, + @"strptime parsing failed: %:z failed: parsed hour component of time zone offset, but could not find required colon separator", ); insta::assert_snapshot!( p("%::z", "+0530"), - @r#"strptime parsing failed: %::z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#, + @"strptime parsing failed: %::z failed: parsed hour component of time zone offset, but could not find required colon separator", ); insta::assert_snapshot!( p("%:::z", "+0530"), - @r#"strptime parsing failed: %:::z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#, + @"strptime parsing failed: %:::z failed: parsed hour component of time zone offset, but could not find required colon separator", ); insta::assert_snapshot!( p("%z", "+05"), - @r#"strptime parsing failed: %z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#, + @"strptime parsing failed: %z failed: parsed hour component of time zone offset, but could not find required minute component", ); insta::assert_snapshot!( p("%:z", "+05"), - @r#"strptime parsing failed: %:z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#, + @"strptime parsing failed: %:z failed: parsed hour component of time zone offset, but could not find required minute component", ); insta::assert_snapshot!( p("%::z", "+05"), - @r#"strptime parsing failed: %::z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#, + @"strptime parsing failed: %::z failed: parsed hour component of time zone offset, but could not find required minute component", ); insta::assert_snapshot!( p("%::z", "+05:30"), - @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"#, + @"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset, but could not find required second component", ); insta::assert_snapshot!( p("%:::z", "+5"), - @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"#, + @"strptime parsing failed: %:::z failed: failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input", ); insta::assert_snapshot!( p("%z", "+0530:15"), - @r#"strptime expects to consume the entire input, but ":15" remains unparsed"#, + @"strptime expects to consume the entire input, but `:15` remains unparsed", ); insta::assert_snapshot!( p("%:z", "+05:3015"), - @r#"strptime expects to consume the entire input, but "15" remains unparsed"#, + @"strptime expects to consume the entire input, but `15` remains unparsed", ); insta::assert_snapshot!( p("%::z", "+05:3015"), - @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"#, + @"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset, but could not find required second component", ); insta::assert_snapshot!( p("%:::z", "+05:3015"), - @r#"strptime expects to consume the entire input, but "15" remains unparsed"#, + @"strptime expects to consume the entire input, but `15` remains unparsed", ); } @@ -2192,7 +2086,7 @@ mod tests { insta::assert_snapshot!( p("%^50C%", "2000000000000000000#0077)()"), - @"strptime parsing failed: %C failed: century `2000000000000000000` is too big, must be in range 0-99", + @"strptime parsing failed: %C failed: parameter 'century' with value 2000000000000000000 is not in the required range of 0..=99", ); } } diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index 15178e5..0e0cf43 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, + civil::{self, ISOWeekDate}, error::Error, fmt::Write, span::Span, @@ -320,13 +320,12 @@ impl DateTimeParser { /// ); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "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", + /// "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", /// ); /// ``` /// @@ -410,11 +409,10 @@ impl DateTimeParser { /// ); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "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", + /// "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`", /// ); /// ``` /// @@ -1027,9 +1025,8 @@ impl DateTimeParser { /// // Normally this operation will fail. /// assert_eq!( /// PARSER.parse_zoned(timestamp).unwrap_err().to_string(), - /// "failed to find time zone in square brackets in \ - /// \"2025-01-02T15:13-05\", which is required for \ - /// parsing a zoned instant", + /// "failed to find time zone annotation in square brackets, \ + /// which is required for parsing a zoned datetime", /// ); /// /// // But you can work-around this with `Pieces`, which gives you direct @@ -1073,8 +1070,8 @@ impl DateTimeParser { /// /// assert_eq!( /// PARSER.parse_date("2024-03-10T00:00:00Z").unwrap_err().to_string(), - /// "cannot parse civil date from string with a Zulu offset, \ - /// parse as a `Timestamp` and convert to a civil date instead", + /// "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", /// ); /// /// # Ok::<(), Box>(()) @@ -1110,6 +1107,22 @@ 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. @@ -1964,6 +1977,25 @@ 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. @@ -2455,7 +2487,7 @@ mod tests { ); insta::assert_snapshot!( DateTimeParser::new().parse_date("-000000-01-01").unwrap_err(), - @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"###, + @"failed to parse year in date: 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 9826f5e..6a6083d 100644 --- a/src/fmt/temporal/parser.rs +++ b/src/fmt/temporal/parser.rs @@ -1,6 +1,6 @@ use crate::{ - civil::{Date, DateTime, Time}, - error::{err, Error, ErrorContext}, + civil::{Date, DateTime, ISOWeekDate, Time, Weekday}, + error::{fmt::temporal::Error as E, Error, ErrorContext}, fmt::{ offset::{self, ParsedOffset}, rfc9557::{self, ParsedAnnotations}, @@ -67,7 +67,7 @@ impl<'i> ParsedDateTime<'i> { } #[cfg_attr(feature = "perf-inline", inline(always))] - pub(super) fn to_ambiguous_zoned( + fn to_ambiguous_zoned( &self, db: &TimeZoneDatabase, offset_conflict: OffsetConflict, @@ -76,14 +76,10 @@ 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_else(|| { - err!( - "failed to find time zone in square brackets \ - in {:?}, which is required for parsing a zoned instant", - self.input, - ) - })?; + let tz_annotation = self + .annotations + .to_time_zone_annotation()? + .ok_or(E::MissingTimeZoneAnnotation)?; let tz = tz_annotation.to_time_zone_with(db)?; // If there's no offset, then our only choice, regardless of conflict @@ -102,11 +98,7 @@ 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) - .with_context(|| { - err!("parsing {input:?} failed", input = self.input) - }); + return OffsetConflict::AlwaysOffset.resolve(dt, Offset::UTC, tz); } let offset = parsed_offset.to_offset()?; let is_equal = |parsed: Offset, candidate: Offset| { @@ -137,46 +129,30 @@ impl<'i> ParsedDateTime<'i> { }; parsed == candidate }; - offset_conflict.resolve_with(dt, offset, tz, is_equal).with_context( - || err!("parsing {input:?} failed", input = self.input), - ) + offset_conflict.resolve_with(dt, offset, tz, is_equal) } #[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_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 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 offset = parsed_offset.to_offset()?; let dt = DateTime::from_parts(self.date.date, time); - let timestamp = offset.to_timestamp(dt).with_context(|| { - err!( - "failed to convert civil datetime to timestamp \ - with offset {offset}", - ) - })?; + let timestamp = offset + .to_timestamp(dt) + .context(E::ConvertDateTimeToTimestamp { 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(err!( - "cannot parse civil date from string with a Zulu \ - offset, parse as a `Timestamp` and convert to a civil \ - datetime instead", - )); + return Err(Error::from(E::CivilDateTimeZulu)); } Ok(DateTime::from_parts(self.date.date, self.time())) } @@ -184,11 +160,7 @@ 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(err!( - "cannot parse civil date from string with a Zulu \ - offset, parse as a `Timestamp` and convert to a civil \ - date instead", - )); + return Err(Error::from(E::CivilDateTimeZulu)); } Ok(self.date.date) } @@ -272,24 +244,11 @@ impl<'i> ParsedTimeZone<'i> { ) -> Result { match self.kind { ParsedTimeZoneKind::Named(iana_name) => { - 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) + db.get(iana_name).context(E::FailedTzdbLookup) } ParsedTimeZoneKind::Offset(poff) => { - let offset = poff.to_offset().with_context(|| { - err!( - "offset successfully parsed from {input}, \ - but failed to convert to numeric `Offset`", - input = self.input, - ) - })?; + let offset = + poff.to_offset().context(E::FailedOffsetNumeric)?; Ok(TimeZone::fixed(offset)) } #[cfg(feature = "alloc")] @@ -330,7 +289,7 @@ impl DateTimeParser { ) -> Result>, Error> { let mkslice = parse::slicer(input); let Parsed { value: date, input } = self.parse_date_spec(input)?; - if input.is_empty() { + let Some((&first, tail)) = input.split_first() else { let value = ParsedDateTime { input: escape::Bytes(mkslice(input)), date, @@ -339,12 +298,11 @@ impl DateTimeParser { annotations: ParsedAnnotations::none(), }; return Ok(Parsed { value, input }); - } - let (time, offset, input) = if !matches!(input[0], b' ' | b'T' | b't') - { + }; + let (time, offset, input) = if !matches!(first, b' ' | b'T' | b't') { (None, None, input) } else { - let input = &input[1..]; + let input = tail; // 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). @@ -384,20 +342,17 @@ impl DateTimeParser { #[cfg_attr(feature = "perf-inline", inline(always))] pub(super) fn parse_temporal_time<'i>( &self, - mut input: &'i [u8], + input: &'i [u8], ) -> Result>, Error> { let mkslice = parse::slicer(input); - if input.starts_with(b"T") || input.starts_with(b"t") { - input = &input[1..]; + if let Some(input) = + input.strip_prefix(b"T").or_else(|| input.strip_prefix(b"t")) + { 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(err!( - "cannot parse civil time from string with a Zulu \ - offset, parse as a `Timestamp` and convert to a civil \ - time instead", - )); + return Err(Error::from(E::CivilDateTimeZulu)); } let Parsed { input, .. } = self.parse_annotations(input)?; return Ok(Parsed { value: time, input }); @@ -414,18 +369,10 @@ 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(err!( - "cannot parse plain time from full datetime string with a \ - Zulu offset, parse as a `Timestamp` and convert to a \ - plain time instead", - )); + return Err(Error::from(E::CivilDateTimeZulu)); } let Some(time) = dt.time else { - return Err(err!( - "successfully parsed date from {parsed:?}, but \ - no time component was found", - parsed = dt.input, - )); + return Err(Error::from(E::MissingTimeInDate)); }; return Ok(Parsed { value: time, input }); } @@ -436,11 +383,7 @@ 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(err!( - "cannot parse plain time from string with a Zulu \ - offset, parse as a `Timestamp` and convert to a plain \ - time instead", - )); + return Err(Error::from(E::CivilDateTimeZulu)); } // The possible ambiguities occur with the time AND the // optional offset, so try to parse what we have so far as @@ -452,18 +395,10 @@ impl DateTimeParser { if !time.extended { let possibly_ambiguous = mkslice(input); if self.parse_month_day(possibly_ambiguous).is_ok() { - return Err(err!( - "parsed time from {parsed:?} is ambiguous \ - with a month-day date", - parsed = escape::Bytes(possibly_ambiguous), - )); + return Err(Error::from(E::AmbiguousTimeMonthDay)); } if self.parse_year_month(possibly_ambiguous).is_ok() { - return Err(err!( - "parsed time from {parsed:?} is ambiguous \ - with a year-month date", - parsed = escape::Bytes(possibly_ambiguous), - )); + return Err(Error::from(E::AmbiguousTimeYearMonth)); } } // OK... carry on. @@ -476,9 +411,7 @@ impl DateTimeParser { &self, mut input: &'i [u8], ) -> Result>, Error> { - let Some(first) = input.first().copied() else { - return Err(err!("an empty string is not a valid time zone")); - }; + let &first = input.first().ok_or(E::EmptyTimeZone)?; let original = escape::Bytes(input); if matches!(first, b'+' | b'-') { static P: offset::Parser = offset::Parser::new() @@ -495,13 +428,8 @@ 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 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 tzid = core::str::from_utf8(consumed) + .map_err(|_| E::InvalidTimeZoneUtf8)?; let kind = ParsedTimeZoneKind::Named(tzid); let value = ParsedTimeZone { input: original, kind }; Ok(Parsed { value, input: remaining }) @@ -521,12 +449,12 @@ impl DateTimeParser { let mkconsumed = parse::slicer(input); let mut saw_number = false; loop { - let Some(byte) = input.first().copied() else { break }; + let Some((&byte, tail)) = input.split_first() else { break }; if byte.is_ascii_whitespace() { break; } saw_number = saw_number || byte.is_ascii_digit(); - input = &input[1..]; + input = tail; } let consumed = mkconsumed(input); if !saw_number { @@ -534,10 +462,7 @@ impl DateTimeParser { } #[cfg(not(feature = "alloc"))] { - Err(err!( - "cannot parsed time zones other than fixed offsets \ - without the `alloc` crate feature enabled", - )) + Err(Error::from(E::AllocPosixTimeZone)) } #[cfg(feature = "alloc")] { @@ -559,6 +484,50 @@ 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 @@ -568,40 +537,32 @@ 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).with_context(|| { - err!("failed to parse year in date {original:?}") - })?; + 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("failed to parse separator after year")?; + .context(E::FailedSeparatorAfterYear)?; // Parse month component. let Parsed { value: month, input } = - self.parse_month(input).with_context(|| { - err!("failed to parse month in date {original:?}") - })?; + self.parse_month(input).context(E::FailedMonthInDate)?; // Parse optional separator. let Parsed { input, .. } = self .parse_date_separator(input, extended) - .context("failed to parse separator after month")?; + .context(E::FailedSeparatorAfterMonth)?; // Parse day component. let Parsed { value: day, input } = - self.parse_day(input).with_context(|| { - err!("failed to parse day in date {original:?}") - })?; + self.parse_day(input).context(E::FailedDayInDate)?; - let date = Date::new_ranged(year, month, day).with_context(|| { - err!("date parsed from {original:?} is not valid") - })?; + let date = + Date::new_ranged(year, month, day).context(E::InvalidDate)?; let value = ParsedDate { input: escape::Bytes(mkslice(input)), date }; Ok(Parsed { value, input }) } @@ -618,13 +579,10 @@ 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).with_context(|| { - err!("failed to parse hour in time {original:?}") - })?; + self.parse_hour(input).context(E::FailedHourInTime)?; let extended = input.starts_with(b":"); // Parse optional minute component. @@ -645,9 +603,7 @@ impl DateTimeParser { return Ok(Parsed { value, input }); } let Parsed { value: minute, input } = - self.parse_minute(input).with_context(|| { - err!("failed to parse minute in time {original:?}") - })?; + self.parse_minute(input).context(E::FailedMinuteInTime)?; // Parse optional second component. let Parsed { value: has_second, input } = @@ -667,18 +623,12 @@ impl DateTimeParser { return Ok(Parsed { value, input }); } let Parsed { value: second, input } = - self.parse_second(input).with_context(|| { - err!("failed to parse second in time {original:?}") - })?; + self.parse_second(input).context(E::FailedSecondInTime)?; // Parse an optional fractional component. let Parsed { value: nanosecond, input } = - parse_temporal_fraction(input).with_context(|| { - err!( - "failed to parse fractional nanoseconds \ - in time {original:?}", - ) - })?; + parse_temporal_fraction(input) + .context(E::FailedFractionalSecondInTime)?; let time = Time::new_ranged( hour, @@ -715,33 +665,26 @@ 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).with_context(|| { - err!("failed to parse month in month-day {original:?}") - })?; + self.parse_month(input).context(E::FailedMonthInMonthDay)?; // Skip over optional separator. - if input.starts_with(b"-") { - input = &input[1..]; + if let Some(tail) = input.strip_prefix(b"-") { + input = tail; } // Parse day component. let Parsed { value: day, input } = - self.parse_day(input).with_context(|| { - err!("failed to parse day in month-day {original:?}") - })?; + self.parse_day(input).context(E::FailedDayInMonthDay)?; // 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).with_context(|| { - err!("month-day parsed from {original:?} is not valid") - })?; + let _ = + Date::new_ranged(year, month, day).context(E::InvalidMonthDay)?; // We have a valid year-month. But we don't return it because we just // need to check validity. @@ -758,31 +701,24 @@ 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).with_context(|| { - err!("failed to parse year in date {original:?}") - })?; + self.parse_year(input).context(E::FailedYearInYearMonth)?; // Skip over optional separator. - if input.starts_with(b"-") { - input = &input[1..]; + if let Some(tail) = input.strip_prefix(b"-") { + input = tail; } // Parse month component. let Parsed { value: month, input } = - self.parse_month(input).with_context(|| { - err!("failed to parse month in month-day {original:?}") - })?; + self.parse_month(input).context(E::FailedMonthInYearMonth)?; // 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).with_context(|| { - err!("year-month parsed from {original:?} is not valid") - })?; + let _ = + Date::new_ranged(year, month, day).context(E::InvalidYearMonth)?; // We have a valid year-month. But we don't return it because we just // need to check validity. @@ -804,50 +740,33 @@ 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 { - 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 }) + return self.parse_signed_year(input, sign); } + + 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 ::: @@ -860,17 +779,11 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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, input) = + parse::split(input, 2).ok_or(E::ExpectedTwoDigitMonth)?; + let month = parse::i64(month).context(E::ParseMonthTwoDigit)?; let month = - t::Month::try_new("month", month).context("month is not valid")?; + t::Month::try_new("month", month).context(E::InvalidMonth)?; Ok(Parsed { value: month, input }) } @@ -885,16 +798,10 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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")?; + 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)?; Ok(Parsed { value: day, input }) } @@ -913,17 +820,10 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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")?; + 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)?; Ok(Parsed { value: hour, input }) } @@ -942,17 +842,11 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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")?; + 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)?; Ok(Parsed { value: minute, input }) } @@ -972,22 +866,16 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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), - ) - })?; + let (second, input) = + parse::split(input, 2).ok_or(E::ExpectedTwoDigitSecond)?; + let mut second = parse::i64(second).context(E::ParseSecondTwoDigit)?; // 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("second is not valid")?; + let second = + t::Second::try_new("second", second).context(E::InvalidSecond)?; Ok(Parsed { value: second, input }) } @@ -1007,7 +895,7 @@ impl DateTimeParser { input: &'i [u8], ) -> Result>, Error> { const P: rfc9557::Parser = rfc9557::Parser::new(); - if input.is_empty() || input[0] != b'[' { + if input.first().map_or(true, |&b| b != b'[') { let value = ParsedAnnotations::none(); return Ok(Parsed { input, value }); } @@ -1022,32 +910,24 @@ impl DateTimeParser { #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_date_separator<'i>( &self, - mut input: &'i [u8], + 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(err!( - "expected no separator after month since none was \ - found after the year, but found a '-' separator", - )); + return Err(Error::from(E::ExpectedNoSeparator)); } return Ok(Parsed { value: (), input }); } - if input.is_empty() { - return Err(err!( - "expected '-' separator, but found end of input" - )); + let (&first, input) = + input.split_first().ok_or(E::ExpectedSeparatorFoundEndOfInput)?; + if first != b'-' { + return Err(Error::from(E::ExpectedSeparatorFoundByte { + byte: first, + })); } - 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 }) } @@ -1068,13 +948,16 @@ impl DateTimeParser { extended: bool, ) -> Parsed<'i, bool> { if !extended { - let expected = - input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit); + let expected = parse::split(input, 2) + .map_or(false, |(prefix, _)| { + prefix.iter().all(u8::is_ascii_digit) + }); return Parsed { value: expected, input }; } - let is_separator = input.get(0).map_or(false, |&b| b == b':'); - if is_separator { - input = &input[1..]; + let mut is_separator = false; + if let Some(tail) = input.strip_prefix(b":") { + is_separator = true; + input = tail; } Parsed { value: is_separator, input } } @@ -1094,9 +977,9 @@ impl DateTimeParser { #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_year_sign<'i>( &self, - mut input: &'i [u8], + input: &'i [u8], ) -> Parsed<'i, Option> { - let Some(sign) = input.get(0).copied() else { + let Some((&sign, tail)) = input.split_first() else { return Parsed { value: None, input }; }; let sign = if sign == b'+' { @@ -1106,8 +989,55 @@ impl DateTimeParser { } else { return Parsed { value: None, input }; }; - input = &input[1..]; - Parsed { value: Some(sign), 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 }) } } @@ -1138,14 +1068,7 @@ impl SpanParser { let parsed = parsed.and_then(|_| builder.to_span())?; parsed.into_full() } - - 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) - ) - }) + imp(self, input.as_ref()) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1160,14 +1083,7 @@ impl SpanParser { let parsed = parsed.and_then(|_| builder.to_signed_duration())?; parsed.into_full() } - - 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) - ) - }) + imp(self, input.as_ref()) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1187,14 +1103,7 @@ impl SpanParser { let d = parsed.value; parsed.into_full_with(format_args!("{d:?}")) } - - 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) - ) - }) + imp(self, input.as_ref()) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1203,7 +1112,6 @@ 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) @@ -1211,8 +1119,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_duration_designator(input)?; let Parsed { input, .. } = self.parse_date_units(input, builder)?; let Parsed { value: has_time, mut input } = self.parse_time_designator(input); @@ -1221,11 +1129,7 @@ impl SpanParser { input = parsed.input; if builder.get_min().map_or(true, |min| min > Unit::Hour) { - 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", - )); + return Err(Error::from(E::ExpectedTimeUnits)); } } builder.set_sign(sign); @@ -1238,7 +1142,6 @@ 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) @@ -1246,25 +1149,17 @@ 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_duration_designator(input)?; let Parsed { value: has_time, input } = self.parse_time_designator(input); if !has_time { - 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", - )); + return Err(Error::from(E::ExpectedTimeDesignator)); } let Parsed { input, .. } = self.parse_time_units(input, builder)?; if builder.get_min().map_or(true, |min| min > Unit::Hour) { - 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", - )); + return Err(Error::from(E::ExpectedTimeUnits)); } builder.set_sign(sign); Ok(Parsed { value: (), input }) @@ -1339,26 +1234,21 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - 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] { + let (&first, input) = input + .split_first() + .ok_or(E::ExpectedDateDesignatorFoundEndOfInput)?; + let unit = match first { b'Y' | b'y' => Unit::Year, b'M' | b'm' => Unit::Month, b'W' | b'w' => Unit::Week, b'D' | b'd' => Unit::Day, - unknown => { - return Err(err!( - "expected to find date unit designator suffix \ - (Y, M, W or D), but found {found:?} instead", - found = escape::Byte(unknown), - )); + _ => { + return Err(Error::from(E::ExpectedDateDesignatorFoundByte { + byte: first, + })); } }; - Ok(Parsed { value: unit, input: &input[1..] }) + Ok(Parsed { value: unit, input }) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -1366,25 +1256,20 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - 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] { + let (&first, input) = input + .split_first() + .ok_or(E::ExpectedTimeDesignatorFoundEndOfInput)?; + let unit = match first { b'H' | b'h' => Unit::Hour, b'M' | b'm' => Unit::Minute, b'S' | b's' => Unit::Second, - unknown => { - return Err(err!( - "expected to find time unit designator suffix \ - (H, M or S), but found {found:?} instead", - found = escape::Byte(unknown), - )); + _ => { + return Err(Error::from(E::ExpectedTimeDesignatorFoundByte { + byte: first, + })); } }; - Ok(Parsed { value: unit, input: &input[1..] }) + Ok(Parsed { value: unit, input }) } // DurationDesignator ::: one of @@ -1394,30 +1279,28 @@ impl SpanParser { &self, input: &'i [u8], ) -> Result, Error> { - if input.is_empty() { - return Err(err!( - "expected to find duration beginning with 'P' or 'p', \ - but found end of input", - )); + 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 !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..] }) + Ok(Parsed { value: (), input }) } // TimeDesignator ::: one of // T t #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_time_designator<'i>(&self, input: &'i [u8]) -> Parsed<'i, bool> { - if input.is_empty() || !matches!(input[0], b'T' | b't') { + let Some((&first, tail)) = input.split_first() else { + return Parsed { value: false, input }; + }; + if !matches!(first, b'T' | b't') { return Parsed { value: false, input }; } - Parsed { value: true, input: &input[1..] } + Parsed { value: true, input: tail } } // TemporalSign ::: @@ -1429,17 +1312,13 @@ impl SpanParser { #[cold] #[inline(never)] fn parse_sign<'i>(&self, input: &'i [u8]) -> Parsed<'i, Sign> { - 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 + 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 } } else { - return Parsed { value: Sign::Positive, input }; - }; - Parsed { value: sign, input: &input[1..] } + Parsed { value: Sign::Positive, input } + } } } @@ -1481,63 +1360,63 @@ mod tests { insta::assert_snapshot!( p(b"P0d"), - @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"#, + @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", ); insta::assert_snapshot!( p(b"PT0d"), - @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"#, + @"expected to find time unit designator suffix (`H`, `M` or `S`), but found `d` instead", ); insta::assert_snapshot!( p(b"P0dT1s"), - @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"#, + @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", ); insta::assert_snapshot!( p(b""), - @r#"failed to parse "" as an ISO 8601 duration string: expected to find duration beginning with 'P' or 'p', but found end of input"#, + @"expected to find duration beginning with `P` or `p`, but found end of input", ); insta::assert_snapshot!( p(b"P"), - @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"#, + @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", ); insta::assert_snapshot!( p(b"PT"), - @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"#, + @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", ); insta::assert_snapshot!( p(b"PTs"), - @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"#, + @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", ); insta::assert_snapshot!( p(b"PT1s1m"), - @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)"#, + @"found value 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"), - @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)"#, + @"found value 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"), - @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)"#, + @"found value 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"), - @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"#, + @"value for seconds is too big (or small) to fit into a signed 64-bit integer", ); insta::assert_snapshot!( p(b"PT9223372036854775808s"), - @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"#, + @"value for seconds is too big (or small) to fit into a signed 64-bit integer", ); insta::assert_snapshot!( p(b"PT1m9223372036854775807s"), - @r#"failed to parse "PT1m9223372036854775807s" as an ISO 8601 duration string: accumulated `SignedDuration` of `1m` overflowed when adding 9223372036854775807 of unit second"#, + @"accumulated duration overflowed when adding value to unit second", ); insta::assert_snapshot!( p(b"PT2562047788015215.6h"), - @r#"failed to parse "PT2562047788015215.6h" as an ISO 8601 duration string: accumulated `SignedDuration` of `2562047788015215h` overflowed when adding 0.600000000 of unit hour"#, + @"accumulated duration overflowed when adding fractional value to unit hour", ); } @@ -1578,76 +1457,76 @@ mod tests { insta::assert_snapshot!( p(b"-PT1S"), - @r#"failed to parse "-PT1S" as an ISO 8601 duration string: cannot parse negative duration into unsigned `std::time::Duration`"#, + @"cannot parse negative duration into unsigned `std::time::Duration`", ); insta::assert_snapshot!( p(b"-PT0S"), - @r#"failed to parse "-PT0S" as an ISO 8601 duration string: cannot parse negative duration into unsigned `std::time::Duration`"#, + @"cannot parse negative duration into unsigned `std::time::Duration`", ); insta::assert_snapshot!( p(b"P0d"), - @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"#, + @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", ); insta::assert_snapshot!( p(b"PT0d"), - @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"#, + @"expected to find time unit designator suffix (`H`, `M` or `S`), but found `d` instead", ); insta::assert_snapshot!( p(b"P0dT1s"), - @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"#, + @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", ); insta::assert_snapshot!( p(b""), - @r#"failed to parse "" as an ISO 8601 duration string: expected to find duration beginning with 'P' or 'p', but found end of input"#, + @"expected to find duration beginning with `P` or `p`, but found end of input", ); insta::assert_snapshot!( p(b"P"), - @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"#, + @"parsing ISO 8601 duration in this context requires that the duration contain a time component and no components of days or greater", ); insta::assert_snapshot!( p(b"PT"), - @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"#, + @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", ); insta::assert_snapshot!( p(b"PTs"), - @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"#, + @"found a time designator (`T` or `t`) in an ISO 8601 duration string, but did not find any time units", ); insta::assert_snapshot!( p(b"PT1s1m"), - @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)"#, + @"found value 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"), - @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)"#, + @"found value 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"), - @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)"#, + @"found value 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"), - @r#"failed to parse "-PT9223372036854775809S" as an ISO 8601 duration string: cannot parse negative duration into unsigned `std::time::Duration`"#, + @"cannot parse negative duration into unsigned `std::time::Duration`", ); insta::assert_snapshot!( p(b"PT18446744073709551616S"), - @r#"failed to parse "PT18446744073709551616S" as an ISO 8601 duration string: number `18446744073709551616` too big to parse into 64-bit integer"#, + @"number too big to parse into 64-bit integer", ); insta::assert_snapshot!( p(b"PT5124095576030431H16.999999999S"), - @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"#, + @"accumulated duration overflowed when adding value to unit second", ); insta::assert_snapshot!( p(b"PT1M18446744073709551556S"), - @r#"failed to parse "PT1M18446744073709551556S" as an ISO 8601 duration string: accumulated `std::time::Duration` of `60s` overflowed when adding 18446744073709551556 of unit second"#, + @"accumulated duration overflowed when adding value to unit second", ); insta::assert_snapshot!( p(b"PT5124095576030431.5H"), - @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"#, + @"accumulated duration overflowed when adding fractional value to unit hour", ); } @@ -1710,7 +1589,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", @@ -1721,14 +1600,13 @@ 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]", @@ -1739,7 +1617,6 @@ mod tests { time: None, offset: None, annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1750,8 +1627,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", @@ -1768,14 +1645,13 @@ 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", @@ -1798,14 +1674,13 @@ 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]", @@ -1828,7 +1703,6 @@ mod tests { }, ), annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1839,8 +1713,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]", @@ -1861,7 +1735,6 @@ mod tests { }, ), annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1872,8 +1745,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]", @@ -1896,7 +1769,6 @@ mod tests { }, ), annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1907,7 +1779,7 @@ mod tests { }, input: "", } - "###); + "#); } #[test] @@ -1916,7 +1788,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", @@ -1933,14 +1805,13 @@ 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", @@ -1957,14 +1828,13 @@ 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", @@ -1981,13 +1851,12 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { - input: "", time_zone: None, }, }, input: "", } - "###); + "#); } #[test] @@ -1996,7 +1865,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", @@ -2013,14 +1882,13 @@ 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", @@ -2037,13 +1905,12 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { - input: "", time_zone: None, }, }, input: "", } - "###); + "#); } #[test] @@ -2190,11 +2057,11 @@ mod tests { insta::assert_snapshot!( p(b"010203"), - @r###"parsed time from "010203" is ambiguous with a month-day date"###, + @"parsed time is ambiguous with a month-day date", ); insta::assert_snapshot!( p(b"130112"), - @r###"parsed time from "130112" is ambiguous with a year-month date"###, + @"parsed time is ambiguous with a year-month date", ); } @@ -2206,21 +2073,21 @@ mod tests { insta::assert_snapshot!( p(b"2024-06-01[America/New_York]"), - @r###"successfully parsed date from "2024-06-01[America/New_York]", but no time component was found"###, + @"successfully parsed date, 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]"), - @r###"successfully parsed date from "2099-12-01[America/New_York]", but no time component was found"###, + @"successfully parsed date, 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]"), - @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"###, + @"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", ); } @@ -2232,19 +2099,19 @@ mod tests { insta::assert_snapshot!( p(b"T00:00:00Z"), - @"cannot parse civil time from string with a Zulu offset, parse as a `Timestamp` and convert to a civil time instead", + @"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", ); insta::assert_snapshot!( p(b"00:00:00Z"), - @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead", + @"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", ); insta::assert_snapshot!( p(b"000000Z"), - @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead", + @"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", ); insta::assert_snapshot!( p(b"2099-12-01T00:00:00Z"), - @"cannot parse plain time from full datetime string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead", + @"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", ); } @@ -2303,7 +2170,7 @@ mod tests { fn err_date_empty() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"").unwrap_err(), - @r###"failed to parse year in date "": expected four digit year (or leading sign for six digit year), but found end of input"###, + @"failed to parse year in date: expected four digit year (or leading sign for six digit year), but found end of input", ); } @@ -2311,40 +2178,40 @@ mod tests { fn err_date_year() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"123").unwrap_err(), - @r###"failed to parse year in date "123": expected four digit year (or leading sign for six digit year), but found end of input"###, + @"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!( DateTimeParser::new().parse_date_spec(b"123a").unwrap_err(), - @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"###, + @"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!( DateTimeParser::new().parse_date_spec(b"-9999").unwrap_err(), - @r###"failed to parse year in date "-9999": expected six digit year (because of a leading sign), but found end of input"###, + @"failed to parse year in date: 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(), - @r###"failed to parse year in date "+9999": expected six digit year (because of a leading sign), but found end of input"###, + @"failed to parse year in date: 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(), - @r###"failed to parse year in date "-99999": expected six digit year (because of a leading sign), but found end of input"###, + @"failed to parse year in date: 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(), - @r###"failed to parse year in date "+99999": expected six digit year (because of a leading sign), but found end of input"###, + @"failed to parse year in date: 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(), - @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"###, + @"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!( DateTimeParser::new().parse_date_spec(b"+999999").unwrap_err(), - @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"###, + @"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!( DateTimeParser::new().parse_date_spec(b"-010000").unwrap_err(), - @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"###, + @"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", ); } @@ -2352,19 +2219,19 @@ mod tests { fn err_date_month() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-").unwrap_err(), - @r###"failed to parse month in date "2024-": expected two digit month, but found end of input"###, + @"failed to parse month in date: expected two digit month, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024").unwrap_err(), - @r###"failed to parse month in date "2024": expected two digit month, but found end of input"###, + @"failed to parse month in date: expected two digit month, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-13-01").unwrap_err(), - @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"###, + @"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", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"20241301").unwrap_err(), - @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"###, + @"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", ); } @@ -2372,27 +2239,27 @@ mod tests { fn err_date_day() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-12-").unwrap_err(), - @r###"failed to parse day in date "2024-12-": expected two digit day, but found end of input"###, + @"failed to parse day in date: expected two digit day, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"202412").unwrap_err(), - @r###"failed to parse day in date "202412": expected two digit day, but found end of input"###, + @"failed to parse day in date: expected two digit day, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-12-40").unwrap_err(), - @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"###, + @"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", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-11-31").unwrap_err(), - @r###"date parsed from "2024-11-31" is not valid: parameter 'day' with value 31 is not in the required range of 1..=30"###, + @"parsed date 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(), - @r###"date parsed from "2024-02-30" is not valid: parameter 'day' with value 30 is not in the required range of 1..=29"###, + @"parsed date 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(), - @r###"date parsed from "2023-02-29" is not valid: parameter 'day' with value 29 is not in the required range of 1..=28"###, + @"parsed date is not valid: parameter 'day' with value 29 is not in the required range of 1..=28", ); } @@ -2400,11 +2267,11 @@ mod tests { fn err_date_separator() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-1231").unwrap_err(), - @r###"failed to parse separator after month: expected '-' separator, but found "3" instead"###, + @"failed to parse separator after month: expected `-` separator, but found `3`", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"202412-31").unwrap_err(), - @"failed to parse separator after month: expected no separator after month since none was found after the year, but found a '-' separator", + @"failed to parse separator after month: expected no separator since none was found after the year, but found a `-` separator", ); } @@ -2533,7 +2400,7 @@ mod tests { fn err_time_empty() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"").unwrap_err(), - @r###"failed to parse hour in time "": expected two digit hour, but found end of input"###, + @"failed to parse hour in time: expected two digit hour, but found end of input", ); } @@ -2541,15 +2408,15 @@ mod tests { fn err_time_hour() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"a").unwrap_err(), - @r###"failed to parse hour in time "a": expected two digit hour, but found end of input"###, + @"failed to parse hour in time: expected two digit hour, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"1a").unwrap_err(), - @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"###, + @"failed to parse hour in time: failed to parse two digit integer as hour: invalid digit, expected 0-9 but got a", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"24").unwrap_err(), - @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"###, + @"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", ); } @@ -2557,19 +2424,19 @@ mod tests { fn err_time_minute() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:").unwrap_err(), - @r###"failed to parse minute in time "01:": expected two digit minute, but found end of input"###, + @"failed to parse minute in time: expected two digit minute, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:a").unwrap_err(), - @r###"failed to parse minute in time "01:a": expected two digit minute, but found end of input"###, + @"failed to parse minute in time: expected two digit minute, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:1a").unwrap_err(), - @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"###, + @"failed to parse minute in time: failed to parse two digit integer as minute: invalid digit, expected 0-9 but got a", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:60").unwrap_err(), - @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"###, + @"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", ); } @@ -2577,19 +2444,19 @@ mod tests { fn err_time_second() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:").unwrap_err(), - @r###"failed to parse second in time "01:02:": expected two digit second, but found end of input"###, + @"failed to parse second in time: expected two digit second, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:a").unwrap_err(), - @r###"failed to parse second in time "01:02:a": expected two digit second, but found end of input"###, + @"failed to parse second in time: expected two digit second, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:1a").unwrap_err(), - @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"###, + @"failed to parse second in time: failed to parse two digit integer as second: invalid digit, expected 0-9 but got a", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:61").unwrap_err(), - @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"###, + @"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", ); } @@ -2597,11 +2464,262 @@ mod tests { fn err_time_fractional() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:03.").unwrap_err(), - @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"###, + @"failed to parse fractional seconds in time: found decimal after seconds component, but did not find any digits after decimal", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:03.a").unwrap_err(), - @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"###, + @"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", ); } } diff --git a/src/fmt/temporal/pieces.rs b/src/fmt/temporal/pieces.rs index 156ff3f..ca311c8 100644 --- a/src/fmt/temporal/pieces.rs +++ b/src/fmt/temporal/pieces.rs @@ -146,9 +146,8 @@ use crate::{ /// /// assert_eq!( /// "2025-01-03T17:28-05".parse::().unwrap_err().to_string(), -/// "failed to find time zone in square brackets in \ -/// \"2025-01-03T17:28-05\", which is required for \ -/// parsing a zoned instant", +/// "failed to find time zone annotation in square brackets, \ +/// which is required for parsing a zoned datetime", /// ); /// ``` /// diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs index 8e80a7d..b01c332 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -1,9 +1,9 @@ use crate::{ - civil::{Date, DateTime, Time}, - error::{err, Error}, + civil::{Date, DateTime, ISOWeekDate, Time}, + error::{fmt::temporal::Error as E, Error}, fmt::{ temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind}, - util::{DecimalFormatter, FractionalFormatter}, + util::{FractionalFormatter, IntegerFormatter}, Write, WriteExt, }, span::Span, @@ -108,11 +108,11 @@ impl DateTimePrinter { date: &Date, mut wtr: W, ) -> Result<(), Error> { - 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); + 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); 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: DecimalFormatter = DecimalFormatter::new().padding(2); + static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); wtr.write_int(&FMT_TWO, time.hour())?; @@ -197,13 +197,7 @@ impl DateTimePrinter { // // Anyway, if you're seeing this error and think there should be a // different behavior, please file an issue. - 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`)", - )) + Err(Error::from(E::PrintTimeZoneFailure)) } pub(super) fn print_pieces( @@ -255,6 +249,34 @@ 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, @@ -282,7 +304,7 @@ impl DateTimePrinter { offset: &Offset, mut wtr: W, ) -> Result<(), Error> { - static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); + static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); wtr.write_str(if offset.is_negative() { "-" } else { "+" })?; let mut hours = offset.part_hours_ranged().abs().get(); @@ -317,7 +339,7 @@ impl DateTimePrinter { offset: &Offset, mut wtr: W, ) -> Result<(), Error> { - static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); + static FMT_TWO: IntegerFormatter = IntegerFormatter::new().padding(2); wtr.write_str(if offset.is_negative() { "-" } else { "+" })?; let hours = offset.part_hours_ranged().abs().get(); @@ -405,7 +427,7 @@ impl SpanPrinter { span: &Span, mut wtr: W, ) -> Result<(), Error> { - static FMT_INT: DecimalFormatter = DecimalFormatter::new(); + static FMT_INT: IntegerFormatter = IntegerFormatter::new(); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); if span.is_negative() { @@ -519,7 +541,7 @@ impl SpanPrinter { dur: &SignedDuration, mut wtr: W, ) -> Result<(), Error> { - static FMT_INT: DecimalFormatter = DecimalFormatter::new(); + static FMT_INT: IntegerFormatter = IntegerFormatter::new(); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); let mut non_zero_greater_than_second = false; @@ -568,7 +590,7 @@ impl SpanPrinter { dur: &core::time::Duration, mut wtr: W, ) -> Result<(), Error> { - static FMT_INT: DecimalFormatter = DecimalFormatter::new(); + static FMT_INT: IntegerFormatter = IntegerFormatter::new(); static FMT_FRACTION: FractionalFormatter = FractionalFormatter::new(); let mut non_zero_greater_than_second = false; @@ -620,7 +642,10 @@ impl SpanPrinter { mod tests { use alloc::string::String; - use crate::{civil::date, span::ToSpan}; + use crate::{ + civil::{date, Weekday}, + span::ToSpan, + }; use super::*; @@ -922,4 +947,22 @@ 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 3e1773a..23662ea 100644 --- a/src/fmt/util.rs +++ b/src/fmt/util.rs @@ -1,7 +1,7 @@ use crate::{ - error::{err, ErrorContext}, + error::{fmt::util::Error as E, ErrorContext}, fmt::Parsed, - util::{c::Sign, escape, parse, t}, + util::{c::Sign, parse, t}, Error, SignedDuration, Span, Unit, }; @@ -14,52 +14,31 @@ 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. /// -/// By default, this only includes the sign if it's negative. To always include -/// the sign, set `force_sign` to `true`. +/// This only includes the sign when formatting a negative signed integer. #[derive(Clone, Copy, Debug)] -pub(crate) struct DecimalFormatter { - force_sign: Option, +pub(crate) struct IntegerFormatter { minimum_digits: u8, padding_byte: u8, } -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', - } +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' } } - /// Format the given value using this configuration as a signed decimal + /// Format the given value using this configuration as a signed integer /// ASCII number. #[cfg_attr(feature = "perf-inline", inline(always))] - pub(crate) const fn format_signed(&self, value: i64) -> Decimal { - Decimal::signed(self, value) + pub(crate) const fn format_signed(&self, value: i64) -> Integer { + Integer::signed(self, value) } - /// Format the given value using this configuration as an unsigned decimal + /// Format the given value using this configuration as an unsigned integer /// ASCII number. #[cfg_attr(feature = "perf-inline", inline(always))] - 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 } + pub(crate) const fn format_unsigned(&self, value: u64) -> Integer { + Integer::unsigned(self, value) } /// The minimum number of digits/padding that this number should be @@ -69,181 +48,113 @@ impl DecimalFormatter { /// /// 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) -> DecimalFormatter { - if digits > Decimal::MAX_I64_DIGITS { - digits = Decimal::MAX_I64_DIGITS; + pub(crate) const fn padding(self, mut digits: u8) -> IntegerFormatter { + if digits > Integer::MAX_LEN { + digits = Integer::MAX_LEN; } - DecimalFormatter { minimum_digits: digits, ..self } + IntegerFormatter { 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) -> DecimalFormatter { - DecimalFormatter { padding_byte: byte, ..self } + pub(crate) const fn padding_byte(self, byte: u8) -> IntegerFormatter { + IntegerFormatter { padding_byte: byte, ..self } } - /// 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 { + /// Returns the minimum number of digits for an integer value. + const fn get_minimum_digits(&self) -> u8 { + if self.minimum_digits <= Integer::MAX_LEN { self.minimum_digits } else { - 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 + Integer::MAX_LEN } } } -impl Default for DecimalFormatter { - fn default() -> DecimalFormatter { - DecimalFormatter::new() +impl Default for IntegerFormatter { + fn default() -> IntegerFormatter { + IntegerFormatter::new() } } -/// A formatted decimal number that can be converted to a sequence of bytes. +/// A formatted integer number that can be converted to a sequence of bytes. #[derive(Debug)] -pub(crate) struct Decimal { +pub(crate) struct Integer { buf: [u8; Self::MAX_LEN as usize], start: u8, - end: u8, } -impl Decimal { +impl Integer { /// 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 - /// decimal representation using ASCII bytes. + /// integer representation using ASCII bytes. #[cfg_attr(feature = "perf-inline", inline(always))] const fn unsigned( - formatter: &DecimalFormatter, + formatter: &IntegerFormatter, mut value: u64, - ) -> Decimal { - let mut decimal = Decimal { - buf: [0; Self::MAX_LEN as usize], - start: Self::MAX_LEN, - end: Self::MAX_LEN, - }; + ) -> Integer { + let mut integer = + Integer { buf: [0; Self::MAX_LEN as usize], start: Self::MAX_LEN }; loop { - decimal.start -= 1; + integer.start -= 1; let digit = (value % 10) as u8; value /= 10; - decimal.buf[decimal.start as usize] = b'0' + digit; + integer.buf[integer.start as usize] = b'0' + digit; if value == 0 { break; } } - while decimal.len() < formatter.get_unsigned_minimum_digits() { - decimal.start -= 1; - decimal.buf[decimal.start as usize] = formatter.padding_byte; + while integer.len() < formatter.get_minimum_digits() { + integer.start -= 1; + integer.buf[integer.start as usize] = formatter.padding_byte; } - decimal + integer } - /// Using the given formatter, turn the value given into a signed decimal + /// Using the given formatter, turn the value given into a signed integer /// representation using ASCII bytes. #[cfg_attr(feature = "perf-inline", inline(always))] - const fn signed(formatter: &DecimalFormatter, mut value: i64) -> Decimal { + const fn signed(formatter: &IntegerFormatter, value: i64) -> Integer { // Specialize the common case to generate tighter codegen. - 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; + if value >= 0 { + return Integer::unsigned(formatter, value.unsigned_abs()); } - Decimal::signed_cold(formatter, value) + Integer::signed_cold(formatter, value) } #[cold] #[inline(never)] - 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; - } + 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'-'; } - 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 + integer } /// Returns the total number of ASCII bytes (including the sign) that are - /// used to represent this decimal number. + /// used to represent this integer number. #[inline] const fn len(&self) -> u8 { - self.end - self.start + Self::MAX_LEN - self.start } - /// Returns the ASCII representation of this decimal as a byte slice. + /// Returns the ASCII representation of this integer 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)..usize::from(self.end)] + &self.buf[usize::from(self.start)..] } - /// Returns the ASCII representation of this decimal as a string slice. + /// Returns the ASCII representation of this integer as a string slice. #[inline] pub(crate) fn as_str(&self) -> &str { // SAFETY: This is safe because all bytes written to `self.buf` are @@ -462,14 +373,10 @@ impl DurationUnits { if let Some(min) = self.min { if min <= unit { - 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(), - )); + return Err(Error::from(E::OutOfOrderUnits { + found: unit, + previous: min, + })); } } // Given the above check, the given unit must be smaller than any we @@ -503,12 +410,7 @@ impl DurationUnits { ) -> Result<(), Error> { if let Some(min) = self.min { if min <= Unit::Hour { - 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(), - )); + return Err(Error::from(E::OutOfOrderHMS { found: min })); } } self.set_unit_value(Unit::Hour, hours)?; @@ -539,15 +441,11 @@ 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 { - return Err(err!( - "fractional {plural} are not supported", - plural = min.plural() - )); + if min > Unit::Hour || min == Unit::Nanosecond { + return Err(Error::from(E::NotAllowedFractionalUnit { + found: min, + })); } } self.fraction = Some(fraction); @@ -642,13 +540,6 @@ 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, @@ -682,7 +573,7 @@ impl DurationUnits { set(span) .or_else(|err| fractional_fallback(err, unit, value, span)) - .with_context(|| error_context(unit, value)) + .context(E::FailedValueSet { unit }) } let (min, _) = self.get_min_max_units()?; @@ -692,25 +583,25 @@ impl DurationUnits { let value = self.get_unit_value(Unit::Year)?; span = span .try_years(value) - .with_context(|| error_context(Unit::Year, value))?; + .context(E::FailedValueSet { unit: Unit::Year })?; } if self.values[Unit::Month.as_usize()] != 0 { let value = self.get_unit_value(Unit::Month)?; span = span .try_months(value) - .with_context(|| error_context(Unit::Month, value))?; + .context(E::FailedValueSet { unit: Unit::Month })?; } if self.values[Unit::Week.as_usize()] != 0 { let value = self.get_unit_value(Unit::Week)?; span = span .try_weeks(value) - .with_context(|| error_context(Unit::Week, value))?; + .context(E::FailedValueSet { unit: Unit::Week })?; } if self.values[Unit::Day.as_usize()] != 0 { let value = self.get_unit_value(Unit::Day)?; span = span .try_days(value) - .with_context(|| error_context(Unit::Day, value))?; + .context(E::FailedValueSet { unit: Unit::Day })?; } if self.values[Unit::Hour.as_usize()] != 0 { let value = self.get_unit_value(Unit::Hour)?; @@ -822,11 +713,7 @@ impl DurationUnits { fn to_signed_duration_general(&self) -> Result { let (min, max) = self.get_min_max_units()?; if max > Unit::Hour { - return Err(err!( - "parsing {unit} units into a `SignedDuration` is not supported \ - (perhaps try parsing into a `Span` instead)", - unit = max.singular(), - )); + return Err(Error::from(E::NotAllowedCalendarUnit { unit: max })); } let mut sdur = SignedDuration::ZERO; @@ -834,85 +721,43 @@ 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_else(|| { - err!( - "accumulated `SignedDuration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Hour.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Hour })?; } 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_else(|| { - err!( - "accumulated `SignedDuration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Minute.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Minute })?; } 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_else(|| { - err!( - "accumulated `SignedDuration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Second.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Second })?; } 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_else(|| { - err!( - "accumulated `SignedDuration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Millisecond.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?; } 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_else(|| { - err!( - "accumulated `SignedDuration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Microsecond.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?; } 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_else(|| { - err!( - "accumulated `SignedDuration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Nanosecond.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?; } if let Some(fraction) = self.get_fraction()? { sdur = sdur .checked_add(fractional_duration(min, fraction)?) - .ok_or_else(|| { - err!( - "accumulated `SignedDuration` of `{sdur:?}` \ - overflowed when adding 0.{fraction} of unit {unit}", - unit = min.singular(), - ) - })?; + .ok_or(E::OverflowForUnitFractional { unit: min })?; } Ok(sdur) @@ -1003,19 +848,12 @@ impl DurationUnits { } if self.sign.is_negative() { - return Err(err!( - "cannot parse negative duration into unsigned \ - `std::time::Duration`", - )); + return Err(Error::from(E::NotAllowedNegative)); } let (min, max) = self.get_min_max_units()?; if max > Unit::Hour { - return Err(err!( - "parsing {unit} units into a `std::time::Duration` \ - is not supported (perhaps try parsing into a `Span` instead)", - unit = max.singular(), - )); + return Err(Error::from(E::NotAllowedCalendarUnit { unit: max })); } let mut sdur = core::time::Duration::ZERO; @@ -1023,73 +861,37 @@ impl DurationUnits { let value = self.values[Unit::Hour.as_usize()]; sdur = try_from_hours(value) .and_then(|nanos| sdur.checked_add(nanos)) - .ok_or_else(|| { - err!( - "accumulated `std::time::Duration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Hour.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Hour })?; } 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_else(|| { - err!( - "accumulated `std::time::Duration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Minute.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Minute })?; } 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_else(|| { - err!( - "accumulated `std::time::Duration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Second.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Second })?; } 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_else(|| { - err!( - "accumulated `std::time::Duration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Millisecond.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Millisecond })?; } 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_else(|| { - err!( - "accumulated `std::time::Duration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Microsecond.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Microsecond })?; } 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_else(|| { - err!( - "accumulated `std::time::Duration` of `{sdur:?}` \ - overflowed when adding {value} of unit {unit}", - unit = Unit::Nanosecond.singular(), - ) - })?; + .ok_or(E::OverflowForUnit { unit: Unit::Nanosecond })?; } if let Some(fraction) = self.get_fraction()? { @@ -1097,13 +899,7 @@ impl DurationUnits { .checked_add( fractional_duration(min, fraction)?.unsigned_abs(), ) - .ok_or_else(|| { - err!( - "accumulated `std::time::Duration` of `{sdur:?}` \ - overflowed when adding 0.{fraction} of unit {unit}", - unit = min.singular(), - ) - })?; + .ok_or(E::OverflowForUnitFractional { unit: Unit::Hour })?; } Ok(sdur) @@ -1122,7 +918,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(err!("no parsed duration components")); + return Err(Error::from(E::EmptyDuration)); }; Ok((min, max)) } @@ -1143,21 +939,12 @@ impl DurationUnits { } // Otherwise, if a conversion to `i64` fails, then that failure // is correct. - 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() - ) - })?; + let mut value = i64::try_from(value) + .map_err(|_| E::SignedOverflowForUnit { unit })?; if sign.is_negative() { - 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() - ) - })?; + value = value + .checked_neg() + .ok_or(E::SignedOverflowForUnit { unit })?; } Ok(value) } @@ -1258,21 +1045,13 @@ pub(crate) fn parse_temporal_fraction<'i>( } let digits = mkdigits(input); if digits.is_empty() { - return Err(err!( - "found decimal after seconds component, \ - but did not find any decimal digits after decimal", - )); + return Err(Error::from(E::MissingFractionalDigits)); } // 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).map_err(|err| { - err!( - "failed to parse {digits:?} as fractional component \ - (up to 9 digits, nanosecond precision): {err}", - digits = escape::Bytes(digits), - ) - })?; + let nanoseconds = + parse::fraction(digits).context(E::InvalidFraction)?; // OK because parsing is forcefully limited to 9 digits, // which can never be greater than `999_999_99`, // which is less than `u32::MAX`. @@ -1411,18 +1190,10 @@ fn fractional_time_to_span( } if !sdur.is_zero() { let nanos = sdur.as_nanos(); - 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}", - ) - })?; + let nanos64 = + i64::try_from(nanos).map_err(|_| E::InvalidFractionNanos)?; + span = + span.try_nanoseconds(nanos64).context(E::InvalidFractionNanos)?; } Ok(span) @@ -1452,13 +1223,9 @@ fn fractional_time_to_duration( ) -> Result { let sdur = duration_unit_value(unit, value)?; let fraction_dur = fractional_duration(unit, fraction)?; - 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(), - ) - }) + Ok(sdur + .checked_add(fraction_dur) + .ok_or(E::OverflowForUnitFractional { unit })?) } /// Converts the fraction of the given unit to a signed duration. @@ -1488,10 +1255,9 @@ fn fractional_duration( Unit::Millisecond => fraction / t::NANOS_PER_MICRO.value(), Unit::Microsecond => fraction / t::NANOS_PER_MILLI.value(), unit => { - return Err(err!( - "fractional {unit} units are not allowed", - unit = unit.singular(), - )) + return Err(Error::from(E::NotAllowedFractionalUnit { + found: unit, + })); } }; Ok(SignedDuration::from_nanos(nanos)) @@ -1516,17 +1282,13 @@ fn duration_unit_value( Unit::Hour => { let seconds = value .checked_mul(t::SECONDS_PER_HOUR.value()) - .ok_or_else(|| { - err!("converting {value} hours to seconds overflows i64") - })?; + .ok_or(E::ConversionToSecondsFailed { unit: Unit::Hour })?; SignedDuration::from_secs(seconds) } Unit::Minute => { let seconds = value .checked_mul(t::SECONDS_PER_MINUTE.value()) - .ok_or_else(|| { - err!("converting {value} minutes to seconds overflows i64") - })?; + .ok_or(E::ConversionToSecondsFailed { unit: Unit::Minute })?; SignedDuration::from_secs(seconds) } Unit::Second => SignedDuration::from_secs(value), @@ -1534,11 +1296,9 @@ fn duration_unit_value( Unit::Microsecond => SignedDuration::from_micros(value), Unit::Nanosecond => SignedDuration::from_nanos(value), unsupported => { - return Err(err!( - "parsing {unit} units into a `SignedDuration` is not supported \ - (perhaps try parsing into a `Span` instead)", - unit = unsupported.singular(), - )); + return Err(Error::from(E::NotAllowedCalendarUnit { + unit: unsupported, + })) } }; Ok(sdur) @@ -1551,43 +1311,30 @@ mod tests { use super::*; #[test] - fn decimal() { - let x = DecimalFormatter::new().format_signed(i64::MIN); + fn integer() { + let x = IntegerFormatter::new().format_signed(i64::MIN); assert_eq!(x.as_str(), "-9223372036854775808"); - let x = DecimalFormatter::new().format_signed(i64::MIN + 1); + let x = IntegerFormatter::new().format_signed(i64::MIN + 1); assert_eq!(x.as_str(), "-9223372036854775807"); - let x = DecimalFormatter::new().format_signed(i64::MAX); + let x = IntegerFormatter::new().format_signed(i64::MAX); assert_eq!(x.as_str(), "9223372036854775807"); - let x = - DecimalFormatter::new().force_sign(true).format_signed(i64::MAX); - assert_eq!(x.as_str(), "+9223372036854775807"); - - let x = DecimalFormatter::new().format_signed(0); + let x = IntegerFormatter::new().format_signed(0); assert_eq!(x.as_str(), "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); + let x = IntegerFormatter::new().padding(4).format_signed(0); assert_eq!(x.as_str(), "0000"); - let x = DecimalFormatter::new().padding(4).format_signed(789); + let x = IntegerFormatter::new().padding(4).format_signed(789); assert_eq!(x.as_str(), "0789"); - let x = DecimalFormatter::new().padding(4).format_signed(-789); + 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"); + let x = IntegerFormatter::new().padding(4).format_signed(789); + assert_eq!(x.as_str(), "0789"); } #[test] diff --git a/src/logging.rs b/src/logging.rs index b0eb6c7..9a4076b 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<(), crate::Error> { + pub(crate) fn init() -> Result<(), log::SetLoggerError> { #[cfg(all(feature = "std", feature = "logging"))] { - log::set_logger(LOGGER).map_err(crate::Error::adhoc)?; + log::set_logger(LOGGER)?; log::set_max_level(log::LevelFilter::Trace); Ok(()) } diff --git a/src/now.rs b/src/now.rs index 6865850..c9725c3 100644 --- a/src/now.rs +++ b/src/now.rs @@ -83,15 +83,16 @@ 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: \ - subtracting {duration:?} from Unix epoch overflowed" + "failed to get current time from Javascript date: \ + arithmetic on Unix epoch overflowed" ) }; timestamp diff --git a/src/shared/posix.rs b/src/shared/posix.rs index 2f5a7bd..62c58ef 100644 --- a/src/shared/posix.rs +++ b/src/shared/posix.rs @@ -3,8 +3,6 @@ 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,8 +15,9 @@ 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. - #[cfg(feature = "alloc")] - pub fn parse(bytes: &[u8]) -> Result, Error> { + pub fn parse( + bytes: &[u8], + ) -> Result, PosixTimeZoneError> { // 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 @@ -31,10 +30,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]), Error> { + ) -> Result<(PosixTimeZone, &'b [u8]), PosixTimeZoneError> + { let parser = Parser { ianav3plus: true, ..Parser::new(bytes) }; parser.parse_prefix() } @@ -344,12 +343,11 @@ impl + Debug> PosixTimeZone { impl> core::fmt::Display for PosixTimeZone { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!( + core::fmt::Display::fmt( + &AbbreviationDisplay(self.std_abbrev.as_ref()), 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)?; } @@ -363,22 +361,29 @@ impl> PosixDst { std_offset: &PosixOffset, f: &mut core::fmt::Formatter, ) -> core::fmt::Result { - write!(f, "{}", AbbreviationDisplay(self.abbrev.as_ref()))?; + core::fmt::Display::fmt( + &AbbreviationDisplay(self.abbrev.as_ref()), + f, + )?; // 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 { - write!(f, "{}", self.offset)?; + core::fmt::Display::fmt(&self.offset, f)?; } - write!(f, ",{}", self.rule)?; + f.write_str(",")?; + core::fmt::Display::fmt(&self.rule, f)?; Ok(()) } } impl core::fmt::Display for PosixRule { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{},{}", self.start, self.end) + core::fmt::Display::fmt(&self.start, f)?; + f.write_str(",")?; + core::fmt::Display::fmt(&self.end, f)?; + Ok(()) } } @@ -429,11 +434,12 @@ impl PosixDayTime { impl core::fmt::Display for PosixDayTime { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", self.date)?; + core::fmt::Display::fmt(&self.date, f)?; // This is the default time, so don't write it if we // don't need to. if self.time != PosixTime::DEFAULT { - write!(f, "/{}", self.time)?; + f.write_str("/")?; + core::fmt::Display::fmt(&self.time, f)?; } Ok(()) } @@ -501,10 +507,19 @@ impl PosixDay { impl core::fmt::Display for PosixDay { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { - PosixDay::JulianOne(n) => write!(f, "J{n}"), - PosixDay::JulianZero(n) => write!(f, "{n}"), + PosixDay::JulianOne(n) => { + f.write_str("J")?; + core::fmt::Display::fmt(&n, f) + } + PosixDay::JulianZero(n) => core::fmt::Display::fmt(&n, f), PosixDay::WeekdayOfMonth { month, week, weekday } => { - write!(f, "M{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(()) } } } @@ -517,7 +532,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() { - write!(f, "-")?; + f.write_str("-")?; // The default is positive, so when // positive, we write nothing. } @@ -525,7 +540,7 @@ impl core::fmt::Display for PosixTime { let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - write!(f, "{h}")?; + core::fmt::Display::fmt(&h, f)?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -548,13 +563,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 { - write!(f, "-")?; + f.write_str("-")?; } let second = self.second.unsigned_abs(); let h = second / 3600; let m = (second / 60) % 60; let s = second % 60; - write!(f, "{h}")?; + core::fmt::Display::fmt(&h, f)?; if m != 0 || s != 0 { write!(f, ":{m:02}")?; if s != 0 { @@ -576,9 +591,11 @@ 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 == '-') { - write!(f, "<{s}>") + f.write_str("<")?; + core::fmt::Display::fmt(&s, f)?; + f.write_str(">") } else { - write!(f, "{s}") + core::fmt::Display::fmt(&s, f) } } } @@ -672,15 +689,12 @@ 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, Error> { + fn parse( + &self, + ) -> Result, PosixTimeZoneError> { let (time_zone, remaining) = self.parse_prefix()?; if !remaining.is_empty() { - 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), - )); + return Err(ErrorKind::FoundRemaining.into()); } Ok(time_zone) } @@ -689,7 +703,8 @@ impl<'s> Parser<'s> { /// returns the remaining input. fn parse_prefix( &self, - ) -> Result<(PosixTimeZone, &'s [u8]), Error> { + ) -> Result<(PosixTimeZone, &'s [u8]), PosixTimeZoneError> + { let time_zone = self.parse_posix_time_zone()?; Ok((time_zone, self.remaining())) } @@ -700,18 +715,14 @@ impl<'s> Parser<'s> { /// TZ string. fn parse_posix_time_zone( &self, - ) -> Result, Error> { + ) -> Result, PosixTimeZoneError> { if self.is_done() { - return Err(err!( - "an empty string is not a valid POSIX time zone" - )); + return Err(ErrorKind::Empty.into()); } - 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 std_abbrev = + self.parse_abbreviation().map_err(ErrorKind::AbbreviationStd)?; + let std_offset = + self.parse_posix_offset().map_err(ErrorKind::OffsetStd)?; let mut dst = None; if !self.is_done() && (self.byte().is_ascii_alphabetic() || self.byte() == b'<') @@ -732,49 +743,30 @@ impl<'s> Parser<'s> { fn parse_posix_dst( &self, std_offset: &PosixOffset, - ) -> Result, Error> { - let abbrev = self - .parse_abbreviation() - .map_err(|e| err!("failed to parse DST abbreviation: {e}"))?; + ) -> Result, PosixTimeZoneError> { + let abbrev = + self.parse_abbreviation().map_err(ErrorKind::AbbreviationDst)?; if self.is_done() { - return Err(err!( - "found DST abbreviation `{abbrev}`, but no transition \ - rule (this is technically allowed by POSIX, but has \ - unspecified behavior)", - )); + return Err(ErrorKind::FoundDstNoRule.into()); } // 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(|e| err!("failed to parse DST offset: {e}"))?; + offset = + self.parse_posix_offset().map_err(ErrorKind::OffsetDst)?; if self.is_done() { - 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, - )); + return Err(ErrorKind::FoundDstNoRuleWithOffset.into()); } } if self.byte() != b',' { - return Err(err!( - "after parsing DST offset in POSIX time zone string, \ - found `{}` but expected a ','", - Byte(self.byte()), - )); + return Err(ErrorKind::ExpectedCommaAfterDst.into()); } if !self.bump() { - return Err(err!( - "after parsing DST offset in POSIX time zone string, \ - found end of string after a trailing ','", - )); + return Err(ErrorKind::FoundEndAfterComma.into()); } - let rule = self.parse_rule()?; + let rule = self.parse_rule().map_err(ErrorKind::Rule)?; Ok(PosixDst { abbrev, offset, rule }) } @@ -790,18 +782,17 @@ 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(err!( - "found opening '<' quote for abbreviation in \ - POSIX time zone string, and expected a name \ - following it, but found the end of string instead" + return Err(AbbreviationError::Quoted( + QuotedAbbreviationError::UnexpectedEndAfterOpening, )); } - self.parse_quoted_abbreviation() + self.parse_quoted_abbreviation().map_err(AbbreviationError::Quoted) } else { self.parse_unquoted_abbreviation() + .map_err(AbbreviationError::Unquoted) } } @@ -816,19 +807,16 @@ 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(err!( - "expected abbreviation with at most {} bytes, \ - but found a longer abbreviation beginning with `{}`", - Abbreviation::capacity(), - Bytes(&self.tz[start..][..i]), - )); + return Err(UnquotedAbbreviationError::TooLong); } if !self.bump() { break; @@ -843,18 +831,10 @@ 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. - err!( - "found abbreviation `{}`, but it is not valid UTF-8", - Bytes(&self.tz[start..end]), - ) + UnquotedAbbreviationError::InvalidUtf8 })?; if abbrev.len() < 3 { - return Err(err!( - "expected abbreviation with 3 or more bytes, but found \ - abbreviation {:?} with {} bytes", - abbrev, - abbrev.len(), - )); + return Err(UnquotedAbbreviationError::TooShort); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -872,7 +852,9 @@ 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() @@ -882,12 +864,7 @@ impl<'s> Parser<'s> { break; } if i >= Abbreviation::capacity() { - return Err(err!( - "expected abbreviation with at most {} bytes, \ - but found a longer abbreviation beginning with `{}`", - Abbreviation::capacity(), - Bytes(&self.tz[start..][..i]), - )); + return Err(QuotedAbbreviationError::TooLong); } if !self.bump() { break; @@ -902,33 +879,17 @@ 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. - err!( - "found abbreviation `{}`, but it is not valid UTF-8", - Bytes(&self.tz[start..end]), - ) + QuotedAbbreviationError::InvalidUtf8 })?; if self.is_done() { - return Err(err!( - "found non-empty quoted abbreviation {abbrev:?}, but \ - did not find expected end-of-quoted abbreviation \ - '>' character", - )); + return Err(QuotedAbbreviationError::UnexpectedEnd); } if self.byte() != b'>' { - return Err(err!( - "found non-empty quoted abbreviation {abbrev:?}, but \ - found `{}` instead of end-of-quoted abbreviation '>' \ - character", - Byte(self.byte()), - )); + return Err(QuotedAbbreviationError::UnexpectedLastByte); } self.bump(); if abbrev.len() < 3 { - return Err(err!( - "expected abbreviation with 3 or more bytes, but found \ - abbreviation {abbrev:?} with {} bytes", - abbrev.len(), - )); + return Err(QuotedAbbreviationError::TooShort); } // OK because we verified above that the abbreviation // does not exceed `Abbreviation::capacity`. @@ -943,30 +904,18 @@ 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() - .map_err(|e| { - err!( - "failed to parse sign for time offset \ - in POSIX time zone string: {e}", - ) - })? - .unwrap_or(1); + fn parse_posix_offset(&self) -> Result { + let sign = self.parse_optional_sign()?.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(err!( - "incomplete time in POSIX timezone (missing minutes)", - )); + return Err(PosixOffsetError::IncompleteMinutes); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(err!( - "incomplete time in POSIX timezone (missing seconds)", - )); + return Err(PosixOffsetError::IncompleteSeconds); } second = self.parse_second()?; } @@ -997,19 +946,16 @@ 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(|e| { - err!("failed to parse start of DST transition rule: {e}") - })?; + fn parse_rule(&self) -> Result { + let start = self + .parse_posix_datetime() + .map_err(PosixRuleError::DateTimeStart)?; if self.maybe_byte() != Some(b',') || !self.bump() { - return Err(err!( - "expected end of DST rule after parsing the start \ - of the DST rule" - )); + return Err(PosixRuleError::ExpectedEnd); } - let end = self.parse_posix_datetime().map_err(|e| { - err!("failed to parse end of DST transition rule: {e}") - })?; + let end = self + .parse_posix_datetime() + .map_err(PosixRuleError::DateTimeEnd)?; Ok(PosixRule { start, end }) } @@ -1021,7 +967,9 @@ 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, @@ -1030,10 +978,7 @@ impl<'s> Parser<'s> { return Ok(daytime); } if !self.bump() { - return Err(err!( - "expected time specification after '/' following a date - specification in a POSIX time zone DST transition rule", - )); + return Err(PosixDateTimeError::ExpectedTime); } daytime.time = self.parse_posix_time()?; Ok(daytime) @@ -1052,16 +997,11 @@ 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(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" - )); + return Err(PosixDateError::ExpectedJulianNoLeap); } Ok(PosixDay::JulianOne(self.parse_posix_julian_day_no_leap()?)) } @@ -1070,22 +1010,12 @@ impl<'s> Parser<'s> { )), b'M' => { if !self.bump() { - 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" - )); + return Err(PosixDateError::ExpectedMonthWeekWeekday); } let (month, week, weekday) = self.parse_weekday_of_month()?; Ok(PosixDay::WeekdayOfMonth { month, week, weekday }) } - _ => 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()), - )), + _ => Err(PosixDateError::UnexpectedByte), } } @@ -1095,22 +1025,16 @@ 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(|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" - ) - })?; + .map_err(PosixJulianNoLeapError::Parse)?; + let number = i16::try_from(number) + .map_err(|_| PosixJulianNoLeapError::Range)?; if !(1 <= number && number <= 365) { - return Err(err!( - "parsed one based Julian day `{number}`, \ - but one based Julian day in POSIX time zone \ - must be in range 1..=365", - )); + return Err(PosixJulianNoLeapError::Range); } Ok(number) } @@ -1121,22 +1045,16 @@ 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(|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" - ) - })?; + .map_err(PosixJulianLeapError::Parse)?; + let number = + i16::try_from(number).map_err(|_| PosixJulianLeapError::Range)?; if !(0 <= number && number <= 365) { - return Err(err!( - "parsed zero based Julian day `{number}`, \ - but zero based Julian day in POSIX time zone \ - must be in range 0..=365", - )); + return Err(PosixJulianLeapError::Range); } Ok(number) } @@ -1150,31 +1068,22 @@ 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), Error> { + fn parse_weekday_of_month( + &self, + ) -> Result<(i8, i8, i8), WeekdayOfMonthError> { let month = self.parse_month()?; if self.maybe_byte() != Some(b'.') { - return Err(err!( - "expected '.' after month `{month}` in \ - POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedDotAfterMonth); } if !self.bump() { - return Err(err!( - "expected week after month `{month}` in \ - POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedWeekAfterMonth); } let week = self.parse_week()?; if self.maybe_byte() != Some(b'.') { - return Err(err!( - "expected '.' after week `{week}` in POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedDotAfterWeek); } if !self.bump() { - return Err(err!( - "expected day-of-week after week `{week}` in \ - POSIX time zone rule" - )); + return Err(WeekdayOfMonthError::ExpectedDayOfWeekAfterWeek); } let weekday = self.parse_weekday()?; Ok((month, week, weekday)) @@ -1186,17 +1095,9 @@ 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() - .map_err(|e| { - err!( - "failed to parse sign for transition time \ - in POSIX time zone string: {e}", - ) - })? - .unwrap_or(1); + let sign = self.parse_optional_sign()?.unwrap_or(1); let hour = self.parse_hour_ianav3plus()?; (sign, hour) } else { @@ -1205,18 +1106,12 @@ impl<'s> Parser<'s> { let (mut minute, mut second) = (0, 0); if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(err!( - "incomplete transition time in \ - POSIX time zone string (missing minutes)", - )); + return Err(PosixTimeError::IncompleteMinutes); } minute = self.parse_minute()?; if self.maybe_byte() == Some(b':') { if !self.bump() { - return Err(err!( - "incomplete transition time in \ - POSIX time zone string (missing seconds)", - )); + return Err(PosixTimeError::IncompleteSeconds); } second = self.parse_second()?; } @@ -1241,19 +1136,13 @@ 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)?; - let number = i8::try_from(number).map_err(|_| { - err!( - "month `{number}` in POSIX time zone \ - does not fit into 8-bit integer" - ) - })?; + 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)?; if !(1 <= number && number <= 12) { - return Err(err!( - "parsed month `{number}`, but month in \ - POSIX time zone must be in range 1..=12", - )); + return Err(MonthError::Range); } Ok(number) } @@ -1262,19 +1151,14 @@ 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)?; - let number = i8::try_from(number).map_err(|_| { - err!( - "week `{number}` in POSIX time zone \ - does not fit into 8-bit integer" - ) - })?; + 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)?; if !(1 <= number && number <= 5) { - return Err(err!( - "parsed week `{number}`, but week in \ - POSIX time zone must be in range 1..=5" - )); + return Err(WeekOfMonthError::Range); } Ok(number) } @@ -1286,20 +1170,13 @@ 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)?; - let number = i8::try_from(number).map_err(|_| { - err!( - "weekday `{number}` in POSIX time zone \ - does not fit into 8-bit integer" - ) - })?; + 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)?; if !(0 <= number && number <= 6) { - return Err(err!( - "parsed weekday `{number}`, but weekday in \ - POSIX time zone must be in range `0..=6` \ - (with `0` corresponding to Sunday)", - )); + return Err(WeekdayError::Range); } Ok(number) } @@ -1315,27 +1192,20 @@ 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(|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" - ) - })?; + .map_err(HourIanaError::Parse)?; + let number = + i16::try_from(number).map_err(|_| HourIanaError::Range)?; 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(err!( - "parsed hour `{number}`, but hour in IANA v3+ \ - POSIX time zone must be in range `-167..=167`", - )); + return Err(HourIanaError::Range); } Ok(number) } @@ -1349,21 +1219,14 @@ 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(|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" - ) - })?; + .map_err(HourPosixError::Parse)?; + let number = + i8::try_from(number).map_err(|_| HourPosixError::Range)?; if !(0 <= number && number <= 24) { - return Err(err!( - "parsed hour `{number}`, but hour in \ - POSIX time zone must be in range `0..=24`", - )); + return Err(HourPosixError::Range); } Ok(number) } @@ -1375,21 +1238,13 @@ 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(|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" - ) - })?; + .map_err(MinuteError::Parse)?; + let number = i8::try_from(number).map_err(|_| MinuteError::Range)?; if !(0 <= number && number <= 59) { - return Err(err!( - "parsed minute `{number}`, but minute in \ - POSIX time zone must be in range `0..=59`", - )); + return Err(MinuteError::Range); } Ok(number) } @@ -1401,21 +1256,13 @@ 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(|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" - ) - })?; + .map_err(SecondError::Parse)?; + let number = i8::try_from(number).map_err(|_| SecondError::Range)?; if !(0 <= number && number <= 59) { - return Err(err!( - "parsed second `{number}`, but second in \ - POSIX time zone must be in range `0..=59`", - )); + return Err(SecondError::Range); } Ok(number) } @@ -1431,27 +1278,20 @@ 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 i in 0..n { + for _ in 0..n { if self.is_done() { - return Err(err!("expected {n} digits, but found {i}")); + return Err(NumberError::ExpectedLength); } let byte = self.byte(); let digit = match byte.checked_sub(b'0') { None => { - return Err(err!( - "invalid digit, expected 0-9 but got {}", - Byte(byte), - )); + return Err(NumberError::InvalidDigit); } Some(digit) if digit > 9 => { - return Err(err!( - "invalid digit, expected 0-9 but got {}", - Byte(byte), - )) + return Err(NumberError::InvalidDigit); } Some(digit) => { debug_assert!((0..=9).contains(&digit)); @@ -1461,12 +1301,7 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or_else(|| { - err!( - "number `{}` too big to parse into 64-bit integer", - Bytes(&self.tz[start..][..i]), - ) - })?; + .ok_or(NumberError::TooBig)?; self.bump(); } Ok(number) @@ -1478,14 +1313,16 @@ 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(err!("invalid number, no digits found")); + return Err(NumberError::Empty); } break; } @@ -1493,12 +1330,7 @@ impl<'s> Parser<'s> { number = number .checked_mul(10) .and_then(|n| n.checked_add(digit)) - .ok_or_else(|| { - err!( - "number `{}` too big to parse into 64-bit integer", - Bytes(&self.tz[start..][..i]), - ) - })?; + .ok_or(NumberError::TooBig)?; self.bump(); } Ok(number) @@ -1511,26 +1343,20 @@ 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, Error> { + fn parse_optional_sign(&self) -> Result, OptionalSignError> { if self.is_done() { return Ok(None); } Ok(match self.byte() { b'-' => { if !self.bump() { - return Err(err!( - "expected digit after '-' sign, \ - but got end of input", - )); + return Err(OptionalSignError::ExpectedDigitAfterMinus); } Some(-1) } b'+' => { if !self.bump() { - return Err(err!( - "expected digit after '+' sign, \ - but got end of input", - )); + return Err(OptionalSignError::ExpectedDigitAfterPlus); } Some(1) } @@ -1588,12 +1414,717 @@ impl<'s> Parser<'s> { } } -// Tests require parsing, and parsing requires alloc. -#[cfg(feature = "alloc")] +#[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", + ), + } + } +} + #[cfg(test)] mod tests { - use alloc::string::ToString; - use super::*; fn posix_time_zone( @@ -1601,21 +2132,26 @@ mod tests { ) -> PosixTimeZone { let input = input.as_ref(); let tz = PosixTimeZone::parse(input).unwrap(); - // 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()); + #[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()); + } tz } diff --git a/src/shared/tzif.rs b/src/shared/tzif.rs index cef139e..ef74b85 100644 --- a/src/shared/tzif.rs +++ b/src/shared/tzif.rs @@ -3,8 +3,6 @@ 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 +55,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 2036. See above.) +// (Although we won't go above `FATTEN_UP_TO_YEAR`. See above.) const FATTEN_MAX_TRANSITIONS: usize = 300; impl TzifOwned { @@ -78,11 +76,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(|e| err!("failed to parse 32-bit header: {e}"))?; + let (header32, rest) = + Header::parse(4, bytes).map_err(TzifErrorKind::Header32)?; let (mut tzif, rest) = if header32.version == 0 { TzifOwned::parse32(name, header32, rest)? } else { @@ -115,7 +113,7 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> Result<(TzifOwned, &'b [u8]), Error> { + ) -> Result<(TzifOwned, &'b [u8]), TzifError> { let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -146,14 +144,11 @@ impl TzifOwned { name: Option, header32: Header, bytes: &'b [u8], - ) -> 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}"))?; + ) -> 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)?; let mut tzif = TzifOwned { fixed: TzifFixed { name, @@ -188,9 +183,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TzifError> { let (bytes, rest) = try_split_at( - "transition times data block", + SplitAtError::TransitionTimes, bytes, header.transition_times_len()?, )?; @@ -230,8 +225,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; @@ -246,21 +241,17 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TransitionTypeError> { let (bytes, rest) = try_split_at( - "transition types data block", + SplitAtError::TransitionTypes, 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(err!( - "found transition type index {type_index}, - but there are only {} local time types", - header.tzh_typecnt, - )); + return Err(TransitionTypeError::ExceedsLocalTimeTypes); } self.transitions.infos[transition_index].type_index = type_index; } @@ -271,9 +262,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TzifError> { let (bytes, rest) = try_split_at( - "local time types data block", + SplitAtError::LocalTimeTypes, bytes, header.local_time_types_len()?, )?; @@ -281,8 +272,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(err!( - "found local time type with out-of-bounds offset: {offset}" + return Err(TzifError::from( + LocalTimeTypeError::InvalidOffset { offset }, )); } let is_dst = chunk[4] == 1; @@ -302,49 +293,30 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TimeZoneDesignatorError> { let (bytes, rest) = try_split_at( - "time zone designations data block", + SplitAtError::TimeZoneDesignations, bytes, - header.time_zone_designations_len()?, + header.time_zone_designations_len(), )?; - self.fixed.designations = - String::from_utf8(bytes.to_vec()).map_err(|_| { - err!( - "time zone designations are not valid UTF-8: {:?}", - Bytes(bytes), - ) - })?; + self.fixed.designations = String::from_utf8(bytes.to_vec()) + .map_err(|_| TimeZoneDesignatorError::InvalidUtf8)?; // Holy hell, this is brutal. The boundary conditions are crazy. - for (i, typ) in self.types.iter_mut().enumerate() { + for typ in self.types.iter_mut() { let start = usize::from(typ.designation.0); - 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", - ) - })?; + 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)?; } Ok(rest) } @@ -357,9 +329,9 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], TzifError> { let (bytes, rest) = try_split_at( - "leap seconds data block", + SplitAtError::LeapSeconds, bytes, header.leap_second_len()?, )?; @@ -378,7 +350,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 @@ -392,16 +364,16 @@ impl TzifOwned { &mut self, header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], IndicatorError> { let (std_wall_bytes, rest) = try_split_at( - "standard/wall indicators data block", + SplitAtError::StandardWallIndicators, bytes, - header.standard_wall_len()?, + header.standard_wall_len(), )?; let (ut_local_bytes, rest) = try_split_at( - "UT/local indicators data block", + SplitAtError::UTLocalIndicators, 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 @@ -409,14 +381,8 @@ 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. - 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", - )); - } + if ut_local_bytes.iter().any(|&byte| byte != 0) { + return Err(IndicatorError::UtLocalNonZero); } } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() { for (i, &byte) in std_wall_bytes.iter().enumerate() { @@ -427,10 +393,7 @@ impl TzifOwned { } else if byte == 1 { TzifIndicator::LocalStandard } else { - return Err(err!( - "found invalid std/wall indicator '{byte}' for \ - local time type {i}, it must be 0 or 1", - )); + return Err(IndicatorError::InvalidStdWallIndicator); }; } } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() { @@ -444,18 +407,9 @@ impl TzifOwned { (1, 0) => TzifIndicator::LocalStandard, (1, 1) => TzifIndicator::UTStandard, (0, 1) => { - 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::InvalidUtWallCombination); } + _ => return Err(IndicatorError::InvalidCombination), }; } } else { @@ -472,42 +426,31 @@ impl TzifOwned { &mut self, _header: &Header, bytes: &'b [u8], - ) -> Result<&'b [u8], Error> { + ) -> Result<&'b [u8], FooterError> { if bytes.is_empty() { - return Err(err!( - "invalid V2+ TZif footer, expected \\n, \ - but found unexpected end of data", - )); + return Err(FooterError::UnexpectedEnd); } if bytes[0] != b'\n' { - return Err(err!( - "invalid V2+ TZif footer, expected {:?}, but found {:?}", - Byte(b'\n'), - Byte(bytes[0]), - )); + return Err(FooterError::MismatchEnd); } 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 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 nlat = toscan + .iter() + .position(|&b| b == b'\n') + .ok_or(FooterError::TerminatorNotFound)?; 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 the GNU tooling allow it via the `TZ` environment variable + // that GNU tooling allows 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(|e| err!("{e}"))?; + let posix_tz = PosixTimeZone::parse(bytes) + .map_err(FooterError::InvalidPosixTz)?; self.fixed.posix_tz = Some(posix_tz); } Ok(&rest[1..]) @@ -520,7 +463,9 @@ 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<(), Error> { + fn verify_posix_time_zone_consistency( + &self, + ) -> Result<(), InconsistentPosixTimeZoneError> { // 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 @@ -547,35 +492,13 @@ impl TzifOwned { let (ioff, abbrev, is_dst) = tz.to_offset_info(ITimestamp::from_second(*last)); if ioff.second != typ.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, - )); + return Err(InconsistentPosixTimeZoneError::Offset); } if is_dst != typ.is_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, - )); + return Err(InconsistentPosixTimeZoneError::Dst); } if abbrev != self.designation(&typ) { - 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, - )); + return Err(InconsistentPosixTimeZoneError::Designation); } Ok(()) } @@ -591,7 +514,6 @@ 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); @@ -866,14 +788,14 @@ impl Header { fn parse( time_size: usize, bytes: &[u8], - ) -> Result<(Header, &[u8]), Error> { + ) -> Result<(Header, &[u8]), HeaderError> { assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8"); if bytes.len() < 44 { - return Err(err!("invalid header: too short")); + return Err(HeaderError::TooShort); } let (magic, rest) = bytes.split_at(4); if magic != b"TZif" { - return Err(err!("invalid header: magic bytes mismatch")); + return Err(HeaderError::MismatchMagic); } let (version, rest) = rest.split_at(1); let (_reserved, rest) = rest.split_at(15); @@ -885,40 +807,42 @@ 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| 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}"))?; + 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 } + })?; if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt { - return Err(err!( - "expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \ - or equal to tzh_typecnt={tzh_typecnt}", - )); + return Err(HeaderError::MismatchUtType); } if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt { - return Err(err!( - "expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \ - or equal to tzh_typecnt={tzh_typecnt}", - )); + return Err(HeaderError::MismatchStdType); } if tzh_typecnt < 1 { - return Err(err!( - "expected tzh_typecnt={tzh_typecnt} to be at least 1", - )); + return Err(HeaderError::ZeroType); } if tzh_charcnt < 1 { - return Err(err!( - "expected tzh_charcnt={tzh_charcnt} to be at least 1", - )); + return Err(HeaderError::ZeroChar); } let header = Header { @@ -949,64 +873,514 @@ 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_else(|| { - err!( - "length of data block in V{} tzfile is too big", - self.version - ) - }) + .ok_or(HeaderError::InvalidDataBlock { version: self.version }) } - 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_times_len(&self) -> Result { + self.tzh_timecnt + .checked_mul(self.time_size) + .ok_or(HeaderError::InvalidTimeCount) } - fn transition_types_len(&self) -> Result { - Ok(self.tzh_timecnt) + fn transition_types_len(&self) -> usize { + self.tzh_timecnt } - 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 local_time_types_len(&self) -> Result { + self.tzh_typecnt.checked_mul(6).ok_or(HeaderError::InvalidTypeCount) } - fn time_zone_designations_len(&self) -> Result { - Ok(self.tzh_charcnt) + fn time_zone_designations_len(&self) -> usize { + 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_else(|| { - err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt) - }) + self.tzh_leapcnt + .checked_mul(record_len) + .ok_or(HeaderError::InvalidLeapSecondCount) } - fn standard_wall_len(&self) -> Result { - Ok(self.tzh_ttisstdcnt) + fn standard_wall_len(&self) -> usize { + self.tzh_ttisstdcnt } - fn ut_local_len(&self) -> Result { - Ok(self.tzh_ttisutcnt) + 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, + ) } } @@ -1016,16 +1390,12 @@ impl Header { /// 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: &'static str, + what: SplitAtError, bytes: &'b [u8], at: usize, -) -> Result<(&'b [u8], &'b [u8]), Error> { +) -> Result<(&'b [u8], &'b [u8]), SplitAtError> { if at > bytes.len() { - Err(err!( - "expected at least {at} bytes for {what}, \ - but found only {} bytes", - bytes.len(), - )) + Err(what) } else { Ok(bytes.split_at(at)) } @@ -1042,14 +1412,9 @@ 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(|_| { - err!( - "failed to parse integer {n} (too big, max allowed is {}", - usize::MAX - ) - }) + usize::try_from(n).map_err(|_| U32UsizeError) } /// 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 deleted file mode 100644 index 36597e2..0000000 --- a/src/shared/util/error.rs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 1593f90..0000000 --- a/src/shared/util/escape.rs +++ /dev/null @@ -1,88 +0,0 @@ -/*! -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 5336eca..291289f 100644 --- a/src/shared/util/itime.rs +++ b/src/shared/util/itime.rs @@ -22,8 +22,6 @@ 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,11 +139,13 @@ 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( - || err!("adding `{seconds}s` to datetime overflowed"), - )?; + ) -> Result { + let day_second = self + .time + .to_second() + .second + .checked_add(seconds) + .ok_or_else(|| RangeError::DateTimeSeconds)?; 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 { - const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; - const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; + pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; + pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; /// Converts days since the Unix epoch to a Gregorian date. /// @@ -217,20 +217,17 @@ 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(|| { - err!("adding `{amount}` to epoch day `{epoch_day}` overflowed i32") - })?; + let sum = epoch_day + .checked_add(amount) + .ok_or_else(|| RangeError::EpochDayI32)?; let ret = IEpochDay { epoch_day: sum }; if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) { - 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, - )); + return Err(RangeError::EpochDayDays); } Ok(ret) } @@ -258,14 +255,11 @@ 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(err!( - "day={day} is out of range for year={year} \ - and month={month}, must be in range 1..={max_day}", - )); + return Err(RangeError::DateInvalidDays { year, month }); } } Ok(IDate { year, month, day }) @@ -281,37 +275,22 @@ impl IDate { pub(crate) fn from_day_of_year( year: i16, day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 366) { - 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), - )); + return Err(RangeError::DateInvalidDayOfYear { year }); } let start = IDate { year, month: 1, day: 1 }.to_epoch_day(); let end = start .checked_add(i32::from(day) - 1) - .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, - ) - })? + // This can only happen when `year=9999` and `day=366`. + .map_err(|_| RangeError::DayOfYear)? .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(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), - )); + return Err(RangeError::DateInvalidDayOfYear { year }); } Ok(end) } @@ -327,12 +306,9 @@ 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(err!( - "day-of-year={day} is out of range for year={year}, \ - must be in range 1..=365", - )); + return Err(RangeError::DateInvalidDayOfYearNoLeap); } if day >= 60 && is_leap_year(year) { day += 1; @@ -390,12 +366,9 @@ impl IDate { &self, nth: i8, weekday: IWeekday, - ) -> Result { + ) -> Result { if nth == 0 || !(-5 <= nth && nth <= 5) { - return Err(err!( - "got nth weekday of `{nth}`, but \ - must be non-zero and in range `-5..=5`", - )); + return Err(RangeError::NthWeekdayOfMonth); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -412,13 +385,10 @@ impl IDate { // of `Day`, we can't let this boundary condition escape. So we // check it here. if day < 1 { - 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), - )); + return Err(RangeError::DateInvalidDays { + year: self.year, + month: self.month, + }); } IDate::try_new(self.year, self.month, day) } @@ -426,16 +396,12 @@ 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(err!( - "returning yesterday for -9999-01-01 is not \ - possible because it is less than Jiff's supported - minimum date", - )); + return Err(RangeError::Yesterday); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -448,16 +414,12 @@ 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(err!( - "returning tomorrow for 9999-12-31 is not \ - possible because it is greater than Jiff's supported - maximum date", - )); + return Err(RangeError::Tomorrow); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -469,34 +431,20 @@ 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(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, - )); + return Err(RangeError::YearPrevious); } 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(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, - )); + return Err(RangeError::YearNext); } Ok(year) } @@ -506,7 +454,7 @@ impl IDate { pub(crate) fn checked_add_days( &self, amount: i32, - ) -> Result { + ) -> Result { match amount { 0 => Ok(*self), -1 => self.yesterday(), @@ -718,6 +666,84 @@ 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. @@ -920,4 +946,20 @@ 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 a31dc54..971f365 100644 --- a/src/shared/util/mod.rs +++ b/src/shared/util/mod.rs @@ -1,5 +1,2 @@ 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 deleted file mode 100644 index 1cccc47..0000000 --- a/src/shared/util/utf8.rs +++ /dev/null @@ -1,37 +0,0 @@ -/// 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 f70f1ab..852ea93 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::{err, ErrorContext}, + error::{signed_duration::Error as E, ErrorContext}, fmt::{friendly, temporal}, tz::Offset, - util::{escape, rangeint::TryRFrom, t}, + util::{rangeint::TryRFrom, t}, Error, RoundMode, Timestamp, Unit, Zoned, }; @@ -65,8 +65,7 @@ const MINS_PER_HOUR: i64 = 60; /// /// assert_eq!( /// "P1d".parse::().unwrap_err().to_string(), -/// "failed to parse \"P1d\" as an ISO 8601 duration string: \ -/// parsing ISO 8601 duration into a `SignedDuration` requires that \ +/// "parsing ISO 8601 duration in this context requires that \ /// the duration contain a time component and no components of days or \ /// greater", /// ); @@ -1456,24 +1455,13 @@ impl SignedDuration { #[inline] pub fn try_from_secs_f64(secs: f64) -> Result { if !secs.is_finite() { - return Err(err!( - "could not convert non-finite seconds \ - {secs} to signed duration", - )); + return Err(Error::from(E::ConvertNonFinite)); } if secs < (i64::MIN as f64) { - return Err(err!( - "floating point seconds {secs} overflows signed duration \ - minimum value of {:?}", - SignedDuration::MIN, - )); + return Err(Error::slim_range("floating point seconds")); } if secs > (i64::MAX as f64) { - return Err(err!( - "floating point seconds {secs} overflows signed duration \ - maximum value of {:?}", - SignedDuration::MAX, - )); + return Err(Error::slim_range("floating point seconds")); } let mut int_secs = secs.trunc() as i64; @@ -1481,15 +1469,9 @@ 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(|| { - 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_secs = int_secs + .checked_add(increment) + .ok_or_else(|| Error::slim_range("floating point seconds"))?; int_nanos = 0; } Ok(SignedDuration::new_unchecked(int_secs, int_nanos)) @@ -1528,24 +1510,13 @@ impl SignedDuration { #[inline] pub fn try_from_secs_f32(secs: f32) -> Result { if !secs.is_finite() { - return Err(err!( - "could not convert non-finite seconds \ - {secs} to signed duration", - )); + return Err(Error::from(E::ConvertNonFinite)); } if secs < (i64::MIN as f32) { - return Err(err!( - "floating point seconds {secs} overflows signed duration \ - minimum value of {:?}", - SignedDuration::MIN, - )); + return Err(Error::slim_range("floating point seconds")); } if secs > (i64::MAX as f32) { - return Err(err!( - "floating point seconds {secs} overflows signed duration \ - maximum value of {:?}", - SignedDuration::MAX, - )); + return Err(Error::slim_range("floating point seconds")); } let mut int_nanos = (secs.fract() * (NANOS_PER_SEC as f32)).round() as i32; @@ -1553,15 +1524,9 @@ 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(|| { - 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_secs = int_secs + .checked_add(increment) + .ok_or_else(|| Error::slim_range("floating point seconds"))?; int_nanos = 0; } Ok(SignedDuration::new_unchecked(int_secs, int_nanos)) @@ -2023,25 +1988,18 @@ impl SignedDuration { time2: std::time::SystemTime, ) -> Result { match time2.duration_since(time1) { - Ok(dur) => SignedDuration::try_from(dur).with_context(|| { - err!( - "unsigned duration {dur:?} for system time since \ - Unix epoch overflowed signed duration" - ) - }), + Ok(dur) => { + SignedDuration::try_from(dur).context(E::ConvertSystemTime) + } Err(err) => { let dur = err.duration(); - 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") - }) + let dur = SignedDuration::try_from(dur) + .context(E::ConvertSystemTime)?; + dur.checked_neg() + .ok_or_else(|| { + Error::slim_range("signed duration seconds") + }) + .context(E::ConvertSystemTime) } } } @@ -2155,17 +2113,15 @@ impl SignedDuration { /// /// assert_eq!( /// SignedDuration::MAX.round(Unit::Hour).unwrap_err().to_string(), - /// "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`", + /// "rounding signed duration to nearest hour \ + /// resulted in a value outside the supported \ + /// range of a `jiff::SignedDuration`", /// ); /// assert_eq!( /// SignedDuration::MIN.round(Unit::Hour).unwrap_err().to_string(), - /// "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`", + /// "rounding signed duration to nearest hour \ + /// resulted in a value outside the supported \ + /// range of a `jiff::SignedDuration`", /// ); /// ``` /// @@ -2176,9 +2132,9 @@ impl SignedDuration { /// /// assert_eq!( /// SignedDuration::ZERO.round(Unit::Day).unwrap_err().to_string(), - /// "rounding `SignedDuration` failed \ - /// because a calendar unit of days was provided \ - /// (to round by calendar units, you must use a `Span`)", + /// "rounding `jiff::SignedDuration` failed \ + /// because a calendar unit of 'days' was provided \ + /// (to round by calendar units, you must use a `jiff::Span`)", /// ); /// ``` #[inline] @@ -2391,16 +2347,19 @@ impl core::fmt::Debug for SignedDuration { if f.alternate() { if self.subsec_nanos() == 0 { - write!(f, "{}s", self.as_secs()) + core::fmt::Display::fmt(&self.as_secs(), f)?; + f.write_str("s") } else if self.as_secs() == 0 { - write!(f, "{}ns", self.subsec_nanos()) + core::fmt::Display::fmt(&self.subsec_nanos(), f)?; + f.write_str("ns") } else { - write!( + core::fmt::Display::fmt(&self.as_secs(), f)?; + f.write_str("s ")?; + core::fmt::Display::fmt( + &self.subsec_nanos().unsigned_abs(), f, - "{}s {}ns", - self.as_secs(), - self.subsec_nanos().unsigned_abs() - ) + )?; + f.write_str("ns") } } else { friendly::DEFAULT_SPAN_PRINTER @@ -2414,9 +2373,8 @@ impl TryFrom for SignedDuration { type Error = Error; fn try_from(d: Duration) -> Result { - let secs = i64::try_from(d.as_secs()).map_err(|_| { - err!("seconds in unsigned duration {d:?} overflowed i64") - })?; + let secs = i64::try_from(d.as_secs()) + .map_err(|_| Error::slim_range("unsigned duration seconds"))?; // Guaranteed to succeed since 0<=nanos<=999,999,999. let nanos = i32::try_from(d.subsec_nanos()).unwrap(); Ok(SignedDuration::new_unchecked(secs, nanos)) @@ -2429,14 +2387,10 @@ impl TryFrom for Duration { fn try_from(sd: SignedDuration) -> Result { // This isn't needed, but improves error messages. if sd.is_negative() { - return Err(err!( - "cannot convert negative duration `{sd:?}` to \ - unsigned `std::time::Duration`", - )); + return Err(Error::slim_range("negative duration seconds")); } - let secs = u64::try_from(sd.as_secs()).map_err(|_| { - err!("seconds in signed duration {sd:?} overflowed u64") - })?; + let secs = u64::try_from(sd.as_secs()) + .map_err(|_| Error::slim_range("signed duration seconds"))?; // 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. @@ -2771,12 +2725,9 @@ impl SignedDurationRound { /// Does the actual duration rounding. fn round(&self, dur: SignedDuration) -> Result { if self.smallest > Unit::Hour { - 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(), - )); + return Err(Error::from(E::RoundCalendarUnit { + unit: self.smallest, + })); } let nanos = t::NoUnits128::new_unchecked(dur.as_nanos()); let increment = t::NoUnits::new_unchecked(self.increment); @@ -2789,12 +2740,7 @@ impl SignedDurationRound { let seconds = rounded / t::NANOS_PER_SECOND; let seconds = t::NoUnits::try_rfrom("seconds", seconds).map_err(|_| { - 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(), - ) + Error::from(E::RoundOverflowed { unit: self.smallest }) })?; let subsec_nanos = rounded % t::NANOS_PER_SECOND; // OK because % 1_000_000_000 above guarantees that the result fits @@ -2838,25 +2784,22 @@ 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 { - 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 Some((&byte, tail)) = bytes.split_first() else { + return Err(crate::Error::from( + crate::error::fmt::Error::HybridDurationEmpty, )); - } - let mut first = bytes[0]; + }; + 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. if first == b'+' || first == b'-' { - 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), + let Some(&byte) = tail.first() else { + return Err(crate::Error::from( + crate::error::fmt::Error::HybridDurationPrefix { sign: first }, )); - } - first = bytes[1]; + }; + first = byte; } if first == b'P' || first == b'p' { temporal::DEFAULT_SPAN_PARSER.parse_duration(bytes) @@ -3048,15 +2991,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format", + @r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, ); insta::assert_snapshot!( p("+").unwrap_err(), - @"found nothing after sign `+`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format", + @r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, ); insta::assert_snapshot!( p("-").unwrap_err(), - @"found nothing after sign `-`, which is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format", + @r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, ); } @@ -3093,15 +3036,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @"an empty string is not a valid `SignedDuration`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2", + @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"#, ); insta::assert_snapshot!( p("+").unwrap_err(), - @"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", + @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"#, ); insta::assert_snapshot!( p("-").unwrap_err(), - @"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", + @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"#, ); } diff --git a/src/span.rs b/src/span.rs index 7119bd7..48e9826 100644 --- a/src/span.rs +++ b/src/span.rs @@ -3,12 +3,11 @@ use core::{cmp::Ordering, time::Duration as UnsignedDuration}; use crate::{ civil::{Date, DateTime, Time}, duration::{Duration, SDuration}, - error::{err, Error, ErrorContext}, + error::{span::Error as E, 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}, @@ -557,12 +556,13 @@ 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 \ -/// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ +/// `jiff::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,10 +662,11 @@ 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 `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", +/// (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", /// ); /// /// # Ok::<(), Box>(()) @@ -2325,7 +2326,8 @@ 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`. Namely, by default: + /// convert a `Span` to a `SignedDuration` and instead will be ably to + /// use `SignedDuration::try_from(span)`. Namely, by default: /// /// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours. /// * [`Timestamp::until`] guarantees that the biggest non-zero unit is @@ -2336,12 +2338,14 @@ 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. 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, 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.) /// - /// Of course, any of the above can be changed by asking, for example, - /// `Zoned::until` to return units up to years. + /// 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. /// /// # Errors /// @@ -2398,24 +2402,10 @@ impl Span { let relspan = result .and_then(|r| r.into_relative_span(Unit::Second, *self)) .with_context(|| match relative.kind { - 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::Civil(_) => E::ToDurationCivil, + SpanRelativeToKind::Zoned(_) => E::ToDurationZoned, SpanRelativeToKind::DaysAre24Hours => { - err!( - "could not compute normalized relative span \ - from {self} when all days are assumed to be \ - 24 hours", - ) + E::ToDurationDaysAre24Hours } })?; debug_assert!(relspan.span.largest_unit() <= Unit::Second); @@ -3198,13 +3188,7 @@ impl Span { &self, ) -> Option { let non_time_unit = self.largest_calendar_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(), - )) + Some(Error::from(E::NotAllowedCalendarUnits { unit: non_time_unit })) } /// Returns the largest non-zero calendar unit, or `None` if there are no @@ -3272,7 +3256,7 @@ impl Span { if self.nanoseconds != C(0) { write!(buf, ", nanoseconds: {:?}", self.nanoseconds).unwrap(); } - write!(buf, " }}").unwrap(); + buf.push_str(" }}"); buf } @@ -3305,8 +3289,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, } } @@ -3473,10 +3457,7 @@ impl TryFrom for UnsignedDuration { fn try_from(sp: Span) -> Result { // This isn't needed, but improves error messages. if sp.is_negative() { - return Err(err!( - "cannot convert negative span {sp:?} \ - to unsigned std::time::Duration", - )); + return Err(Error::from(E::ConvertNegative)); } SignedDuration::try_from(sp).and_then(UnsignedDuration::try_from) } @@ -3543,18 +3524,15 @@ impl TryFrom for Span { #[inline] fn try_from(d: UnsignedDuration) -> Result { - let seconds = i64::try_from(d.as_secs()).map_err(|_| { - err!("seconds from {d:?} overflows a 64-bit signed integer") - })?; + let seconds = i64::try_from(d.as_secs()) + .map_err(|_| Error::slim_range("unsigned duration seconds"))?; 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).with_context(|| { - err!("duration {d:?} overflows limits of a Jiff `Span`") - })?; + let span = Span::new().try_seconds(seconds)?; // 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 @@ -3606,10 +3584,8 @@ impl TryFrom for SignedDuration { #[inline] fn try_from(sp: Span) -> Result { - requires_relative_date_err(sp.largest_unit()).context( - "failed to convert span to duration without relative datetime \ - (must use `Span::to_duration` instead)", - )?; + requires_relative_date_err(sp.largest_unit()) + .context(E::ConvertSpanToSignedDuration)?; Ok(sp.to_duration_invariant()) } } @@ -3678,9 +3654,7 @@ impl TryFrom for Span { / t::NANOS_PER_MICRO.value(); let nanoseconds = nanoseconds % t::NANOS_PER_MICRO.value(); - let span = Span::new().try_seconds(seconds).with_context(|| { - err!("signed duration {d:?} overflows limits of a Jiff `Span`") - })?; + let span = Span::new().try_seconds(seconds)?; // 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 @@ -4454,7 +4428,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 \ - /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// let sum = span1.checked_add( @@ -4684,7 +4658,7 @@ impl<'a> SpanCompare<'a> { /// required. Otherwise, you get an error. /// /// ``` - /// use jiff::{SpanCompare, ToSpan, Unit}; + /// use jiff::{SpanCompare, ToSpan}; /// /// let span1 = 2.days().hours(12); /// let span2 = 60.hours(); @@ -4693,7 +4667,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 \ - /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// let ordering = span1.compare( @@ -4924,7 +4898,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 \ - /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `jiff::SpanRelativeTo::days_are_24_hours()` is used to indicate \ /// invariant 24-hour days, but neither were provided", /// ); /// @@ -5432,8 +5406,9 @@ 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 `SpanRelativeTo::days_are_24_hours()` is used \ - /// to indicate invariant 24-hour days, but neither were provided", + /// time be given or `jiff::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(), @@ -5486,11 +5461,8 @@ impl<'a> SpanRound<'a> { let max = existing_largest.max(largest); let increment = increment::for_span(smallest, self.increment)?; if largest < smallest { - return Err(err!( - "largest unit ('{largest}') cannot be smaller than \ - smallest unit ('{smallest}')", - largest = largest.singular(), - smallest = smallest.singular(), + return Err(Error::from( + E::NotAllowedLargestSmallerThanSmallest { smallest, largest }, )); } let relative = match self.relative { @@ -5516,14 +5488,13 @@ 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("error with `smallest` rounding option")?; + .context(E::OptionSmallest)?; if let Some(largest) = self.largest { requires_relative_date_err(largest) - .context("error with `largest` rounding option")?; + .context(E::OptionLargest)?; } - requires_relative_date_err(existing_largest).context( - "error with largest unit in span to be rounded", - )?; + requires_relative_date_err(existing_largest) + .context(E::OptionLargestInSpan)?; assert!(max <= Unit::Week); return Ok(round_span_invariant( span, smallest, largest, increment, mode, @@ -5673,7 +5644,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 \ - /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `jiff::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: @@ -5709,7 +5680,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 \ - /// `SpanRelativeTo::days_are_24_hours()` is used to indicate \ + /// `jiff::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: @@ -5814,13 +5785,10 @@ impl<'a> SpanRelativeTo<'a> { } SpanRelativeToKind::DaysAre24Hours => { if matches!(unit, Unit::Year | Unit::Month) { - 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(), + return Err(Error::from( + E::RequiresRelativeYearOrMonthGivenDaysAre24Hours { + unit, + }, )); } Ok(None) @@ -6229,27 +6197,12 @@ impl<'a> RelativeSpanKind<'a> { RelativeSpanKind::Civil { ref start, ref end } => start .datetime .until((largest, end.datetime)) - .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(), - ) - })?, + .context(E::FailedSpanBetweenDateTimes { unit: largest })?, + RelativeSpanKind::Zoned { ref start, ref end } => { + start.zoned.until((largest, &*end.zoned)).context( + E::FailedSpanBetweenZonedDateTimes { unit: largest }, + )? + } }; Ok(RelativeSpan { span, kind: self }) } @@ -6290,9 +6243,7 @@ impl RelativeCivil { fn new(datetime: DateTime) -> Result { let timestamp = datetime .to_zoned(TimeZone::UTC) - .with_context(|| { - err!("failed to convert {datetime} to timestamp") - })? + .context(E::ConvertDateTimeToTimestamp)? .timestamp(); Ok(RelativeCivil { datetime, timestamp }) } @@ -6308,14 +6259,10 @@ 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).with_context(|| { - err!("failed to add {span} to {dt}", dt = self.datetime) - })?; + let datetime = self.datetime.checked_add(span)?; let timestamp = datetime .to_zoned(TimeZone::UTC) - .with_context(|| { - err!("failed to convert {datetime} to timestamp") - })? + .context(E::ConvertDateTimeToTimestamp)? .timestamp(); Ok(RelativeCivil { datetime, timestamp }) } @@ -6335,15 +6282,10 @@ impl RelativeCivil { &self, duration: SignedDuration, ) -> Result { - let datetime = - self.datetime.checked_add(duration).with_context(|| { - err!("failed to add {duration:?} to {dt}", dt = self.datetime) - })?; + let datetime = self.datetime.checked_add(duration)?; let timestamp = datetime .to_zoned(TimeZone::UTC) - .with_context(|| { - err!("failed to convert {datetime} to timestamp") - })? + .context(E::ConvertDateTimeToTimestamp)? .timestamp(); Ok(RelativeCivil { datetime, timestamp }) } @@ -6361,15 +6303,9 @@ impl RelativeCivil { largest: Unit, other: &RelativeCivil, ) -> Result { - 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, - ) - }) + self.datetime + .until((largest, other.datetime)) + .context(E::FailedSpanBetweenDateTimes { unit: largest }) } } @@ -6390,9 +6326,7 @@ impl<'a> RelativeZoned<'a> { &self, span: Span, ) -> Result, Error> { - let zoned = self.zoned.checked_add(span).with_context(|| { - err!("failed to add {span} to {zoned}", zoned = self.zoned) - })?; + let zoned = self.zoned.checked_add(span)?; Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) }) } @@ -6406,9 +6340,7 @@ impl<'a> RelativeZoned<'a> { &self, duration: SignedDuration, ) -> Result, Error> { - let zoned = self.zoned.checked_add(duration).with_context(|| { - err!("failed to add {duration:?} to {zoned}", zoned = self.zoned) - })?; + let zoned = self.zoned.checked_add(duration)?; Ok(RelativeZoned { zoned: DumbCow::Owned(zoned) }) } @@ -6425,15 +6357,9 @@ impl<'a> RelativeZoned<'a> { largest: Unit, other: &RelativeZoned<'a>, ) -> Result { - 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, - ) - }) + self.zoned + .until((largest, &*other.zoned)) + .context(E::FailedSpanBetweenZonedDateTimes { unit: largest }) } /// Returns the borrowed version of self; useful when you need to convert @@ -6512,13 +6438,7 @@ impl Nudge { increment, ); let span = Span::from_invariant_nanoseconds(largest, rounded_nanos) - .with_context(|| { - err!( - "failed to convert rounded nanoseconds {rounded_nanos} \ - to span for largest unit as {unit}", - unit = largest.plural(), - ) - })? + .context(E::ConvertNanoseconds { unit: largest })? .years_ranged(balanced.get_years_ranged()) .months_ranged(balanced.get_months_ranged()) .weeks_ranged(balanced.get_weeks_ranged()); @@ -6551,13 +6471,7 @@ impl Nudge { * balanced.get_units_ranged(smallest).div_ceil(increment); let span = balanced .without_lower(smallest) - .try_units_ranged(smallest, truncated.rinto()) - .with_context(|| { - err!( - "failed to set {unit} to {truncated} on span {balanced}", - unit = smallest.singular() - ) - })?; + .try_units_ranged(smallest, truncated.rinto())?; let (relative0, relative1) = clamp_relative_span( relative_start, span, @@ -6578,14 +6492,7 @@ 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()) - .with_context(|| { - err!( - "failed to set {unit} to {truncated} on span {span}", - unit = smallest.singular() - ) - })?; + let span = span.try_units_ranged(smallest, rounded.rinto())?; let rounded_relative_end = if grew_big_unit { relative1 } else { relative0 }; Ok(Nudge { span, rounded_relative_end, grew_big_unit }) @@ -6631,13 +6538,7 @@ impl Nudge { let span = Span::from_invariant_nanoseconds(Unit::Hour, rounded_time_nanos) - .with_context(|| { - err!( - "failed to convert rounded nanoseconds \ - {rounded_time_nanos} to span for largest unit as {unit}", - unit = Unit::Hour.plural(), - ) - })? + .context(E::ConvertNanoseconds { unit: Unit::Hour })? .years_ranged(balanced.get_years_ranged()) .months_ranged(balanced.get_months_ranged()) .weeks_ranged(balanced.get_weeks_ranged()) @@ -6682,23 +6583,8 @@ impl Nudge { let span_start = balanced.without_lower(unit); let new_units = span_start .get_units_ranged(unit) - .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(), - ) - })?; + .try_checked_add("bubble-units", sign)?; + let span_end = span_start.try_units_ranged(unit, new_units)?; let threshold = match relative.kind { RelativeSpanKind::Civil { ref start, .. } => { start.checked_add(span_end)?.timestamp @@ -6742,13 +6628,8 @@ 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).with_context(|| { - err!( - "failed to convert rounded nanoseconds {rounded} \ - to span for largest unit as {unit}", - unit = largest.plural(), - ) - }) + Span::from_invariant_nanoseconds(largest, rounded) + .context(E::ConvertNanoseconds { unit: largest }) } /// Returns the nanosecond timestamps of `relative + span` and `relative + @@ -6772,24 +6653,9 @@ 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) - .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 amount = + span.get_units_ranged(unit).try_checked_add("clamp-units", amount)?; + let span_amount = span.try_units_ranged(unit, amount)?; let relative0 = relative.checked_add(span)?.to_nanosecond(); let relative1 = relative.checked_add(span_amount)?.to_nanosecond(); Ok((relative0, relative1)) @@ -6811,25 +6677,22 @@ 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 { - 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 Some((&byte, tail)) = bytes.split_first() else { + return Err(crate::Error::from( + crate::error::fmt::Error::HybridDurationEmpty, )); - } - let mut first = bytes[0]; + }; + 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. if first == b'+' || first == b'-' { - 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), + let Some(&byte) = tail.first() else { + return Err(crate::Error::from( + crate::error::fmt::Error::HybridDurationPrefix { sign: first }, )); - } - first = bytes[1]; + }; + first = byte; } if first == b'P' || first == b'p' { temporal::DEFAULT_SPAN_PARSER.parse_span(bytes) @@ -6840,23 +6703,11 @@ fn parse_iso_or_friendly(bytes: &[u8]) -> Result { fn requires_relative_date_err(unit: Unit) -> Result<(), Error> { if unit.is_variable() { - 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(), - ) + return Err(Error::from(if matches!(unit, Unit::Week | Unit::Day) { + E::RequiresRelativeWeekOrDay { unit } } else { - err!( - "using unit '{unit}' in a span or configuration \ - requires that a relative reference time be given, \ - but none was provided", - unit = unit.singular(), - ) - }); + E::RequiresRelativeYearOrMonth { unit } + })); } Ok(()) } @@ -7390,15 +7241,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format", + @r#"an empty string is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, ); insta::assert_snapshot!( p("+").unwrap_err(), - @"found nothing after sign `+`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format", + @r#"found nothing after sign `+`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, ); insta::assert_snapshot!( p("-").unwrap_err(), - @"found nothing after sign `-`, which is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format", + @r#"found nothing after sign `-`, which is not a valid duration in either the ISO 8601 format or Jiff's "friendly" format"#, ); } @@ -7435,15 +7286,15 @@ mod tests { insta::assert_snapshot!( p("").unwrap_err(), - @"an empty string is not a valid `Span`, expected either a ISO 8601 or Jiff's 'friendly' format at line 1 column 2", + @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"#, ); insta::assert_snapshot!( p("+").unwrap_err(), - @"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", + @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"#, ); insta::assert_snapshot!( p("-").unwrap_err(), - @"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", + @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"#, ); } } diff --git a/src/timestamp.rs b/src/timestamp.rs index 22a4732..be37120 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::{err, Error, ErrorContext}, + error::{timestamp::Error as E, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -279,8 +279,7 @@ 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 in \ -/// \"2024-06-30 08:30[America/New_York]\", \ +/// "failed to find offset component, \ /// which is required for parsing a timestamp", /// ); /// ``` @@ -1520,9 +1519,7 @@ impl Timestamp { let time_seconds = self.as_second_ranged(); let sum = time_seconds .try_checked_add("span", span_seconds) - .with_context(|| { - err!("adding {span} to {self} overflowed") - })?; + .context(E::OverflowAddSpan)?; return Ok(Timestamp::from_second_ranged(sum)); } } @@ -1530,7 +1527,7 @@ impl Timestamp { let span_nanos = span.to_invariant_nanoseconds(); let sum = time_nanos .try_checked_add("span", span_nanos) - .with_context(|| err!("adding {span} to {self} overflowed"))?; + .context(E::OverflowAddSpan)?; Ok(Timestamp::from_nanosecond_ranged(sum)) } @@ -1540,9 +1537,7 @@ impl Timestamp { duration: SignedDuration, ) -> Result { let start = self.as_duration(); - let end = start.checked_add(duration).ok_or_else(|| { - err!("overflow when adding {duration:?} to {self}") - })?; + let end = start.checked_add(duration).ok_or(E::OverflowAddDuration)?; Timestamp::from_duration(end) } @@ -1648,9 +1643,7 @@ impl Timestamp { duration: A, ) -> Result { let duration: TimestampArithmetic = duration.into(); - duration.saturating_add(self).context( - "saturating `Timestamp` arithmetic requires only time units", - ) + duration.saturating_add(self).context(E::RequiresSaturatingTimeUnits) } /// This routine is identical to [`Timestamp::saturating_add`] with the @@ -3429,11 +3422,10 @@ impl TimestampDifference { .get_largest() .unwrap_or_else(|| self.round.get_smallest().max(Unit::Second)); if largest >= Unit::Day { - return Err(err!( - "unit {largest} is not supported when computing the \ - difference between timestamps (must use units smaller \ - than 'day')", - largest = largest.singular(), + return Err(Error::from( + crate::error::util::RoundingIncrementError::Unsupported { + unit: largest, + }, )); } let nano1 = t1.as_nanosecond_ranged().without_bounds(); @@ -3856,7 +3848,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 `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `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 `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)", ) } @@ -3864,7 +3856,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 `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `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 `jiff::Timestamp`, `jiff::tz::Offset` and `jiff::civil::Time` don't support calendar units in a `jiff::Span`)", ) } diff --git a/src/tz/ambiguous.rs b/src/tz/ambiguous.rs index 45319f5..c662d09 100644 --- a/src/tz/ambiguous.rs +++ b/src/tz/ambiguous.rs @@ -1,6 +1,6 @@ use crate::{ civil::DateTime, - error::{err, Error, ErrorContext}, + error::{tz::ambiguous::Error as E, Error, ErrorContext}, shared::util::itime::IAmbiguousOffset, tz::{Offset, TimeZone}, Timestamp, Zoned, @@ -655,18 +655,10 @@ impl AmbiguousTimestamp { let offset = match self.offset() { AmbiguousOffset::Unambiguous { offset } => offset, AmbiguousOffset::Gap { before, after } => { - return Err(err!( - "the datetime {dt} is ambiguous since it falls into \ - a gap between offsets {before} and {after}", - dt = self.dt, - )); + return Err(Error::from(E::BecauseGap { before, after })); } AmbiguousOffset::Fold { before, after } => { - return Err(err!( - "the datetime {dt} is ambiguous since it falls into \ - a fold between offsets {before} and {after}", - dt = self.dt, - )); + return Err(Error::from(E::BecauseFold { before, after })); } }; offset.to_timestamp(self.dt) @@ -1039,13 +1031,10 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn compatible(self) -> Result { - 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(), - ) - })?; + let ts = self + .ts + .compatible() + .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; Ok(ts.to_zoned(self.tz)) } @@ -1101,13 +1090,10 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn earlier(self) -> Result { - 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(), - ) - })?; + let ts = self + .ts + .earlier() + .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; Ok(ts.to_zoned(self.tz)) } @@ -1163,13 +1149,10 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn later(self) -> Result { - 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(), - ) - })?; + let ts = self + .ts + .later() + .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; Ok(ts.to_zoned(self.tz)) } @@ -1220,13 +1203,10 @@ impl AmbiguousZoned { /// ``` #[inline] pub fn unambiguous(self) -> Result { - 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(), - ) - })?; + let ts = self + .ts + .unambiguous() + .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; Ok(ts.to_zoned(self.tz)) } diff --git a/src/tz/concatenated.rs b/src/tz/concatenated.rs index 84c1257..2d268ee 100644 --- a/src/tz/concatenated.rs +++ b/src/tz/concatenated.rs @@ -4,7 +4,10 @@ use alloc::{ }; use crate::{ - error::{err, Error, ErrorContext}, + error::{ + tz::concatenated::{Error as E, ALLOC_LIMIT}, + Error, ErrorContext, + }, tz::TimeZone, util::{array_str::ArrayStr, escape, utf8}, }; @@ -71,7 +74,7 @@ impl ConcatenatedTzif { alloc(scratch1, self.header.index_len())?; self.rdr .read_exact_at(scratch1, self.header.index_offset) - .context("failed to read index block")?; + .context(E::FailedReadIndex)?; let mut index = &**scratch1; while !index.is_empty() { @@ -94,7 +97,7 @@ impl ConcatenatedTzif { let start = self.header.data_offset.saturating_add(entry.start()); self.rdr .read_exact_at(scratch2, start) - .context("failed to read TZif data block")?; + .context(E::FailedReadData)?; return TimeZone::tzif(name, scratch2).map(Some); } Ok(None) @@ -114,7 +117,7 @@ impl ConcatenatedTzif { alloc(scratch, self.header.index_len())?; self.rdr .read_exact_at(scratch, self.header.index_offset) - .context("failed to read index block")?; + .context(E::FailedReadIndex)?; let names_len = self.header.index_len() / IndexEntry::LEN; // Why are we careless with this alloc? Well, its size is proportional @@ -154,31 +157,17 @@ 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("failed to read concatenated TZif header")?; + rdr.read_exact_at(&mut buf, 0).context(E::FailedReadHeader)?; if &buf[..6] != b"tzdata" { - return Err(err!( - "expected first 6 bytes of concatenated TZif header \ - to be `tzdata`, but found `{found}`", - found = escape::Bytes(&buf[..6]), - )); + return Err(Error::from(E::ExpectedFirstSixBytes)); } if buf[11] != 0 { - return Err(err!( - "expected last byte of concatenated TZif header \ - to be NUL, but found `{found}`", - found = escape::Bytes(&buf[..12]), - )); + return Err(Error::from(E::ExpectedLastByte)); } let version = { - 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]), - ) - })?; + let version = core::str::from_utf8(&buf[6..11]) + .map_err(|_| E::ExpectedVersion)?; // OK because `version` is exactly 5 bytes, by construction. ArrayStr::new(version).unwrap() }; @@ -187,19 +176,12 @@ 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(err!( - "invalid index ({index_offset}) and data ({data_offset}) \ - offsets, expected index offset to be less than or equal \ - to data offset", - )); + return Err(Error::from(E::InvalidIndexDataOffsets)); } // 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(err!( - "length of index block is not a multiple {len}", - len = IndexEntry::LEN, - )); + return Err(Error::from(E::InvalidLengthIndexBlock)); } Ok(header) } @@ -268,12 +250,8 @@ 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(|_| { - err!( - "IANA time zone identifier `{name}` is not valid UTF-8", - name = escape::Bytes(self.name_bytes()), - ) - }) + core::str::from_utf8(self.name_bytes()) + .map_err(|_| Error::from(E::ExpectedIanaName)) } /// Returns the IANA time zone identifier as a byte slice. @@ -350,20 +328,12 @@ 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(|_| err!("offset `{offset}` overflowed `usize`"))?; + .map_err(|_| E::InvalidOffsetOverflowSlice)?; let Some(slice) = self.get(offset..) else { - return Err(err!( - "given offset `{offset}` is not valid \ - (only {len} bytes are available)", - len = self.len(), - )); + return Err(Error::from(E::InvalidOffsetTooBig)); }; if buf.len() > slice.len() { - return Err(err!( - "unexpected EOF, expected {len} bytes but only have {have}", - len = buf.len(), - have = slice.len() - )); + return Err(Error::from(E::ExpectedMoreData)); } buf.copy_from_slice(&slice[..buf.len()]); Ok(()) @@ -395,9 +365,7 @@ impl Read for std::fs::File { offset = u64::try_from(n) .ok() .and_then(|n| n.checked_add(offset)) - .ok_or_else(|| { - err!("offset overflow when reading from `File`") - })?; + .ok_or(E::InvalidOffsetOverflowFile)?; } Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} Err(e) => return Err(Error::io(e)), @@ -419,9 +387,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).with_context( - || err!("failed to seek to offset {offset} in `File`"), - )?; + file.seek(SeekFrom::Start(offset)) + .map_err(Error::io) + .context(E::FailedSeek)?; file.read_exact(buf).map_err(Error::io) } } @@ -443,31 +411,13 @@ 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> { - // 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)", - )); + if additional > ALLOC_LIMIT { + return Err(Error::from(E::AllocRequestOverLimit)); } - bytes.try_reserve_exact(additional).map_err(|_| { - err!( - "failed to allocation {additional} bytes \ - for reading concatenated TZif data" - ) - })?; + bytes.try_reserve_exact(additional).map_err(|_| E::AllocFailed)?; // This... can't actually happen right? - let new_len = bytes - .len() - .checked_add(additional) - .ok_or_else(|| err!("total allocation length overflowed `usize`"))?; + let new_len = + bytes.len().checked_add(additional).ok_or(E::AllocOverflow)?; bytes.resize(new_len, 0); Ok(()) } diff --git a/src/tz/db/bundled/disabled.rs b/src/tz/db/bundled/disabled.rs index 7b0256a..84d8062 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 { - write!(f, "Bundled(unavailable)") + f.write_str("Bundled(unavailable)") } } diff --git a/src/tz/db/bundled/enabled.rs b/src/tz/db/bundled/enabled.rs index 7850ccb..434b3a0 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 like a bug, please report it): {_err}" + tzdb for time zone `{canonical_name}` \ + (this is likely 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 { - write!(f, "Bundled(available)") + f.write_str("Bundled(available)") } } diff --git a/src/tz/db/concatenated/disabled.rs b/src/tz/db/concatenated/disabled.rs index 4da2274..06b8f78 100644 --- a/src/tz/db/concatenated/disabled.rs +++ b/src/tz/db/concatenated/disabled.rs @@ -10,14 +10,12 @@ impl Database { #[cfg(feature = "std")] pub(crate) fn from_path( - path: &std::path::Path, + _path: &std::path::Path, ) -> Result { - Err(crate::error::err!( - "system concatenated tzdb unavailable: \ - crate feature `tzdb-concatenated` is disabled, \ - opening tzdb at {path} has therefore failed", - path = path.display(), - )) + Err(crate::error::Error::from( + crate::error::CrateFeatureError::TzdbConcatenated, + ) + .context(crate::error::tz::db::Error::DisabledConcatenated)) } pub(crate) fn none() -> Database { @@ -41,6 +39,6 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "Concatenated(unavailable)") + f.write_str("Concatenated(unavailable)") } } diff --git a/src/tz/db/concatenated/enabled.rs b/src/tz/db/concatenated/enabled.rs index bd28720..4c95896 100644 --- a/src/tz/db/concatenated/enabled.rs +++ b/src/tz/db/concatenated/enabled.rs @@ -13,7 +13,7 @@ use std::{ }; use crate::{ - error::{err, Error}, + error::{tz::db::Error as E, 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 { - write!(f, "Concatenated(")?; + f.write_str("Concatenated(")?; if let Some(ref path) = self.path { - write!(f, "{}", path.display())?; + path.display().fmt(f)?; } else { - write!(f, "unavailable")?; + f.write_str("unavailable")?; } - write!(f, ")") + f.write_str(")") } } @@ -540,11 +540,7 @@ fn read_names_and_version( let names: Vec> = db.available(scratch)?.into_iter().map(Arc::from).collect(); if names.is_empty() { - return Err(err!( - "found no IANA time zone identifiers in \ - concatenated tzdata file at {path}", - path = path.display(), - )); + return Err(Error::from(E::ConcatenatedMissingIanaIdentifiers)); } Ok((names, db.version())) } diff --git a/src/tz/db/mod.rs b/src/tz/db/mod.rs index d8d7a4e..8c004d3 100644 --- a/src/tz/db/mod.rs +++ b/src/tz/db/mod.rs @@ -1,5 +1,5 @@ use crate::{ - error::{err, Error}, + error::{tz::db::Error as E, Error}, tz::TimeZone, util::{sync::Arc, utf8}, }; @@ -457,22 +457,10 @@ impl TimeZoneDatabase { /// # Ok::<(), Box>(()) /// ``` pub fn get(&self, name: &str) -> Result { - 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)", - ) - } - })?; + let inner = self + .inner + .as_deref() + .ok_or_else(|| E::failed_time_zone_no_database_configured(name))?; match *inner { Kind::ZoneInfo(ref db) => { if let Some(tz) = db.get(name) { @@ -493,7 +481,7 @@ impl TimeZoneDatabase { } } } - Err(err!("failed to find time zone `{name}` in time zone database")) + Err(Error::from(E::failed_time_zone(name))) } /// Returns a list of all available time zone identifiers from this @@ -572,16 +560,16 @@ impl TimeZoneDatabase { impl core::fmt::Debug for TimeZoneDatabase { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "TimeZoneDatabase(")?; + f.write_str("TimeZoneDatabase(")?; let Some(inner) = self.inner.as_deref() else { - return write!(f, "unavailable)"); + return f.write_str("unavailable)"); }; match *inner { - Kind::ZoneInfo(ref db) => write!(f, "{db:?}")?, - Kind::Concatenated(ref db) => write!(f, "{db:?}")?, - Kind::Bundled(ref db) => write!(f, "{db:?}")?, + 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)?, } - write!(f, ")") + f.write_str(")") } } @@ -687,7 +675,7 @@ impl<'d> TimeZoneName<'d> { impl<'d> core::fmt::Display for TimeZoneName<'d> { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", self.as_str()) + f.write_str(self.as_str()) } } diff --git a/src/tz/db/zoneinfo/disabled.rs b/src/tz/db/zoneinfo/disabled.rs index a9c79d4..0290977 100644 --- a/src/tz/db/zoneinfo/disabled.rs +++ b/src/tz/db/zoneinfo/disabled.rs @@ -10,14 +10,12 @@ impl Database { #[cfg(feature = "std")] pub(crate) fn from_dir( - dir: &std::path::Path, + _dir: &std::path::Path, ) -> Result { - Err(crate::error::err!( - "system tzdb unavailable: \ - crate feature `tzdb-zoneinfo` is disabled, \ - opening tzdb at {dir} has therefore failed", - dir = dir.display(), - )) + Err(crate::error::Error::from( + crate::error::CrateFeatureError::TzdbZoneInfo, + ) + .context(crate::error::tz::db::Error::DisabledZoneInfo)) } pub(crate) fn none() -> Database { @@ -41,6 +39,6 @@ impl Database { impl core::fmt::Debug for Database { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "ZoneInfo(unavailable)") + f.write_str("ZoneInfo(unavailable)") } } diff --git a/src/tz/db/zoneinfo/enabled.rs b/src/tz/db/zoneinfo/enabled.rs index 1390394..f4237d4 100644 --- a/src/tz/db/zoneinfo/enabled.rs +++ b/src/tz/db/zoneinfo/enabled.rs @@ -17,7 +17,7 @@ use std::{ }; use crate::{ - error::{err, Error}, + error::{tz::db::Error as E, 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 { - write!(f, "ZoneInfo(")?; + f.write_str("ZoneInfo(")?; if let Some(ref dir) = self.dir { - write!(f, "{}", dir.display())?; + core::fmt::Display::fmt(&dir.display(), f)?; } else { - write!(f, "unavailable")?; + f.write_str("unavailable")?; } - write!(f, ")") + f.write_str(")") } } @@ -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| err.path(base))?; + .map_err(|err| Error::from(err).path(base))?; let lower = original.to_ascii_lowercase(); let inner = ZoneInfoNameInner { full, @@ -792,14 +792,18 @@ fn walk(start: &Path) -> Result, Error> { let time_zone_name = match path.strip_prefix(start) { Ok(time_zone_name) => time_zone_name, - Err(err) => { + // 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) => { 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::adhoc(err)); + seterr(&path, Error::from(E::ZoneInfoStripPrefix)); continue; } }; @@ -817,7 +821,7 @@ fn walk(start: &Path) -> Result, Error> { if names.is_empty() { let err = first_err .take() - .unwrap_or_else(|| err!("{}: no TZif files", start.display())); + .unwrap_or_else(|| Error::from(E::ZoneInfoNoTzifFiles)); 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 b7b492a..f081cf8 100644 --- a/src/tz/offset.rs +++ b/src/tz/offset.rs @@ -6,7 +6,7 @@ use core::{ use crate::{ civil, duration::{Duration, SDuration}, - error::{err, Error, ErrorContext}, + error::{tz::offset::Error as E, Error, ErrorContext}, shared::util::itime::IOffset, span::Span, timestamp::Timestamp, @@ -526,12 +526,8 @@ impl Offset { .to_idatetime() .zip2(self.to_ioffset()) .map(|(idt, ioff)| idt.to_timestamp(ioff)); - Timestamp::from_itimestamp(its).with_context(|| { - err!( - "converting {dt} with offset {offset} to timestamp overflowed", - offset = self, - ) - }) + Timestamp::from_itimestamp(its) + .context(E::ConvertDateTimeToTimestamp { offset: self }) } /// Adds the given span of time to this offset. @@ -660,21 +656,11 @@ impl Offset { ) -> Result { let duration = t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs()) - .with_context(|| { - err!( - "adding signed duration {duration:?} \ - to offset {self} overflowed maximum offset seconds" - ) - })?; + .context(E::OverflowAddSignedDuration)?; let offset_seconds = self.seconds_ranged(); let seconds = offset_seconds .try_checked_add("offset-seconds", duration) - .with_context(|| { - err!( - "adding signed duration {duration:?} \ - to offset {self} overflowed" - ) - })?; + .context(E::OverflowAddSignedDuration)?; Ok(Offset::from_seconds_ranged(seconds)) } @@ -975,8 +961,7 @@ impl Offset { /// assert_eq!(Offset::MAX.to_string(), "+25:59:59"); /// assert_eq!( /// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(), - /// "rounding offset `+25:59:59` resulted in a duration of 26h, \ - /// which overflows `Offset`", + /// "rounding time zone offset resulted in a duration that overflows", /// ); /// ``` #[inline] @@ -1125,7 +1110,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 { - write!(f, "+00") + f.write_str("+00") } else if hours != 0 && minutes == 0 && seconds == 0 { write!(f, "{sign}{hours:02}") } else if minutes != 0 && seconds == 0 { @@ -1347,11 +1332,10 @@ impl TryFrom for Offset { } else if subsec <= -500_000_000 { seconds = seconds.saturating_sub(1); } - let seconds = i32::try_from(seconds).map_err(|_| { - err!("`SignedDuration` of {sdur} overflows `Offset`") - })?; + let seconds = + i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?; Offset::from_seconds(seconds) - .map_err(|_| err!("`SignedDuration` of {sdur} overflows `Offset`")) + .map_err(|_| Error::from(E::OverflowSignedDuration)) } } @@ -1599,20 +1583,11 @@ impl OffsetRound { fn round(&self, offset: Offset) -> Result { let smallest = self.0.get_smallest(); if !(Unit::Second <= smallest && smallest <= Unit::Hour) { - 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(), - )); + return Err(Error::from(E::RoundInvalidUnit { unit: smallest })); } let rounded_sdur = SignedDuration::from(offset).round(self.0)?; - Offset::try_from(rounded_sdur).map_err(|_| { - err!( - "rounding offset `{offset}` resulted in a duration \ - of {rounded_sdur:?}, which overflows `Offset`", - ) - }) + Offset::try_from(rounded_sdur) + .map_err(|_| Error::from(E::RoundOverflow)) } } @@ -1888,10 +1863,10 @@ impl OffsetConflict { /// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone()); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "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", + /// "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`", /// ); /// let is_equal = |parsed: Offset, candidate: Offset| { /// parsed == candidate || candidate.round(Unit::Minute).map_or( @@ -1950,11 +1925,10 @@ impl OffsetConflict { /// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::(); /// assert_eq!( /// result.unwrap_err().to_string(), - /// "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", + /// "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`", /// ); /// ``` pub fn resolve_with( @@ -2046,13 +2020,13 @@ impl OffsetConflict { let amb = tz.to_ambiguous_timestamp(dt); match amb.offset() { - 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 { offset } if !is_equal(given, offset) => { + Err(Error::from(E::ResolveRejectUnambiguous { + given, + offset, + tz, + })) + } Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)), Gap { before, after } => { // In `jiff 0.1`, we reported an error when we found a gap @@ -2065,28 +2039,22 @@ impl OffsetConflict { // changed to treat all offsets in a gap as invalid). // // Ref: https://github.com/tc39/proposal-temporal/issues/2892 - 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(), - )) + Err(Error::from(E::ResolveRejectGap { + given, + before, + after, + tz, + })) } Fold { before, after } if !is_equal(given, before) && !is_equal(given, after) => { - 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(), - )) + Err(Error::from(E::ResolveRejectFold { + given, + before, + after, + tz, + })) } Fold { .. } => { let kind = Unambiguous { offset: given }; diff --git a/src/tz/posix.rs b/src/tz/posix.rs index cd0d224..ee65b71 100644 --- a/src/tz/posix.rs +++ b/src/tz/posix.rs @@ -72,14 +72,14 @@ use core::fmt::Debug; use crate::{ civil::DateTime, - error::{err, Error, ErrorContext}, + error::{tz::posix::Error as E, Error, ErrorContext}, shared, timestamp::Timestamp, tz::{ timezone::TimeZoneAbbreviation, AmbiguousOffset, Dst, Offset, TimeZoneOffsetInfo, TimeZoneTransition, }, - util::{array_str::Abbreviation, escape::Bytes, parse}, + util::{array_str::Abbreviation, parse}, }; /// The result of parsing the POSIX `TZ` environment variable. @@ -114,11 +114,7 @@ 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(err!( - "POSIX time zone string with a ':' prefix contains \ - invalid UTF-8: {:?}", - Bytes(&bytes[1..]), - )); + return Err(Error::from(E::ColonPrefixInvalidUtf8)); }; Ok(PosixTzEnv::Implementation(string.into())) } else { @@ -138,8 +134,11 @@ 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) => write!(f, "{tz}"), - PosixTzEnv::Implementation(ref imp) => write!(f, ":{imp}"), + PosixTzEnv::Rule(ref tz) => core::fmt::Display::fmt(tz, f), + PosixTzEnv::Implementation(ref imp) => { + f.write_str(":")?; + core::fmt::Display::fmt(imp, f) + } } } } @@ -211,10 +210,8 @@ impl PosixTimeZone { ) -> Result { let bytes = bytes.as_ref(); let inner = shared::PosixTimeZone::parse(bytes.as_ref()) - .map_err(Error::shared) - .map_err(|e| { - e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes))) - })?; + .map_err(Error::posix_tz) + .context(E::InvalidPosixTz)?; Ok(PosixTimeZone { inner }) } @@ -227,13 +224,8 @@ impl PosixTimeZone { let bytes = bytes.as_ref(); let (inner, remaining) = shared::PosixTimeZone::parse_prefix(bytes.as_ref()) - .map_err(Error::shared) - .map_err(|e| { - e.context(err!( - "invalid POSIX TZ string {:?}", - Bytes(bytes) - )) - })?; + .map_err(Error::posix_tz) + .context(E::InvalidPosixTz)?; Ok((PosixTimeZone { inner }, remaining)) } diff --git a/src/tz/system/mod.rs b/src/tz/system/mod.rs index 8cae45c..a8e6487 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::{err, Error, ErrorContext}, + error::{tz::system::Error as E, Error, ErrorContext}, tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase}, util::cache::Expiration, }; @@ -141,22 +141,20 @@ 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( - "TZ environment variable set, but failed to read value", - )); + return Err(err.context(E::FailedEnvTz)); } } if let Some(tz) = sys::get(db) { return Ok(tz); } - Err(err!("failed to find system time zone")) + Err(Error::from(E::FailedSystemTimeZone)) } /// Materializes a `TimeZone` from a `TZ` environment variable. @@ -184,8 +182,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)); @@ -196,15 +194,7 @@ 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_else(|| { - err!( - "failed to parse {tzenv:?} as a POSIX TZ transition \ - string, or as valid UTF-8", - ) - })? - .to_string() + tzenv.to_str().ok_or(E::FailedPosixTzAndUtf8)?.to_string() } Ok(PosixTzEnv::Implementation(string)) => string.to_string(), Ok(PosixTzEnv::Rule(tz)) => { @@ -231,26 +221,20 @@ 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(|| { - err!( - "failed to read TZ={tz_name_or_path:?} \ - as a TZif file after attempting a tzdb \ - lookup for `{tz_name_or_path}`", - ) - }) + .ok_or_else(|| Error::from(E::FailedEnvTzAsTzif)) .map(Some) } }; @@ -260,16 +244,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}`)", ); } } @@ -279,13 +263,7 @@ 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(|| { - err!( - "failed to read TZ={tz_name_or_path:?} \ - as a TZif file after attempting a tzdb \ - lookup for `{name}`", - ) - }) + .ok_or_else(|| Error::from(E::FailedEnvTzAsTzif)) .map(Some) } @@ -298,8 +276,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) - .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:?}"))?; + .context(E::FailedUnnamedTzifRead)?; + let tz = + TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?; Ok(tz) } diff --git a/src/tz/system/windows/mod.rs b/src/tz/system/windows/mod.rs index ec23143..1711b31 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::{err, Error, ErrorContext}, + error::{tz::system::Error as E, Error, ErrorContext}, tz::{TimeZone, TimeZoneDatabase}, util::utf8, }; @@ -79,16 +79,12 @@ 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(err!( - "found Windows time zone name {tz_key_name}, \ - but could not find a mapping for it to an \ - IANA time zone name", - )); + return Err(Error::from(E::WindowsMissingIanaMapping)); }; 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) } @@ -107,24 +103,19 @@ 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( - "could not get TimeZoneKeyName from \ - winapi DYNAMIC_TIME_ZONE_INFORMATION", - )?; + .context(E::WindowsTimeZoneKeyName)?; 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_else(|| { - err!("failed to convert u16 slice to UTF-8 (no NUL terminator found)") - })?; + let nul = code_units + .iter() + .position(|&cu| cu == 0) + .ok_or(E::WindowsUtf16DecodeNul)?; let string = String::from_utf16(&code_units[..nul]) - .map_err(Error::adhoc) - .with_context(|| { - err!("failed to convert u16 slice to UTF-8 (invalid UTF-16)") - })?; + .map_err(|_| E::WindowsUtf16DecodeInvalid)?; Ok(string) } diff --git a/src/tz/timezone.rs b/src/tz/timezone.rs index 0664cb3..297ecff 100644 --- a/src/tz/timezone.rs +++ b/src/tz/timezone.rs @@ -1,6 +1,6 @@ use crate::{ civil::DateTime, - error::{err, Error}, + error::{tz::timezone::Error as E, Error}, tz::{ ambiguous::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned}, offset::{Dst, Offset}, @@ -392,10 +392,8 @@ impl TimeZone { pub fn try_system() -> Result { #[cfg(not(feature = "tz-system"))] { - Err(err!( - "failed to get system time zone since 'tz-system' \ - crate feature is not enabled", - )) + Err(Error::from(crate::error::CrateFeatureError::TzSystem) + .context(E::FailedSystem)) } #[cfg(feature = "tz-system")] { @@ -916,7 +914,7 @@ impl TimeZone { /// assert_eq!( /// tz.to_fixed_offset().unwrap_err().to_string(), /// "cannot convert non-fixed IANA time zone \ - /// to offset without timestamp or civil datetime", + /// to offset without a timestamp or civil datetime", /// ); /// /// let tz = TimeZone::UTC; @@ -935,11 +933,7 @@ impl TimeZone { #[inline] pub fn to_fixed_offset(&self) -> Result { let mkerr = || { - err!( - "cannot convert non-fixed {kind} time zone to offset \ - without timestamp or civil datetime", - kind = self.kind_description(), - ) + Error::from(E::ConvertNonFixed { kind: self.kind_description() }) }; repr::each! { &self.repr, @@ -1392,7 +1386,7 @@ impl TimeZone { /// Returns a short description about the kind of this time zone. /// /// This is useful in error messages. - fn kind_description(&self) -> &str { + fn kind_description(&self) -> &'static str { repr::each! { &self.repr, UTC => "UTC", @@ -1887,12 +1881,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 => 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}"), + 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), } } } @@ -1939,6 +1933,10 @@ 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; @@ -2271,9 +2269,9 @@ mod repr { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { each! { self, - UTC => write!(f, "UTC"), - UNKNOWN => write!(f, "Etc/Unknown"), - FIXED(offset) => write!(f, "{offset:?}"), + UTC => f.write_str("UTC"), + UNKNOWN => f.write_str("Etc/Unknown"), + FIXED(offset) => core::fmt::Debug::fmt(&offset, f), STATIC_TZIF(tzif) => { // The full debug output is a bit much, so constrain it. let field = tzif.name().unwrap_or("Local"); @@ -2284,7 +2282,11 @@ mod repr { let field = tzif.name().unwrap_or("Local"); f.debug_tuple("TZif").field(&field).finish() }, - ARC_POSIX(posix) => write!(f, "Posix({posix})"), + ARC_POSIX(posix) => { + f.write_str("Posix(")?; + core::fmt::Display::fmt(&posix, f)?; + f.write_str(")") + }, } } } diff --git a/src/tz/tzif.rs b/src/tz/tzif.rs index 6f2c815..4271ff5 100644 --- a/src/tz/tzif.rs +++ b/src/tz/tzif.rs @@ -157,8 +157,7 @@ impl TzifOwned { name: Option, bytes: &[u8], ) -> Result { - let sh = - shared::TzifOwned::parse(name, bytes).map_err(Error::shared)?; + let sh = shared::TzifOwned::parse(name, bytes).map_err(Error::tzif)?; Ok(TzifOwned::from_shared_owned(sh)) } @@ -571,9 +570,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 => write!(f, "local/wall"), - shared::TzifIndicator::LocalStandard => write!(f, "local/std"), - shared::TzifIndicator::UTStandard => write!(f, "ut/std"), + shared::TzifIndicator::LocalWall => f.write_str("local/wall"), + shared::TzifIndicator::LocalStandard => f.write_str("local/std"), + shared::TzifIndicator::UTStandard => f.write_str("ut/std"), } } } diff --git a/src/tz/zic.rs b/src/tz/zic.rs index 28db5b0..dc702f3 100644 --- a/src/tz/zic.rs +++ b/src/tz/zic.rs @@ -111,7 +111,10 @@ use alloc::{ use crate::{ civil::{Date, DateTime, Time, Weekday}, - error::{err, Error, ErrorContext}, + error::{ + tz::zic::{Error as E, MAX_LINE_LEN}, + Error, ErrorContext, + }, span::{Span, SpanFieldwise, ToSpan}, timestamp::Timestamp, tz::{Dst, Offset}, @@ -205,12 +208,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().map_err(|e| { - err!("SAVE value in rule {:?} is too big: {e}", inner.name) + 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 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; @@ -260,13 +263,12 @@ impl ZicP { ) -> Result<(), Error> { while parser.read_next_fields()? { self.parse_one(&mut parser) - .map_err(|e| e.context(err!("line {}", parser.line_number)))?; + .context(E::Line { number: parser.line_number })?; } if let Some(ref name) = parser.continuation_zone_for { - return Err(err!( - "expected continuation zone line for {name:?}, \ - but found end of data instead", - )); + return Err(Error::from(E::ExpectedContinuationZoneLine { + name: name.as_str().into(), + })); } Ok(()) } @@ -277,9 +279,8 @@ impl ZicP { assert!(!p.fields.is_empty()); if let Some(name) = p.continuation_zone_for.take() { - let zone = ZoneContinuationP::parse(&p.fields).map_err(|e| { - e.context("failed to parse continuation 'Zone' line") - })?; + let zone = ZoneContinuationP::parse(&p.fields) + .context(E::FailedContinuationZone)?; 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. @@ -293,51 +294,45 @@ 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) - .map_err(|e| e.context("failed to parse 'Rule' line"))?; + let rule = RuleP::parse(rest).context(E::FailedRuleLine)?; 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) - .map_err(|e| e.context("failed to parse first 'Zone' line"))?; + let first = ZoneFirstP::parse(rest).context(E::FailedZoneFirst)?; 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(err!( - "found zone with name {name:?} that conflicts \ - with a link of the same name", - )); + return Err(Error::from(E::DuplicateZoneLink { + name: name.into(), + })); } if let Some(previous_zone) = self.zones.insert(name, zone) { - return Err(err!( - "found duplicate zone for {:?}", - previous_zone.first.name.name, - )); + return Err(Error::from(E::DuplicateZone { + name: previous_zone.first.name.name.into(), + })); } } else if first.starts_with("L") && "Link".starts_with(first) { - let link = LinkP::parse(rest) - .map_err(|e| e.context("failed to parse 'Link' line"))?; + let link = LinkP::parse(rest).context(E::FailedLinkLine)?; let name = link.name.name.clone(); if self.zones.contains_key(&name) { - return Err(err!( - "found link with name {name:?} that conflicts \ - with a zone of the same name", - )); + return Err(Error::from(E::DuplicateLinkZone { + name: name.into(), + })); } if let Some(previous_link) = self.links.insert(name, link) { - return Err(err!( - "found duplicate link for {:?}", - previous_link.name.name, - )); + return Err(Error::from(E::DuplicateLink { + name: previous_link.name.name.into(), + })); } // 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(err!("unrecognized zic line: {first:?}")); + return Err(Error::from(E::UnrecognizedZicLine) + .context(E::Line { number: p.line_number })); } Ok(()) } @@ -369,10 +364,9 @@ struct RuleP { impl RuleP { fn parse(fields: &[&str]) -> Result { if fields.len() != 9 { - return Err(err!( - "expected exactly 9 fields for rule, but found {} fields", - fields.len(), - )); + return Err(Error::from(E::ExpectedRuleNineFields { + got: fields.len(), + })); } let (name_field, fields) = (fields[0], &fields[1..]); let (from_field, fields) = (fields[0], &fields[1..]); @@ -386,28 +380,21 @@ impl RuleP { let name = name_field .parse::() - .map_err(|e| e.context("failed to parse NAME field"))?; + .context(E::FailedParseFieldName)?; let from = from_field .parse::() - .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"))?; + .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)?; let save = save_field .parse::() - .map_err(|e| e.context("failed to parse SAVE field"))?; + .context(E::FailedParseFieldSave)?; let letters = letters_field .parse::() - .map_err(|e| e.context("failed to parse LETTERS field"))?; + .context(E::FailedParseFieldLetters)?; Ok(RuleP { name, from, to, inn, on, at, save, letters }) } @@ -420,9 +407,10 @@ impl RuleP { RuleToP::Year { year } => year, }; if start > end { - return Err(err!( - "found start year {start} to be greater than end year {end}" - )); + return Err(Error::from(E::InvalidRuleYear { + start: start.get(), + end: end.get(), + })); } Ok(start..=end) } @@ -462,7 +450,7 @@ struct ZoneFirstP { impl ZoneFirstP { fn parse(fields: &[&str]) -> Result { if fields.len() < 4 { - return Err(err!("first ZONE line must have at least 4 fields")); + return Err(Error::from(E::ExpectedFirstZoneFourFields)); } let (name_field, fields) = (fields[0], &fields[1..]); let (stdoff_field, fields) = (fields[0], &fields[1..]); @@ -470,23 +458,20 @@ impl ZoneFirstP { let (format_field, fields) = (fields[0], &fields[1..]); let name = name_field .parse::() - .map_err(|e| e.context("failed to parse NAME field"))?; + .context(E::FailedParseFieldName)?; let stdoff = stdoff_field .parse::() - .map_err(|e| e.context("failed to parse STDOFF field"))?; + .context(E::FailedParseFieldStdOff)?; let rules = rules_field .parse::() - .map_err(|e| e.context("failed to parse RULES field"))?; + .context(E::FailedParseFieldRules)?; let format = format_field .parse::() - .map_err(|e| e.context("failed to parse FORMAT field"))?; + .context(E::FailedParseFieldFormat)?; let until = if fields.is_empty() { None } else { - Some( - ZoneUntilP::parse(fields) - .map_err(|e| e.context("failed to parse UNTIL field"))?, - ) + Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?) }; Ok(ZoneFirstP { name, stdoff, rules, format, until }) } @@ -512,29 +497,24 @@ struct ZoneContinuationP { impl ZoneContinuationP { fn parse(fields: &[&str]) -> Result { if fields.len() < 3 { - return Err(err!( - "continuation ZONE line must have at least 3 fields" - )); + return Err(Error::from(E::ExpectedContinuationZoneThreeFields)); } 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::() - .map_err(|e| e.context("failed to parse STDOFF field"))?; + .context(E::FailedParseFieldStdOff)?; let rules = rules_field .parse::() - .map_err(|e| e.context("failed to parse RULES field"))?; + .context(E::FailedParseFieldRules)?; let format = format_field .parse::() - .map_err(|e| e.context("failed to parse FORMAT field"))?; + .context(E::FailedParseFieldFormat)?; let until = if fields.is_empty() { None } else { - Some( - ZoneUntilP::parse(fields) - .map_err(|e| e.context("failed to parse UNTIL field"))?, - ) + Some(ZoneUntilP::parse(fields).context(E::FailedParseFieldUntil)?) }; Ok(ZoneContinuationP { stdoff, rules, format, until }) } @@ -553,17 +533,14 @@ struct LinkP { impl LinkP { fn parse(fields: &[&str]) -> Result { if fields.len() != 2 { - return Err(err!( - "expected exactly 2 fields after LINK, but found {}", - fields.len() - )); + return Err(Error::from(E::ExpectedLinkTwoFields)); } let target = fields[0] .parse::() - .map_err(|e| e.context("failed to parse LINK target"))?; + .context(E::FailedParseFieldLinkTarget)?; let name = fields[1] .parse::() - .map_err(|e| e.context("failed to parse LINK name"))?; + .context(E::FailedParseFieldLinkName)?; Ok(LinkP { target, name }) } } @@ -592,12 +569,9 @@ 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(err!("NAME field for rule cannot be empty")) + Err(Error::from(E::ExpectedNonEmptyName)) } else if name.starts_with(|ch| matches!(ch, '0'..='9' | '+' | '-')) { - Err(err!( - "NAME field cannot begin with a digit, + or -, \ - but {name:?} begins with one of those", - )) + Err(Error::from(E::ExpectedNameBegin)) } else { Ok(RuleNameP { name: name.to_string() }) } @@ -614,8 +588,7 @@ impl FromStr for RuleFromP { type Err = Error; fn from_str(from: &str) -> Result { - let year = parse_year(from) - .map_err(|e| e.context("failed to parse FROM field"))?; + let year = parse_year(from).context(E::FailedParseFieldFrom)?; Ok(RuleFromP { year }) } } @@ -642,8 +615,7 @@ impl FromStr for RuleToP { } else if to.starts_with("o") && "only".starts_with(to) { Ok(RuleToP::Only) } else { - let year = parse_year(to) - .map_err(|e| e.context("failed to parse TO field"))?; + let year = parse_year(to).context(E::FailedParseFieldTo)?; Ok(RuleToP::Year { year }) } } @@ -679,7 +651,7 @@ impl FromStr for RuleInP { return Ok(RuleInP { month }); } } - Err(err!("unrecognized month name: {field:?}")) + Err(Error::from(E::UnrecognizedMonthName)) } } @@ -747,7 +719,7 @@ impl FromStr for RuleOnP { // field. That gets checked at a higher level. Ok(RuleOnP::Day { day }) } else { - Err(err!("unrecognized format for day-of-month: {field:?}")) + Err(Error::from(E::UnrecognizedDayOfMonthFormat)) } } } @@ -782,7 +754,7 @@ impl FromStr for RuleAtP { fn from_str(at: &str) -> Result { if at.is_empty() { - return Err(err!("empty field is not a valid AT value")); + return Err(Error::from(E::ExpectedNonEmptyAt)); } let (span_string, suffix_string) = at.split_at(at.len() - 1); if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) { @@ -813,7 +785,7 @@ impl FromStr for RuleAtSuffixP { "w" => Ok(RuleAtSuffixP::Wall), "s" => Ok(RuleAtSuffixP::Standard), "u" | "g" | "z" => Ok(RuleAtSuffixP::Universal), - _ => Err(err!("unrecognized AT time suffix {suffix:?}")), + _ => Err(Error::from(E::UnrecognizedAtTimeSuffix)), } } } @@ -867,7 +839,7 @@ impl FromStr for RuleSaveP { fn from_str(at: &str) -> Result { if at.is_empty() { - return Err(err!("empty field is not a valid SAVE value")); + return Err(Error::from(E::ExpectedNonEmptySave)); } let (span_string, suffix_string) = at.split_at(at.len() - 1); if suffix_string.chars().all(|ch| ch.is_ascii_alphabetic()) { @@ -902,7 +874,7 @@ impl FromStr for RuleSaveSuffixP { match suffix { "s" => Ok(RuleSaveSuffixP::Standard), "d" => Ok(RuleSaveSuffixP::Dst), - _ => Err(err!("unrecognized SAVE time suffix {suffix:?}")), + _ => Err(Error::from(E::UnrecognizedSaveTimeSuffix)), } } } @@ -939,14 +911,13 @@ impl FromStr for ZoneNameP { fn from_str(name: &str) -> Result { if name.is_empty() { - return Err(err!("zone names cannot be empty")); + return Err(Error::from(E::ExpectedNonEmptyZoneName)); } for component in name.split('/') { if component == "." || component == ".." { - return Err(err!( - "component {component:?} in zone name {name:?} cannot \ - be \".\" or \"..\"", - )); + return Err(Error::from(E::ExpectedZoneNameComponentNoDots { + component: component.into(), + })); } } Ok(ZoneNameP { name: name.to_string() }) @@ -1035,16 +1006,12 @@ impl FromStr for ZoneFormatP { fn from_str(format: &str) -> Result { fn check_abbrev(abbrev: &str) -> Result { if abbrev.is_empty() { - return Err(err!("empty abbreviations are not allowed")); + return Err(Error::from(E::ExpectedNonEmptyAbbreviation)); } let is_ok = |ch| matches!(ch, '+'|'-'|'0'..='9'|'A'..='Z'|'a'..='z'); if !abbrev.chars().all(is_ok) { - return Err(err!( - "abbreviation {abbrev:?} \ - contains invalid character; only \"+\", \"-\" and \ - ASCII alpha-numeric characters are allowed" - )); + return Err(Error::from(E::InvalidAbbreviation)); } Ok(abbrev.to_string()) } @@ -1098,28 +1065,24 @@ enum ZoneUntilP { impl ZoneUntilP { fn parse(fields: &[&str]) -> Result { if fields.is_empty() { - return Err(err!("expected at least a year")); + return Err(Error::from(E::ExpectedUntilYear)); } let (year_field, fields) = (fields[0], &fields[1..]); - let year = parse_year(year_field) - .map_err(|e| e.context("failed to parse year"))?; + let year = parse_year(year_field).context(E::FailedParseYear)?; if fields.is_empty() { return Ok(ZoneUntilP::Year { year }); } let (month_field, fields) = (fields[0], &fields[1..]); - let month = month_field - .parse::() - .map_err(|e| e.context("failed to parse month"))?; + let month = + month_field.parse::().context(E::FailedParseMonth)?; if fields.is_empty() { return Ok(ZoneUntilP::YearMonth { year, month }); } let (day_field, fields) = (fields[0], &fields[1..]); - let day = day_field - .parse::() - .map_err(|e| e.context("failed to parse day"))?; + let day = day_field.parse::().context(E::FailedParseDay)?; if fields.is_empty() { return Ok(ZoneUntilP::YearMonthDay { year, month, day }); } @@ -1127,13 +1090,9 @@ impl ZoneUntilP { let (duration_field, fields) = (fields[0], &fields[1..]); let duration = duration_field .parse::() - .map_err(|e| e.context("failed to parse time duration"))?; + .context(E::FailedParseTimeDuration)?; if !fields.is_empty() { - return Err(err!( - "expected no more fields after time of day, \ - but found: {fields:?}", - fields = fields.join(" "), - )); + return Err(Error::from(E::ExpectedNothingAfterTime)); } Ok(ZoneUntilP::YearMonthDayTime { year, month, day, duration }) } @@ -1200,10 +1159,8 @@ fn parse_year(year: &str) -> Result { } else { (t::Sign::N::<1>(), year) }; - 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}"))?; + let number = parse::i64(rest.as_bytes()).context(E::FailedParseYear)?; + let year = t::Year::try_new("year", number)?; Ok(year * sign) } @@ -1239,36 +1196,28 @@ 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(err!( - "expected time duration to contain at least one hour digit" - )); + return Err(Error::from(E::ExpectedTimeOneHour)); } - 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"))?; + let hours = + parse::i64(hour_digits.as_bytes()).context(E::FailedParseHour)?; + span = span.try_hours(hours.saturating_mul(i64::from(sign.get())))?; if rest.is_empty() { return Ok(span); } // Now pluck out the minute component. if !rest.starts_with(":") { - return Err(err!("expected ':' after hours, but found {rest:?}")); + return Err(Error::from(E::ExpectedColonAfterHour)); } 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(err!( - "expected minute digits after 'HH:', but found {rest:?} instead" - )); + return Err(Error::from(E::ExpectedMinuteAfterHours)); } - 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") - })?; + let minutes = + parse::i64(minute_digits.as_bytes()).context(E::FailedParseMinute)?; + let minutes_ranged = t::Minute::try_new("minutes", minutes)?; span = span.minutes_ranged((minutes_ranged * sign).rinto()); if rest.is_empty() { return Ok(span); @@ -1276,21 +1225,17 @@ fn parse_span(span: &str) -> Result { // Now pluck out the second component. if !rest.starts_with(":") { - return Err(err!("expected ':' after minutes, but found {rest:?}")); + return Err(Error::from(E::ExpectedColonAfterMinute)); } 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(err!( - "expected second digits after 'MM:', but found {rest:?} instead" - )); + return Err(Error::from(E::ExpectedSecondAfterMinutes)); } - 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") - })?; + let seconds = + parse::i64(second_digits.as_bytes()).context(E::FailedParseSecond)?; + let seconds_ranged = t::Second::try_new("seconds", seconds)?; span = span.seconds_ranged((seconds_ranged * sign).rinto()); if rest.is_empty() { return Ok(span); @@ -1298,33 +1243,24 @@ fn parse_span(span: &str) -> Result { // Now look for the fractional nanosecond component. if !rest.starts_with(".") { - return Err(err!("expected '.' after seconds, but found {rest:?}")); + return Err(Error::from(E::ExpectedDotAfterSeconds)); } 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(err!( - "expected nanosecond digits after 'SS.', \ - but found {rest:?} instead" - )); + return Err(Error::from(E::ExpectedNanosecondDigits)); } - 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") - })?; + let nanoseconds = parse::fraction(nanosecond_digits.as_bytes()) + .context(E::FailedParseNanosecond)?; + let nanoseconds_ranged = + t::FractionalNanosecond::try_new("nanoseconds", nanoseconds)?; span = span.nanoseconds_ranged((nanoseconds_ranged * sign).rinto()); // We should have consumed everything at this point. if !rest.is_empty() { - return Err(err!( - "found unrecognized trailing {rest:?} in time duration" - )); + return Err(Error::from(E::UnrecognizedTrailingTimeDuration)); } span.rebalance(Unit::Hour) } @@ -1334,10 +1270,8 @@ 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()) - .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"))?; + let number = parse::i64(string.as_bytes()).context(E::FailedParseDay)?; + let day = t::Day::try_new("day", number)?; Ok(day) } @@ -1357,7 +1291,7 @@ fn parse_weekday(string: &str) -> Result { return Ok(weekday); } } - Err(err!("unrecognized day of the week: {string:?}")) + Err(Error::from(E::UnrecognizedDayOfWeek)) } /// A parser that emits lines as sequences of fields. @@ -1393,7 +1327,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(|e| err!("invalid UTF-8: {e}"))?; + .map_err(|_| Error::from(E::InvalidUtf8))?; Ok(FieldParser::new(src)) } @@ -1409,12 +1343,10 @@ 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_else(|| err!("line count overflowed"))?; + self.line_number = + self.line_number.checked_add(1).ok_or(E::LineOverflow)?; parse_fields(&line, &mut self.fields) - .with_context(|| err!("line {}", self.line_number))?; + .context(E::Line { number: self.line_number })?; if self.fields.is_empty() { continue; } @@ -1452,13 +1384,6 @@ 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, @@ -1469,15 +1394,11 @@ fn parse_fields<'a>( fields.clear(); if line.len() > MAX_LINE_LEN { - return Err(err!( - "line with length {} exceeds \ - max length of {MAX_LINE_LEN}", - line.len() - )); + return Err(Error::from(E::LineMaxLength)); } // Do a quick scan for a NUL terminator. They are illegal in all cases. if line.contains('\x00') { - return Err(err!("found line with NUL byte, which isn't allowed")); + return Err(Error::from(E::LineNul)); } // The current state of the parser. We start at whitespace, since it also // means "before a field." @@ -1522,9 +1443,8 @@ fn parse_fields<'a>( } State::AfterQuote => { if !is_space(ch) { - return Err(err!( - "expected whitespace after quoted field, \ - but found {ch:?} instead", + return Err(Error::from( + E::ExpectedWhitespaceAfterQuotedField, )); } State::Whitespace @@ -1537,7 +1457,7 @@ fn parse_fields<'a>( fields.push(&line[start..]); } State::InQuote => { - return Err(err!("found unclosed quote")); + return Err(Error::from(E::ExpectedCloseQuote)); } } Ok(()) diff --git a/src/util/c.rs b/src/util/c.rs index 468e5c0..bd054ab 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() { - write!(f, "-") + f.write_str("-") } else { Ok(()) } diff --git a/src/util/cache.rs b/src/util/cache.rs index bf1e7bf..a6c1eee 100644 --- a/src/util/cache.rs +++ b/src/util/cache.rs @@ -36,15 +36,13 @@ impl Expiration { impl core::fmt::Display for Expiration { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - 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:?}") + 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), + } } } diff --git a/src/util/escape.rs b/src/util/escape.rs index 488785d..8aec114 100644 --- a/src/util/escape.rs +++ b/src/util/escape.rs @@ -4,8 +4,121 @@ Provides convenience routines for escaping raw bytes. This was copied from `regex-automata` with a few light edits. */ -// 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}; +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(()) + } +} diff --git a/src/util/parse.rs b/src/util/parse.rs index fced840..a95d7ff 100644 --- a/src/util/parse.rs +++ b/src/util/parse.rs @@ -1,7 +1,4 @@ -use crate::{ - error::{err, Error}, - util::escape::{Byte, Bytes}, -}; +use crate::error::util::{ParseFractionError, ParseIntError}; /// Parses an `i64` number from the beginning to the end of the given slice of /// ASCII digit characters. @@ -13,38 +10,20 @@ use crate::{ /// 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(err!("invalid number, no digits found")); + return Err(ParseIntError::NoDigitsFound); } let mut n: i64 = 0; for &byte in bytes { - 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), - ) - }, - )?; + 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)?; } Ok(n) } @@ -65,7 +44,9 @@ 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]), Error> { +pub(crate) fn u64_prefix( + bytes: &[u8], +) -> Result<(Option, &[u8]), ParseIntError> { // Discovered via `u64::MAX.to_string().len()`. const MAX_U64_DIGITS: usize = 20; @@ -79,15 +60,10 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option, &[u8]), Error> { 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_else( - #[inline(never)] - || { - err!( - "number `{}` too big to parse into 64-bit integer", - Bytes(&bytes[..digit_count]), - ) - }, - )?; + n = n + .checked_mul(10) + .and_then(|n| n.checked_add(digit)) + .ok_or(ParseIntError::TooBig)?; } if digit_count == 0 { return Ok((None, bytes)); @@ -105,54 +81,33 @@ pub(crate) fn u64_prefix(bytes: &[u8]) -> Result<(Option, &[u8]), Error> { /// /// 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 { - const MAX_PRECISION: usize = 9; - +pub(crate) fn fraction(bytes: &[u8]) -> Result { if bytes.is_empty() { - 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" - )); + return Err(ParseFractionError::NoDigitsFound); + } else if bytes.len() > ParseFractionError::MAX_PRECISION { + return Err(ParseFractionError::TooManyDigits); } let mut n: u32 = 0; for &byte in bytes { let digit = match byte.checked_sub(b'0') { None => { - return Err(err!( - "invalid fractional digit, expected 0-9 but got {}", - Byte(byte), - )); + return Err(ParseFractionError::InvalidDigit(byte)); } Some(digit) if digit > 9 => { - return Err(err!( - "invalid fractional digit, expected 0-9 but got {}", - Byte(byte), - )) + return Err(ParseFractionError::InvalidDigit(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( - || { - err!( - "fractional '{}' too big to parse into 64-bit integer", - Bytes(bytes), - ) - }, - )?; + n = n + .checked_mul(10) + .and_then(|n| n.checked_add(digit)) + .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) - ) - })?; + for _ in bytes.len()..ParseFractionError::MAX_PRECISION { + n = n.checked_mul(10).ok_or_else(|| ParseFractionError::TooBig)?; } Ok(n) } @@ -161,15 +116,17 @@ pub(crate) fn fraction(bytes: &[u8]) -> Result { /// /// This is effectively `OsStr::to_str`, but with a slightly better error /// message. -#[cfg(feature = "tzdb-zoneinfo")] -pub(crate) fn os_str_utf8<'o, O>(os_str: &'o O) -> Result<&'o str, Error> +#[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> where O: ?Sized + AsRef, { let os_str = os_str.as_ref(); os_str .to_str() - .ok_or_else(|| err!("environment value {os_str:?} is not valid UTF-8")) + .ok_or_else(|| crate::error::util::OsStrUtf8Error::from(os_str)) } /// Parses an `OsStr` into a `&str` when `&[u8]` isn't easily available. @@ -178,7 +135,9 @@ 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], Error> +pub(crate) fn os_str_bytes<'o, O>( + os_str: &'o O, +) -> Result<&'o [u8], crate::error::util::OsStrUtf8Error> where O: ?Sized + AsRef, { @@ -190,16 +149,13 @@ 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(string.as_bytes()) + Ok(os_str_utf8(os_str)?.as_bytes()) } } diff --git a/src/util/rangeint.rs b/src/util/rangeint.rs index a20ec34..82bf3a4 100644 --- a/src/util/rangeint.rs +++ b/src/util/rangeint.rs @@ -164,16 +164,15 @@ macro_rules! define_ranged { val: impl Into, ) -> Result { let val = val.into(); - #[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)) + <$repr>::try_from(val).ok().and_then(Self::new).ok_or_else( + || { + Error::range( + what, + val, + Self::MIN_REPR, + Self::MAX_REPR, + ) + }) } #[inline] @@ -182,16 +181,15 @@ macro_rules! define_ranged { val: impl Into, ) -> Result { let val = val.into(); - #[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)) + <$repr>::try_from(val).ok().and_then(Self::new).ok_or_else( + || { + Error::range( + what, + val, + Self::MIN_REPR, + Self::MAX_REPR, + ) + }) } #[inline] @@ -379,7 +377,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] + #[inline(never)] pub(crate) fn to_error_with_bounds( self, what: &'static str, @@ -2095,7 +2093,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 => write!(f, "{:?}", self), + None => core::fmt::Debug::fmt(self, f), } } } diff --git a/src/util/round/increment.rs b/src/util/round/increment.rs index a66e2d2..7f174ac 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::{err, Error}, + error::{util::RoundingIncrementError as E, Error, ErrorContext}, 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, "span", LIMIT) + get_with_limit(unit, increment, LIMIT).context(E::ForSpan) } } @@ -67,7 +67,7 @@ pub(crate) fn for_datetime( t::HOURS_PER_CIVIL_DAY, Constant(2), ]; - get_with_limit(unit, increment, "datetime", LIMIT) + get_with_limit(unit, increment, LIMIT).context(E::ForDateTime) } /// 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, "time", LIMIT) + get_with_limit(unit, increment, LIMIT).context(E::ForTime) } /// Validates the given rounding increment for the given unit. @@ -107,38 +107,25 @@ pub(crate) fn for_timestamp( t::MINUTES_PER_CIVIL_DAY, t::HOURS_PER_CIVIL_DAY, ]; - get_with_max(unit, increment, "timestamp", MAX) + get_with_max(unit, increment, MAX).context(E::ForTimestamp) } 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(err!( - "rounding increment {increment} for {unit} must be \ - greater than zero", - unit = unit.plural(), - )); + return Err(E::GreaterThanZero { unit }); } let Some(must_divide) = limit.get(unit as usize) else { - return Err(err!( - "{what} rounding does not support {unit}", - unit = unit.plural() - )); + return Err(E::Unsupported { unit }); }; let must_divide = t::NoUnits::rfrom(*must_divide); if increment >= must_divide || must_divide % increment != C(0) { - 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(), - )) + Err(E::InvalidDivide { unit, must_divide: must_divide.get() }) } else { Ok(t::NoUnits128::rfrom(increment)) } @@ -147,32 +134,19 @@ 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(err!( - "rounding increment {increment} for {unit} must be \ - greater than zero", - unit = unit.plural(), - )); + return Err(E::GreaterThanZero { unit }); } let Some(must_divide) = max.get(unit as usize) else { - return Err(err!( - "{what} rounding does not support {unit}", - unit = unit.plural() - )); + return Err(E::Unsupported { unit }); }; let must_divide = t::NoUnits::rfrom(*must_divide); if increment > must_divide || must_divide % increment != C(0) { - 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(), - )) + Err(E::InvalidDivide { unit, must_divide: must_divide.get() }) } else { Ok(t::NoUnits128::rfrom(increment)) } diff --git a/src/util/utf8.rs b/src/util/utf8.rs index 0ea226c..bd5b462 100644 --- a/src/util/utf8.rs +++ b/src/util/utf8.rs @@ -1,5 +1,59 @@ 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 @@ -12,11 +66,28 @@ use core::cmp::Ordering; /// /// 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> { - crate::shared::util::utf8::decode(bytes) +/// *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())) } /// Like std's `eq_ignore_ascii_case`, but returns a full `Ordering`. diff --git a/src/zoned.rs b/src/zoned.rs index 8f37e78..60c0c0d 100644 --- a/src/zoned.rs +++ b/src/zoned.rs @@ -6,7 +6,7 @@ use crate::{ Weekday, }, duration::{Duration, SDuration}, - error::{err, Error, ErrorContext}, + error::{zoned::Error as E, Error, ErrorContext}, fmt::{ self, temporal::{self, DEFAULT_DATETIME_PARSER}, @@ -2216,41 +2216,20 @@ impl Zoned { .timestamp() .checked_add(span) .map(|ts| ts.to_zoned(self.time_zone().clone())) - .with_context(|| { - err!( - "failed to add span {span} to timestamp {timestamp} \ - from zoned datetime {zoned}", - timestamp = self.timestamp(), - zoned = self, - ) - }); + .context(E::AddTimestamp); } let span_time = span.only_time(); - 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 dt = self + .datetime() + .checked_add(span_calendar) + .context(E::AddDateTime)?; let tz = self.time_zone(); - 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})" - ) - })?; + let mut ts = tz + .to_ambiguous_timestamp(dt) + .compatible() + .context(E::ConvertDateTimeToTimestamp)?; + ts = ts.checked_add(span_time).context(E::AddTimestamp)?; Ok(ts.to_zoned(tz.clone())) } @@ -4327,13 +4306,7 @@ impl<'a> ZonedDifference<'a> { return zdt1.timestamp().until((largest, zdt2.timestamp())); } if zdt1.time_zone() != zdt2.time_zone() { - 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(), - )); + return Err(Error::from(E::MismatchTimeZoneUntil { largest })); } let tz = zdt1.time_zone(); @@ -4347,43 +4320,27 @@ impl<'a> ZonedDifference<'a> { let mut mid = dt2 .date() .checked_add(Span::new().days_ranged(day_correct * -sign)) - .with_context(|| { - err!( - "failed to add {days} days to date in {dt2}", - days = day_correct * -sign, - ) - })? + .context(E::AddDays)? .to_datetime(dt1.time()); - 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(), - ) - })?; + let mut zmid: Zoned = mid + .to_zoned(tz.clone()) + .context(E::ConvertIntermediateDatetime)?; 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)) - .with_context(|| { - err!( - "failed to add {days} days to date in {dt2}", - days = day_correct * -sign, - ) - })? + .context(E::AddDays)? .to_datetime(dt1.time()); - 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(), - ) - })?; + zmid = mid + .to_zoned(tz.clone()) + .context(E::ConvertIntermediateDatetime)?; if t::sign(zdt2, &zmid) == -sign { + // FIXME panic!("this should be an error too"); } } @@ -4635,32 +4592,18 @@ 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().with_context(move || { - err!("failed to find start of day for {zdt}") - })?; + let start = zdt.start_of_day().context(E::FailedStartOfDay)?; let end = start .checked_add(Span::new().days_ranged(C(1).rinto())) - .with_context(|| { - err!("failed to add 1 day to {start} to find length of day") - })?; + .context(E::FailedLengthOfDay)?; let span = start .timestamp() .until((Unit::Nanosecond, end.timestamp())) - .with_context(|| { - err!( - "failed to compute span in nanoseconds \ - from {start} until {end}" - ) - })?; + .context(E::FailedSpanNanoseconds)?; let nanos = span.get_nanoseconds_ranged(); let day_length = ZonedDayNanoseconds::try_rfrom("nanoseconds-per-zoned-day", nanos) - .with_context(|| { - err!( - "failed to convert span between {start} until {end} \ - to nanoseconds", - ) - })?; + .context(E::FailedSpanNanoseconds)?; let progress = zdt.timestamp().as_nanosecond_ranged() - start.timestamp().as_nanosecond_ranged(); let rounded = self.round.get_mode().round(progress, day_length); @@ -6078,21 +6021,21 @@ mod tests { insta::assert_snapshot!( zdt.round(Unit::Year).unwrap_err(), - @"datetime rounding does not support years" + @"failed rounding datetime: rounding to years is not supported" ); insta::assert_snapshot!( zdt.round(Unit::Month).unwrap_err(), - @"datetime rounding does not support months" + @"failed rounding datetime: rounding to months is not supported" ); insta::assert_snapshot!( zdt.round(Unit::Week).unwrap_err(), - @"datetime rounding does not support weeks" + @"failed rounding datetime: rounding to weeks is not supported" ); let options = ZonedRound::new().smallest(Unit::Day).increment(2); insta::assert_snapshot!( zdt.round(options).unwrap_err(), - @"increment 2 for rounding datetime to days must be 1) less than 2, 2) divide into it evenly and 3) greater than zero" + @"failed rounding datetime: increment for rounding to days must be 1) less than 2, 2) divide into it evenly and 3) greater than zero" ); } @@ -6124,12 +6067,12 @@ mod tests { insta::assert_snapshot!( "1970-06-01T00:00:00-00:44:40[Africa/Monrovia]".parse::().unwrap_err(), - @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"#, + @"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`", ); insta::assert_snapshot!( "1970-06-01T00:00:00-00:45:00[Africa/Monrovia]".parse::().unwrap_err(), - @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"#, + @"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`", ); } diff --git a/testprograms/invalid-tz-environment-variable/main.rs b/testprograms/invalid-tz-environment-variable/main.rs index 75544d2..4dc6eaf 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=\"WAT5HUH\" as a TZif file \ - after attempting a tzdb lookup for `WAT5HUH`", + "`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", ); // 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=\"/usr/share/zoneinfo/WAT5HUH\" as a TZif file \ - after attempting a tzdb lookup for `WAT5HUH`", + "`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", ); unsafe { diff --git a/tests/tc39_262/span/add.rs b/tests/tc39_262/span/add.rs index 187895f..95c8093 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 `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 `jiff::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 24e7651..53807c2 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 `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 `jiff::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 `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 `jiff::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 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", + @"failed to add overflowing span: 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 -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", + @"failed to add overflowing span: 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 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", + @"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", ); 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 631107417600999999999 to span for largest unit as nanoseconds: parameter 'nanoseconds' with value 631107417600999999999 is not in the required range of -9223372036854775807..=9223372036854775807", + @"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", ); 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 `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 `jiff::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 `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 `jiff::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 `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 `jiff::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 (`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 (`jiff::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 (`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 (`jiff::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 `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 `jiff::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 `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 `jiff::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 `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 `jiff::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 `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 `jiff::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 `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 `jiff::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 `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 `jiff::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 631107417601000000000 to span for largest unit as seconds: parameter 'seconds' with value 631107417601 is not in the required range of -631107417600..=631107417600", + @"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", ); Ok(()) diff --git a/tests/tc39_262/span/total.rs b/tests/tc39_262/span/total.rs index a3968ae..985581f 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 `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 `jiff::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 `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 `jiff::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);