From 552b9d1fef2f840ffd4bec2949ddee94d011068b Mon Sep 17 00:00:00 2001 From: Azan Ali Date: Fri, 7 Nov 2025 16:04:40 +0500 Subject: [PATCH 01/30] civil: added `FromStr` and `Display` impls for `ISOWeekDate` This also includes `Deserialize` and `Serialize` Serde impls, deferring to `FromStr` and `Display`, respectively. This brings `ISOWeekDate` into parity with the other datetime types in terms of parsing/printing. N.B. In this commit, we start trying to make error messages a little more consistent. So there are some changes here that impact the messages other than ISO 8601 week date parsing. Closes #412 --- CHANGELOG.md | 11 + src/civil/iso_week_date.rs | 118 +++++++++ src/fmt/temporal/mod.rs | 39 ++- src/fmt/temporal/parser.rs | 496 +++++++++++++++++++++++++++++++----- src/fmt/temporal/printer.rs | 53 +++- 5 files changed, 654 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 705254a..6e55473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 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. + + 0.2.16 (2025-11-07) =================== This release contains a number of enhancements and bug fixes that have accrued diff --git a/src/civil/iso_week_date.rs b/src/civil/iso_week_date.rs index 7452546..2bcdd3f 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}, + 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 @@ -747,6 +793,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 +872,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/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs index 15178e5..52fd799 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, @@ -1110,6 +1110,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 +1980,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 +2490,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 `-000000-01-01`: year zero must be written without a sign or a positive sign, but not a negative sign", ); } diff --git a/src/fmt/temporal/parser.rs b/src/fmt/temporal/parser.rs index 9826f5e..5444804 100644 --- a/src/fmt/temporal/parser.rs +++ b/src/fmt/temporal/parser.rs @@ -1,5 +1,5 @@ use crate::{ - civil::{Date, DateTime, Time}, + civil::{Date, DateTime, ISOWeekDate, Time, Weekday}, error::{err, Error, ErrorContext}, fmt::{ offset::{self, ParsedOffset}, @@ -559,6 +559,64 @@ 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> { + 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}`") + })?; + 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")?; + + // Parse 'W' prefix before week num. + let Parsed { input, .. } = + self.parse_week_prefix(input).with_context(|| { + err!( + "failed to parse week number prefix \ + in date `{original}`" + ) + })?; + + // Parse week num component. + let Parsed { value: week, input } = + self.parse_week_num(input).with_context(|| { + err!("failed to parse week number in date `{original}`") + })?; + + // Parse optional separator. + let Parsed { input, .. } = self + .parse_date_separator(input, extended) + .context("failed to parse separator after week number")?; + + // Parse day component. + let Parsed { value: weekday, input } = + self.parse_weekday(input).with_context(|| { + err!("failed to parse weekday in date `{original}`") + })?; + + let iso_week_date = ISOWeekDate::new_ranged(year, week, weekday) + .with_context(|| { + err!("week date parsed from `{original}` is not valid") + })?; + + Ok(Parsed { value: iso_week_date, input: input }) + } + // Date ::: // DateYear - DateMonth - DateDay // DateYear DateMonth DateDay @@ -573,7 +631,7 @@ impl DateTimeParser { // Parse year component. let Parsed { value: year, input } = self.parse_year(input).with_context(|| { - err!("failed to parse year in date {original:?}") + err!("failed to parse year in date `{original}`") })?; let extended = input.starts_with(b"-"); @@ -585,7 +643,7 @@ impl DateTimeParser { // Parse month component. let Parsed { value: month, input } = self.parse_month(input).with_context(|| { - err!("failed to parse month in date {original:?}") + err!("failed to parse month in date `{original}`") })?; // Parse optional separator. @@ -596,11 +654,11 @@ impl DateTimeParser { // Parse day component. let Parsed { value: day, input } = self.parse_day(input).with_context(|| { - err!("failed to parse day in date {original:?}") + err!("failed to parse day in date `{original}`") })?; let date = Date::new_ranged(year, month, day).with_context(|| { - err!("date parsed from {original:?} is not valid") + err!("date parsed from `{original}` is not valid") })?; let value = ParsedDate { input: escape::Bytes(mkslice(input)), date }; Ok(Parsed { value, input }) @@ -623,7 +681,7 @@ impl DateTimeParser { // Parse hour component. let Parsed { value: hour, input } = self.parse_hour(input).with_context(|| { - err!("failed to parse hour in time {original:?}") + err!("failed to parse hour in time `{original}`") })?; let extended = input.starts_with(b":"); @@ -646,7 +704,7 @@ impl DateTimeParser { } let Parsed { value: minute, input } = self.parse_minute(input).with_context(|| { - err!("failed to parse minute in time {original:?}") + err!("failed to parse minute in time `{original}`") })?; // Parse optional second component. @@ -668,7 +726,7 @@ impl DateTimeParser { } let Parsed { value: second, input } = self.parse_second(input).with_context(|| { - err!("failed to parse second in time {original:?}") + err!("failed to parse second in time `{original}`") })?; // Parse an optional fractional component. @@ -676,7 +734,7 @@ impl DateTimeParser { parse_temporal_fraction(input).with_context(|| { err!( "failed to parse fractional nanoseconds \ - in time {original:?}", + in time `{original}`", ) })?; @@ -720,7 +778,7 @@ impl DateTimeParser { // Parse month component. let Parsed { value: month, mut input } = self.parse_month(input).with_context(|| { - err!("failed to parse month in month-day {original:?}") + err!("failed to parse month in month-day `{original}`") })?; // Skip over optional separator. @@ -731,7 +789,7 @@ impl DateTimeParser { // Parse day component. let Parsed { value: day, input } = self.parse_day(input).with_context(|| { - err!("failed to parse day in month-day {original:?}") + err!("failed to parse day in month-day `{original}`") })?; // Check that the month-day is valid. Since Temporal's month-day @@ -740,7 +798,7 @@ impl DateTimeParser { // 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") + err!("month-day parsed from `{original}` is not valid") })?; // We have a valid year-month. But we don't return it because we just @@ -763,7 +821,7 @@ impl DateTimeParser { // Parse year component. let Parsed { value: year, mut input } = self.parse_year(input).with_context(|| { - err!("failed to parse year in date {original:?}") + err!("failed to parse year in date `{original}`") })?; // Skip over optional separator. @@ -774,14 +832,14 @@ impl DateTimeParser { // Parse month component. let Parsed { value: month, input } = self.parse_month(input).with_context(|| { - err!("failed to parse month in month-day {original:?}") + err!("failed to parse month in month-day `{original}`") })?; // Check that the year-month is valid. We just use a day of 1, since // every month in every year must have a day 1. let day = t::Day::N::<1>(); let _ = Date::new_ranged(year, month, day).with_context(|| { - err!("year-month parsed from {original:?} is not valid") + err!("year-month parsed from `{original}` is not valid") })?; // We have a valid year-month. But we don't return it because we just @@ -865,7 +923,7 @@ impl DateTimeParser { })?; let month = parse::i64(month).with_context(|| { err!( - "failed to parse {month:?} as month (a two digit integer)", + "failed to parse `{month}` as month (a two digit integer)", month = escape::Bytes(month), ) })?; @@ -890,7 +948,7 @@ impl DateTimeParser { })?; let day = parse::i64(day).with_context(|| { err!( - "failed to parse {day:?} as day (a two digit integer)", + "failed to parse `{day}` as day (a two digit integer)", day = escape::Bytes(day), ) })?; @@ -947,7 +1005,7 @@ impl DateTimeParser { })?; let minute = parse::i64(minute).with_context(|| { err!( - "failed to parse {minute:?} as minute (a two digit integer)", + "failed to parse `{minute}` as minute (a two digit integer)", minute = escape::Bytes(minute), ) })?; @@ -977,7 +1035,7 @@ impl DateTimeParser { })?; let mut second = parse::i64(second).with_context(|| { err!( - "failed to parse {second:?} as second (a two digit integer)", + "failed to parse `{second}` as second (a two digit integer)", second = escape::Bytes(second), ) })?; @@ -1031,19 +1089,19 @@ impl DateTimeParser { if input.starts_with(b"-") { return Err(err!( "expected no separator after month since none was \ - found after the year, but found a '-' separator", + found after the year, but found a `-` separator", )); } return Ok(Parsed { value: (), input }); } if input.is_empty() { return Err(err!( - "expected '-' separator, but found end of input" + "expected `-` separator, but found end of input" )); } if input[0] != b'-' { return Err(err!( - "expected '-' separator, but found {found:?} instead", + "expected `-` separator, but found `{found}` instead", found = escape::Byte(input[0]), )); } @@ -1109,6 +1167,75 @@ impl DateTimeParser { input = &input[1..]; Parsed { value: Some(sign), input } } + + /// 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, + mut input: &'i [u8], + ) -> Result, Error> { + if input.is_empty() { + return Err(err!("expected `W` or `w`, but found end of input")); + } + if !matches!(input[0], b'W' | b'w') { + return Err(err!( + "expected `W` or `w`, but found `{found}` instead", + found = escape::Byte(input[0]), + )); + } + input = &input[1..]; + 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_else(|| { + err!("expected two digit week number, but found end of input") + })?; + let week_num = parse::i64(week_num).with_context(|| { + err!( + "failed to parse `{week_num}` as week number, \ + expected a two digit integer", + week_num = escape::Bytes(week_num), + ) + })?; + let week_num = t::ISOWeek::try_new("week_num", week_num) + .with_context(|| { + err!("parsed week number `{week_num}` is not valid") + })?; + + 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_else(|| { + err!("expected one digit weekday, but found end of input") + })?; + let weekday = parse::i64(weekday).with_context(|| { + err!( + "failed to parse `{weekday}` as weekday (a one digit integer)", + weekday = escape::Bytes(weekday), + ) + })?; + let weekday = t::WeekdayOne::try_new("weekday", weekday) + .with_context(|| { + err!("parsed weekday `{weekday}` is not valid") + })?; + let weekday = Weekday::from_monday_one_offset_ranged(weekday); + + Ok(Parsed { value: weekday, input }) + } } /// A parser for Temporal spans. @@ -2220,7 +2347,7 @@ mod tests { // 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 `2099-13-01[America/New_York]`: minute is not valid: parameter 'minute' with value 99 is not in the required range of 0..=59", ); } @@ -2303,7 +2430,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 +2438,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 `123`: expected four digit year (or leading sign for six digit year), but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"123a").unwrap_err(), - @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"###, + @r#"failed to parse year in date `123a`: failed to parse "123a" as year (a four digit integer): invalid digit, expected 0-9 but got a"#, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-9999").unwrap_err(), - @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 `-9999`: expected six digit year (because of a leading sign), but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"+9999").unwrap_err(), - @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 `+9999`: expected six digit year (because of a leading sign), but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-99999").unwrap_err(), - @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 `-99999`: expected six digit year (because of a leading sign), but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"+99999").unwrap_err(), - @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 `+99999`: expected six digit year (because of a leading sign), but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-99999a").unwrap_err(), - @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"###, + @r#"failed to parse year in date `-99999a`: failed to parse "99999a" as year (a six digit integer): invalid digit, expected 0-9 but got a"#, ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"+999999").unwrap_err(), - @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 `+999999`: year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"-010000").unwrap_err(), - @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 `-010000`: year is not valid: parameter 'year' with value 10000 is not in the required range of -9999..=9999", ); } @@ -2352,19 +2479,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 `2024-`: 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 `2024`: 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 `2024-13-01`: month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"20241301").unwrap_err(), - @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 `20241301`: month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12", ); } @@ -2372,27 +2499,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 `2024-12-`: 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 `202412`: 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 `2024-12-40`: day is not valid: parameter 'day' with value 40 is not in the required range of 1..=31", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-11-31").unwrap_err(), - @r###"date parsed from "2024-11-31" is not valid: parameter 'day' with value 31 is not in the required range of 1..=30"###, + @"date parsed from `2024-11-31` is not valid: parameter 'day' with value 31 is not in the required range of 1..=30", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-02-30").unwrap_err(), - @r###"date parsed from "2024-02-30" is not valid: parameter 'day' with value 30 is not in the required range of 1..=29"###, + @"date parsed from `2024-02-30` is not valid: parameter 'day' with value 30 is not in the required range of 1..=29", ); insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2023-02-29").unwrap_err(), - @r###"date parsed from "2023-02-29" is not valid: parameter 'day' with value 29 is not in the required range of 1..=28"###, + @"date parsed from `2023-02-29` is not valid: parameter 'day' with value 29 is not in the required range of 1..=28", ); } @@ -2400,11 +2527,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` instead", ); 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 after month since none was found after the year, but found a `-` separator", ); } @@ -2533,7 +2660,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 +2668,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 `a`: 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"###, + @r#"failed to parse hour in time `1a`: failed to parse "1a" as hour (a two digit integer): invalid digit, expected 0-9 but got a"#, ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"24").unwrap_err(), - @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 `24`: hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23", ); } @@ -2557,19 +2684,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 `01:`: 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 `01:a`: 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 `01:1a`: failed to parse `1a` as minute (a two digit integer): invalid digit, expected 0-9 but got a", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:60").unwrap_err(), - @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 `01:60`: minute is not valid: parameter 'minute' with value 60 is not in the required range of 0..=59", ); } @@ -2577,19 +2704,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 `01:02:`: expected two digit second, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:a").unwrap_err(), - @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 `01:02:a`: expected two digit second, but found end of input", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:1a").unwrap_err(), - @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 `01:02:1a`: failed to parse `1a` as second (a two digit integer): invalid digit, expected 0-9 but got a", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:61").unwrap_err(), - @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 `01:02:61`: second is not valid: parameter 'second' with value 61 is not in the required range of 0..=59", ); } @@ -2597,11 +2724,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 nanoseconds in time `01:02:03.`: found decimal after seconds component, but did not find any decimal digits after decimal", ); insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:03.a").unwrap_err(), - @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 nanoseconds in time `01:02:03.a`: found decimal after seconds component, but did not find any decimal 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 `123`: expected four digit year (or leading sign for six digit year), but found end of input", + ); + insta::assert_snapshot!( + p("123a"), + @r#"failed to parse year in date `123a`: failed to parse "123a" as year (a four digit integer): invalid digit, expected 0-9 but got a"#, + ); + + insta::assert_snapshot!( + p("-9999"), + @"failed to parse year in date `-9999`: 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 `+9999`: 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 `-99999`: 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 `+99999`: expected six digit year (because of a leading sign), but found end of input", + ); + insta::assert_snapshot!( + p("-99999a"), + @r#"failed to parse year in date `-99999a`: failed to parse "99999a" as year (a six digit integer): invalid digit, expected 0-9 but got a"#, + ); + insta::assert_snapshot!( + p("+999999"), + @"failed to parse year in date `+999999`: year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999", + ); + insta::assert_snapshot!( + p("-010000"), + @"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", + ); + } + + #[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 `2024-`: expected `W` or `w`, but found end of input", + ); + insta::assert_snapshot!( + p("2024"), + @"failed to parse week number prefix in date `2024`: 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 `2024-W`: expected two digit week number, but found end of input", + ); + insta::assert_snapshot!( + p("2024-W1"), + @"failed to parse week number in date `2024-W1`: expected two digit week number, but found end of input", + ); + insta::assert_snapshot!( + p("2024-W53-1"), + @"week date parsed from `2024-W53-1` is not valid: ISO week number `53` is invalid for year `2024`", + ); + insta::assert_snapshot!( + p("2030W531"), + @"week date parsed from `2030W531` is not valid: ISO week number `53` is invalid for year `2030`", + ); + } + + #[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"), + @"week date parsed from `2024-W53-1` is not valid: ISO week number `53` is invalid for year `2024`", + ); + insta::assert_snapshot!( + p("2025-W53-1"), + @"week date parsed from `2025-W53-1` is not valid: ISO week number `53` is invalid for year `2025`", + ); + } + + #[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 `2024-W12-`: expected one digit weekday, but found end of input", + ); + insta::assert_snapshot!( + p("2024W12"), + @"failed to parse weekday in date `2024W12`: expected one digit weekday, but found end of input", + ); + insta::assert_snapshot!( + p("2024-W11-8"), + @"failed to parse weekday in date `2024-W11-8`: parsed weekday `8` 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` instead", + ); + insta::assert_snapshot!( + p("2024W01-5"), + @"failed to parse separator after week number: expected no separator after month since none was found after the year, but found a `-` separator", ); } } diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs index 8e80a7d..c65ad80 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -1,5 +1,5 @@ use crate::{ - civil::{Date, DateTime, Time}, + civil::{Date, DateTime, ISOWeekDate, Time}, error::{err, Error}, fmt::{ temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind}, @@ -255,6 +255,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: DecimalFormatter = + DecimalFormatter::new().padding(4); + static FMT_YEAR_NEGATIVE: DecimalFormatter = + DecimalFormatter::new().padding(6); + static FMT_TWO: DecimalFormatter = DecimalFormatter::new().padding(2); + static FMT_ONE: DecimalFormatter = DecimalFormatter::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, @@ -620,7 +648,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 +953,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", + ); + } } From 9d7e099a7a9a653b114de2465c0bc7361700c48b Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Mon, 29 Jul 2024 15:42:37 +0200 Subject: [PATCH 02/30] fuzz: add initial set of fuzzer targets There's a lot more that we could do, but this should be a good start. --- .github/workflows/ci.yml | 17 ++ .vim/coc-settings.json | 1 + fuzz/.gitignore | 4 + fuzz/Cargo.lock | 255 ++++++++++++++++++++++++++++ fuzz/Cargo.toml | 44 +++++ fuzz/fuzz_targets/rfc2822_parse.rs | 50 ++++++ fuzz/fuzz_targets/shim.rs | 48 ++++++ fuzz/fuzz_targets/strtime_parse.rs | 103 +++++++++++ fuzz/fuzz_targets/temporal_parse.rs | 51 ++++++ 9 files changed, 573 insertions(+) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/rfc2822_parse.rs create mode 100644 fuzz/fuzz_targets/shim.rs create mode 100644 fuzz/fuzz_targets/strtime_parse.rs create mode 100644 fuzz/fuzz_targets/temporal_parse.rs 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/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..f2c0ea0 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,255 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "cc" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.4" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[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 = "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 = "syn" +version = "2.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 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!(); From 4526cd266303cd5cc84e42dbe980fb0fc56e3dbf Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 10 Dec 2025 20:30:44 -0500 Subject: [PATCH 03/30] jiff-tzdb: update to tzdb 2025c Ref: https://lists.iana.org/hyperkitty/list/tz-announce@iana.org/thread/TAGXKYLMAQRZRFTERQ33CEKOW7KRJVAK/ --- crates/jiff-tzdb/concatenated-zoneinfo.dat | Bin 203287 -> 203575 bytes crates/jiff-tzdb/tzname.rs | 162 ++++++++++----------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/crates/jiff-tzdb/concatenated-zoneinfo.dat b/crates/jiff-tzdb/concatenated-zoneinfo.dat index 8b5878daa761c8d263e8c257ae07f0af3743bcbf..8990c8c305b5ed3c9e466bd7db7efa66a6a6e76e 100644 GIT binary patch delta 52 zcmV-40L%ZEv<$bl41k0Iv;u5Lx1&n}<3a(1mu*J^l($TE0$>A|VM+p*lW?^lm-tEo KtG9-Z0+EX^pcQZc delta 53 zcmV-50LuTjwhWiF41k0Iv;u5L0c4j@iUO3EC2<05w}Em3^Z}PdjslmL`;G#uw`@lO L<3hKlj{=d4LVp%f 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), ]; From f889e5b40a68588e4c28f99ae9569e78620337f3 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 10 Dec 2025 20:44:39 -0500 Subject: [PATCH 04/30] jiff-tzdb-0.1.5 --- crates/jiff-tzdb/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 34359896a4e2df02269256ccd255a1bb89093068 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 12 Dec 2025 17:35:45 -0500 Subject: [PATCH 05/30] fuzz: update dependencies --- fuzz/Cargo.lock | 173 +++++++++++++++++++++++------------------------- 1 file changed, 81 insertions(+), 92 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index f2c0ea0..5e5046c 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -4,34 +4,60 @@ version = 4 [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "cc" -version = "1.1.7" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ + "find-msvc-tools", "jobserver", "libc", + "shlex", ] [[package]] -name = "derive_arbitrary" -version = "1.3.2" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +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" @@ -64,7 +90,7 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.4" +version = "0.1.5" [[package]] name = "jiff-tzdb-platform" @@ -75,41 +101,35 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom", "libc", ] [[package]] name = "libc" -version = "0.2.155" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "portable-atomic" @@ -144,6 +164,12 @@ 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" @@ -165,10 +191,16 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.109" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +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", @@ -177,79 +209,36 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +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.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" From aa361bbfc2c330deb548ff903b61c8e1abcc4887 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 9 Nov 2025 07:58:44 -0500 Subject: [PATCH 06/30] fmt: un-generic a function This reduces code size somewhat. There are probably more things like this, but I picked this one to test its effects. It got rid of a few LLVM lines from an LTO-compiled binary that uses Jiff. But not much. --- src/fmt/friendly/printer.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/fmt/friendly/printer.rs b/src/fmt/friendly/printer.rs index b3d5bd6..f61bf4c 100644 --- a/src/fmt/friendly/printer.rs +++ b/src/fmt/friendly/printer.rs @@ -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())?; @@ -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); @@ -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(()); } From 54c1b2d25b8b35929140b3d929f85bc26fa72429 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 14 Nov 2025 16:28:06 -0500 Subject: [PATCH 07/30] error: make `map_err` closures as `cold` and non-inlineable This doesn't seem to have much impact as shown via a `cargo llvm-lines`. But this still seems like good sense to have. --- src/error.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index 0675547..66e9497 100644 --- a/src/error.rs +++ b/src/error.rs @@ -647,7 +647,11 @@ impl ErrorContext for Error { impl ErrorContext for Result { #[cfg_attr(feature = "perf-inline", inline(always))] fn context(self, consequent: impl IntoError) -> Result { - self.map_err(|err| err.context_impl(consequent.into_error())) + self.map_err( + #[cold] + #[inline(never)] + |err| err.context_impl(consequent.into_error()), + ) } #[cfg_attr(feature = "perf-inline", inline(always))] @@ -655,7 +659,11 @@ impl ErrorContext for Result { self, consequent: impl FnOnce() -> E, ) -> Result { - self.map_err(|err| err.context_impl(consequent().into_error())) + self.map_err( + #[cold] + #[inline(never)] + |err| err.context_impl(consequent().into_error()), + ) } } From 37803515434b2ad5f1e3c78163601d44a420949d Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 16 Nov 2025 13:38:10 -0500 Subject: [PATCH 08/30] rangeint: don't try to inline error constructor --- src/util/rangeint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/rangeint.rs b/src/util/rangeint.rs index a20ec34..c1a811c 100644 --- a/src/util/rangeint.rs +++ b/src/util/rangeint.rs @@ -379,7 +379,7 @@ macro_rules! define_ranged { /// dependent bounds. For example, when the day of the month is out /// of bounds. The maximum value can vary based on the month (and /// year). - #[inline] + #[inline(never)] pub(crate) fn to_error_with_bounds( self, what: &'static str, From 40941a5188302db120a9b914e9ae73e6121efa2a Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 16 Nov 2025 13:39:33 -0500 Subject: [PATCH 09/30] fmt/strtime: tighten up `to_timestamp` and `to_date` Both of these functions are marked as `inline`, but they can be quite beefy. Instead, set the inlineable implementation as the common case, and put the rest behind a non-inlineable function. --- src/fmt/strtime/mod.rs | 113 ++++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/src/fmt/strtime/mod.rs b/src/fmt/strtime/mod.rs index 9455dd0..32d3fda 100644 --- a/src/fmt/strtime/mod.rs +++ b/src/fmt/strtime/mod.rs @@ -1584,21 +1584,27 @@ impl BrokenDownTime { /// ``` #[inline] pub fn to_timestamp(&self) -> Result { + #[cold] + #[inline(never)] + fn fallback(tm: &BrokenDownTime) -> Result { + let dt = tm + .to_datetime() + .context("datetime required to parse timestamp")?; + let offset = tm + .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", + ) + }) + } 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", - ) - }) + fallback(self) } #[inline] @@ -1681,45 +1687,60 @@ 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(err!("missing year, date cannot be created")); + }; + 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 let Some(weekday) = self.weekday { - if weekday != date.weekday() { + 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(err!( - "parsed weekday {weekday} does not match \ - weekday {got} from parsed date {date}", - weekday = weekday_name_full(weekday), - got = weekday_name_full(date.weekday()), + "a month/day, day-of-year or week date must be \ + present to create a date, but none were found", )); + }; + if let Some(weekday) = tm.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()), + )); + } } + Ok(date) } - 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); + }; + Ok(Date::new_ranged(year, month, day).context("invalid date")?) } #[inline] From 42080c4e71fa25a16a64c5ec460f86ff777922d6 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 16 Nov 2025 13:40:22 -0500 Subject: [PATCH 10/30] fmt/strtime: avoid inlining the `or_else` cases These all generally call `BrokenDownTime::to_date`, which adds a fair bit of code. Since these are the uncommon case, let's not inline that goop. --- src/fmt/strtime/format.rs | 126 ++++++++++++++++++++++++++++++-------- 1 file changed, 99 insertions(+), 27 deletions(-) diff --git a/src/fmt/strtime/format.rs b/src/fmt/strtime/format.rs index 5d5d9a5..e5e8064 100644 --- a/src/fmt/strtime/format.rs +++ b/src/fmt/strtime/format.rs @@ -339,7 +339,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_ranged()), + ) .ok_or_else(|| err!("requires date to format day"))? .get(); ext.write_int(b'0', Some(2), day, self.wtr) @@ -350,7 +353,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_ranged()), + ) .ok_or_else(|| err!("requires date to format day"))? .get(); ext.write_int(b' ', Some(2), day, self.wtr) @@ -431,7 +437,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) .ok_or_else(|| err!("requires date to format month"))? .get(); ext.write_int(b'0', Some(2), month, self.wtr) @@ -442,7 +451,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) .ok_or_else(|| err!("requires date to format month"))?; ext.write_str(Case::AsIs, month_name_full(month), self.wtr) } @@ -452,7 +464,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) .ok_or_else(|| err!("requires date to format month"))?; ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr) } @@ -613,7 +628,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) .ok_or_else(|| err!("requires date to format weekday"))?; ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr) } @@ -623,7 +641,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) .ok_or_else(|| err!("requires date to format weekday"))?; ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr) } @@ -633,7 +654,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) .ok_or_else(|| err!("requires date to format weekday number"))?; ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr) } @@ -643,7 +667,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) .ok_or_else(|| err!("requires date to format weekday number"))?; ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr) } @@ -658,14 +685,20 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_of_year()), + ) .ok_or_else(|| { err!("requires date to format Sunday-based week number") })?; let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) .ok_or_else(|| { err!("requires date to format Sunday-based week number") })? @@ -684,9 +717,15 @@ 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()) - }) + .or_else( + #[inline(never)] + || { + 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") })?; @@ -703,14 +742,20 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_of_year()), + ) .ok_or_else(|| { err!("requires date to format Monday-based week number") })?; let weekday = self .tm .weekday - .or_else(|| self.tm.to_date().ok().map(|d| d.weekday())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.weekday()), + ) .ok_or_else(|| { err!("requires date to format Monday-based week number") })? @@ -729,7 +774,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.year_ranged()), + ) .ok_or_else(|| err!("requires date to format year"))? .get(); ext.write_int(b'0', Some(4), year, self.wtr) @@ -740,7 +788,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.year_ranged()), + ) .ok_or_else(|| err!("requires date to format year (2-digit)"))? .get(); let year = year % 100; @@ -752,7 +803,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.year_ranged()), + ) .ok_or_else(|| err!("requires date to format century (2-digit)"))? .get(); let century = year / 100; @@ -764,9 +818,15 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .iso_week_year - .or_else(|| { - self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged()) - }) + .or_else( + #[inline(never)] + || { + 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") })? @@ -779,9 +839,15 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { let year = self .tm .iso_week_year - .or_else(|| { - self.tm.to_date().ok().map(|d| d.iso_week_date().year_ranged()) - }) + .or_else( + #[inline(never)] + || { + self.tm + .to_date() + .ok() + .map(|d| d.iso_week_date().year_ranged()) + }, + ) .ok_or_else(|| { err!( "requires date to format \ @@ -798,7 +864,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.month_ranged()), + ) .ok_or_else(|| err!("requires date to format quarter"))? .get(); let quarter = match month { @@ -817,7 +886,10 @@ 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())) + .or_else( + #[inline(never)] + || self.tm.to_date().ok().map(|d| d.day_of_year()), + ) .ok_or_else(|| err!("requires date to format day of year"))?; ext.write_int(b'0', Some(3), day, self.wtr) } From 3765a52b8d1e45a3cee182babc32785d4ed9d39c Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 16 Nov 2025 13:41:20 -0500 Subject: [PATCH 11/30] fmt: add `&mut dyn Write` impl for `jiff::fmt::Write` I was playing with using this to avoid parametric polymorphism costs, but I couldn't find my way to a concrete benefit. Either way, this impl should exist for people that want to use it. --- src/fmt/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index b5886fa..e7d22b4 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -334,6 +334,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 From b8757deba8ac5afe2c98859cc9ba7a93a2d6d66e Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 14 Nov 2025 17:16:45 -0500 Subject: [PATCH 12/30] error: switch everything over to structured errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was an incredibly tedious and tortuous refactor. But this removes almost all of the "create ad hoc stringly-typed errors everywhere." This partially makes progress toward #418, but my initial impetus for doing this was to see if I could reduce binary size and improve compilation times. My general target was to see if I could reduce total LLVM lines. I tested this with [Biff] using this command in the root of the Biff repo: ``` cargo llvm-lines --profile release-lto ``` Before this change, Biff had 768,596 LLVM lines. With this change, it has 757,331 lines. So... an improvement, but a very modest one. What about compilation times? This does seem to translate to---also a modest---improvement. For compiling release builds of Biff. Before: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b -r' Benchmark 1: cargo b -r Time (mean ± σ): 7.776 s ± 0.052 s [User: 65.876 s, System: 2.621 s] Range (min … max): 7.690 s … 7.862 s 10 runs ``` After: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b -r' Benchmark 1: cargo b -r Time (mean ± σ): 7.591 s ± 0.067 s [User: 65.686 s, System: 2.564 s] Range (min … max): 7.504 s … 7.689 s 10 runs ``` What about dev builds? Before: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b' Benchmark 1: cargo b Time (mean ± σ): 4.074 s ± 0.022 s [User: 14.493 s, System: 1.818 s] Range (min … max): 4.037 s … 4.099 s 10 runs ``` After: ``` $ hyperfine -w1 --prepare 'cargo clean' 'cargo b' Benchmark 1: cargo b Time (mean ± σ): 4.541 s ± 0.027 s [User: 15.385 s, System: 2.081 s] Range (min … max): 4.503 s … 4.591 s 10 runs ``` Well... that's disappointing. A modest improvement to release builds, but a fairly large regression in dev builds. Maybe it's because of the additional hand-written impls for new structured error types? Bah. And binary size? Normal release builds (not LTO) of Biff that were stripped were 4,431,456 bytes before this change and 4,392,064 after. Hopefully this will unlock other improvements to justify doing this. Note also that this slims down a number of error messages. [Biff]: https://github.com/BurntSushi/biff --- crates/jiff-static/src/shared/util/escape.rs | 38 +- crates/jiff-static/src/shared/util/utf8.rs | 64 +- src/civil/date.rs | 22 +- src/civil/datetime.rs | 70 +- src/civil/iso_week_date.rs | 6 +- src/civil/time.rs | 36 +- src/duration.rs | 15 +- src/error/civil.rs | 86 ++ src/error/duration.rs | 36 + src/error/fmt/friendly.rs | 82 ++ src/error/fmt/mod.rs | 87 ++ src/error/fmt/offset.rs | 153 +++ src/error/fmt/rfc2822.rs | 231 +++++ src/error/fmt/rfc9557.rs | 114 +++ src/error/fmt/strtime.rs | 517 ++++++++++ src/error/fmt/temporal.rs | 354 +++++++ src/error/fmt/util.rs | 115 +++ src/{error.rs => error/mod.rs} | 336 ++++--- src/error/signed_duration.rs | 55 ++ src/error/span.rs | 139 +++ src/error/timestamp.rs | 38 + src/error/tz/ambiguous.rs | 49 + src/error/tz/concatenated.rs | 119 +++ src/error/tz/db.rs | 141 +++ src/error/tz/mod.rs | 8 + src/error/tz/offset.rs | 110 +++ src/error/tz/posix.rs | 35 + src/error/tz/system.rs | 104 ++ src/error/tz/timezone.rs | 43 + src/error/tz/zic.rs | 323 +++++++ src/error/util.rs | 194 ++++ src/error/zoned.rs | 71 ++ src/fmt/friendly/mod.rs | 8 +- src/fmt/friendly/parser.rs | 288 +++--- src/fmt/mod.rs | 20 +- src/fmt/offset.rs | 239 ++--- src/fmt/rfc2822.rs | 340 ++----- src/fmt/rfc9557.rs | 344 +++---- src/fmt/serde.rs | 28 +- src/fmt/strtime/format.rs | 333 +++---- src/fmt/strtime/mod.rs | 247 ++--- src/fmt/strtime/parse.rs | 590 +++++------- src/fmt/temporal/mod.rs | 33 +- src/fmt/temporal/parser.rs | 910 +++++++----------- src/fmt/temporal/pieces.rs | 5 +- src/fmt/temporal/printer.rs | 10 +- src/fmt/util.rs | 265 ++--- src/logging.rs | 4 +- src/now.rs | 15 +- src/shared/util/escape.rs | 38 +- src/shared/util/utf8.rs | 64 +- src/signed_duration.rs | 184 ++-- src/span.rs | 353 ++----- src/timestamp.rs | 32 +- src/tz/ambiguous.rs | 58 +- src/tz/concatenated.rs | 106 +- src/tz/db/bundled/disabled.rs | 2 +- src/tz/db/bundled/enabled.rs | 2 +- src/tz/db/concatenated/disabled.rs | 11 +- src/tz/db/concatenated/enabled.rs | 16 +- src/tz/db/mod.rs | 24 +- src/tz/db/zoneinfo/disabled.rs | 11 +- src/tz/db/zoneinfo/enabled.rs | 16 +- src/tz/offset.rs | 110 +-- src/tz/posix.rs | 28 +- src/tz/system/mod.rs | 64 +- src/tz/system/windows/mod.rs | 29 +- src/tz/timezone.rs | 29 +- src/tz/zic.rs | 320 +++--- src/util/escape.rs | 2 +- src/util/parse.rs | 122 +-- src/util/rangeint.rs | 38 +- src/util/round/increment.rs | 52 +- src/util/utf8.rs | 19 - src/zoned.rs | 121 +-- .../invalid-tz-environment-variable/main.rs | 12 +- tests/tc39_262/span/add.rs | 2 +- tests/tc39_262/span/round.rs | 36 +- tests/tc39_262/span/total.rs | 4 +- 79 files changed, 5517 insertions(+), 3858 deletions(-) create mode 100644 src/error/civil.rs create mode 100644 src/error/duration.rs create mode 100644 src/error/fmt/friendly.rs create mode 100644 src/error/fmt/mod.rs create mode 100644 src/error/fmt/offset.rs create mode 100644 src/error/fmt/rfc2822.rs create mode 100644 src/error/fmt/rfc9557.rs create mode 100644 src/error/fmt/strtime.rs create mode 100644 src/error/fmt/temporal.rs create mode 100644 src/error/fmt/util.rs rename src/{error.rs => error/mod.rs} (76%) create mode 100644 src/error/signed_duration.rs create mode 100644 src/error/span.rs create mode 100644 src/error/timestamp.rs create mode 100644 src/error/tz/ambiguous.rs create mode 100644 src/error/tz/concatenated.rs create mode 100644 src/error/tz/db.rs create mode 100644 src/error/tz/mod.rs create mode 100644 src/error/tz/offset.rs create mode 100644 src/error/tz/posix.rs create mode 100644 src/error/tz/system.rs create mode 100644 src/error/tz/timezone.rs create mode 100644 src/error/tz/zic.rs create mode 100644 src/error/util.rs create mode 100644 src/error/zoned.rs diff --git a/crates/jiff-static/src/shared/util/escape.rs b/crates/jiff-static/src/shared/util/escape.rs index 5ed8cd1..5e3b8b3 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,37 @@ 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)?; + write!(f, "\"")?; + 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)?; diff --git a/crates/jiff-static/src/shared/util/utf8.rs b/crates/jiff-static/src/shared/util/utf8.rs index 08d0c7a..585c0e7 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,22 +69,20 @@ /// *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 diff --git a/src/civil/date.rs b/src/civil/date.rs index c4e113b..2728883 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}, @@ -1057,7 +1057,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::from(E::NthWeekdayNonZero)) } else if nth > C(0) { let nth = nth.max(C(1)); let weekday_diff = weekday.since_ranged(self.weekday().next()); @@ -1515,14 +1515,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 +2935,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)); 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 2bcdd3f..bea792a 100644 --- a/src/civil/iso_week_date.rs +++ b/src/civil/iso_week_date.rs @@ -1,6 +1,6 @@ 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, @@ -711,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 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..4f548b8 --- /dev/null +++ b/src/error/civil.rs @@ -0,0 +1,86 @@ +use crate::{error, Unit}; + +#[derive(Clone, Debug)] +pub(crate) enum Error { + FailedAddDays, + FailedAddDurationOverflowing, + FailedAddSpanDate, + FailedAddSpanOverflowing, + FailedAddSpanTime, + IllegalTimeWithMicrosecond, + IllegalTimeWithMillisecond, + IllegalTimeWithNanosecond, + InvalidISOWeekNumber, + OverflowDaysDuration, + OverflowTimeNanoseconds, + NthWeekdayNonZero, + 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") + } + NthWeekdayNonZero => f.write_str("nth weekday cannot be `0`"), + 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..459133f --- /dev/null +++ b/src/error/fmt/offset.rs @@ -0,0 +1,153 @@ +use crate::{error, shared::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 76% rename from src/error.rs rename to src/error/mod.rs index 66e9497..7aad0ca 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}; -/// 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. /// @@ -65,50 +63,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`. /// @@ -132,6 +86,11 @@ impl Error { Error::from(ErrorKind::Adhoc(AdhocError::from_args(message))) } + #[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 { @@ -139,7 +98,7 @@ impl Error { { let mut err = consequent; if err.inner.is_none() { - err = err!("unknown jiff error"); + err = Error::from(ErrorKind::Unknown); } let inner = err.inner.as_mut().unwrap(); assert!( @@ -160,51 +119,6 @@ impl Error { } 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,6 +134,18 @@ 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)) @@ -328,14 +254,90 @@ 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), + 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), + OsStrUtf8(self::util::OsStrUtf8Error), + ParseInt(self::util::ParseIntError), + ParseFraction(self::util::ParseFractionError), + Range(RangeError), + RoundingIncrement(self::util::RoundingIncrementError), + Shared(SharedError), + 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), + 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), + 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), + OsStrUtf8(ref err) => err.fmt(f), + ParseInt(ref err) => err.fmt(f), + ParseFraction(ref err) => err.fmt(f), + Range(ref err) => err.fmt(f), + RoundingIncrement(ref err) => err.fmt(f), + Shared(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), + Unknown => f.write_str("unknown jiff error"), + Zoned(ref err) => err.fmt(f), } } } @@ -368,18 +370,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 +384,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 +462,32 @@ 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") + } +} + /// A `std::io::Error`. /// /// This type is itself always available, even when the `std` feature is not @@ -581,21 +593,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 +600,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 +609,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,47 +620,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( - #[cold] - #[inline(never)] - |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( - #[cold] - #[inline(never)] - |err| err.context_impl(consequent().into_error()), - ) + self.map_err(|err| { + err.into_error().context_impl(consequent().into_error()) + }) } } @@ -698,7 +679,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..e87dea2 --- /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: \ + Jiff crate feature `tzdb-concatenated` is disabled, \ + opening tzdb at given path has therefore failed", + ), + #[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))] + DisabledZoneInfo => f.write_str( + "system zoneinfo tzdb unavailable: \ + Jiff crate feature `tzdb-zoneinfo` is disabled, \ + opening tzdb at given path has therefore failed", + ), + 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..e649032 --- /dev/null +++ b/src/error/tz/timezone.rs @@ -0,0 +1,43 @@ +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 since Jiff's \ + `tz-system` crate feature is not enabled", + ), + } + } +} 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/mod.rs b/src/fmt/mod.rs index e7d22b4..e32b724 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -166,7 +166,7 @@ and features.) */ use crate::{ - error::{err, Error}, + error::{fmt::Error as E, Error}, util::escape, }; @@ -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))) } } @@ -379,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) } } @@ -422,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)) } } diff --git a/src/fmt/offset.rs b/src/fmt/offset.rs index f56e48a..b73e11f 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)) @@ -413,18 +407,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 +454,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 +487,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 +513,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 +560,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 +576,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 +594,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 +607,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 +852,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 +861,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 +870,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 +879,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 +888,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 +897,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 +906,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 +915,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 +924,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 +933,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 +942,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 +952,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 +987,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 +997,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 +1029,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 +1054,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..36dd7f0 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}, + error::{fmt::rfc2822::Error as E, ErrorContext}, fmt::{util::DecimalFormatter, 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 }) @@ -1423,10 +1301,7 @@ impl DateTimePrinter { // 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()))?; @@ -1484,10 +1359,7 @@ impl DateTimePrinter { // 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 e5e8064..65de289 100644 --- a/src/fmt/strtime/format.rs +++ b/src/fmt/strtime/format.rs @@ -1,5 +1,8 @@ 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, @@ -9,8 +12,8 @@ use crate::{ util::{DecimalFormatter, FractionalFormatter}, Write, WriteExt, }, + shared::util::utf8, tz::Offset, - util::{escape, 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 { @@ -343,7 +323,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.day_ranged()), ) - .ok_or_else(|| err!("requires date to format day"))? + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(2), day, self.wtr) } @@ -357,18 +337,14 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.day_ranged()), ) - .ok_or_else(|| err!("requires date to format day"))? + .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 { @@ -379,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 { @@ -404,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) } @@ -424,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) } @@ -441,7 +401,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.month_ranged()), ) - .ok_or_else(|| err!("requires date to format month"))? + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(2), month, self.wtr) } @@ -455,7 +415,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.month_ranged()), ) - .ok_or_else(|| err!("requires date to format month"))?; + .ok_or(FE::RequiresDate)?; ext.write_str(Case::AsIs, month_name_full(month), self.wtr) } @@ -468,19 +428,14 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.month_ranged()), ) - .ok_or_else(|| err!("requires date to format month"))?; + .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)?; @@ -490,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)?; @@ -504,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 @@ -568,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")?; @@ -592,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 @@ -611,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) } @@ -632,7 +556,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.weekday()), ) - .ok_or_else(|| err!("requires date to format weekday"))?; + .ok_or(FE::RequiresDate)?; ext.write_str(Case::AsIs, weekday_name_full(weekday), self.wtr) } @@ -645,7 +569,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.weekday()), ) - .ok_or_else(|| err!("requires date to format weekday"))?; + .ok_or(FE::RequiresDate)?; ext.write_str(Case::AsIs, weekday_name_abbrev(weekday), self.wtr) } @@ -658,7 +582,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.weekday()), ) - .ok_or_else(|| err!("requires date to format weekday number"))?; + .ok_or(FE::RequiresDate)?; ext.write_int(b' ', None, weekday.to_monday_one_offset(), self.wtr) } @@ -671,7 +595,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.weekday()), ) - .ok_or_else(|| err!("requires date to format weekday number"))?; + .ok_or(FE::RequiresDate)?; ext.write_int(b' ', None, weekday.to_sunday_zero_offset(), self.wtr) } @@ -689,9 +613,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.day_of_year()), ) - .ok_or_else(|| { - err!("requires date to format Sunday-based week number") - })?; + .ok_or(FE::RequiresDate)?; let weekday = self .tm .weekday @@ -699,9 +621,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.weekday()), ) - .ok_or_else(|| { - err!("requires date to format Sunday-based week number") - })? + .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. @@ -726,9 +646,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { .map(|d| d.iso_week_date().week_ranged()) }, ) - .ok_or_else(|| { - err!("requires date to format ISO 8601 week number") - })?; + .ok_or(FE::RequiresDate)?; ext.write_int(b'0', Some(2), weeknum, self.wtr) } @@ -746,9 +664,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.day_of_year()), ) - .ok_or_else(|| { - err!("requires date to format Monday-based week number") - })?; + .ok_or(FE::RequiresDate)?; let weekday = self .tm .weekday @@ -756,9 +672,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.weekday()), ) - .ok_or_else(|| { - err!("requires date to format Monday-based week number") - })? + .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. @@ -778,7 +692,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.year_ranged()), ) - .ok_or_else(|| err!("requires date to format year"))? + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(4), year, self.wtr) } @@ -792,7 +706,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.year_ranged()), ) - .ok_or_else(|| err!("requires date to format year (2-digit)"))? + .ok_or(FE::RequiresDate)? .get(); let year = year % 100; ext.write_int(b'0', Some(2), year, self.wtr) @@ -807,7 +721,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.year_ranged()), ) - .ok_or_else(|| err!("requires date to format century (2-digit)"))? + .ok_or(FE::RequiresDate)? .get(); let century = year / 100; ext.write_int(b' ', None, century, self.wtr) @@ -827,9 +741,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { .map(|d| d.iso_week_date().year_ranged()) }, ) - .ok_or_else(|| { - err!("requires date to format ISO 8601 week-based year") - })? + .ok_or(FE::RequiresDate)? .get(); ext.write_int(b'0', Some(4), year, self.wtr) } @@ -848,12 +760,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { .map(|d| d.iso_week_date().year_ranged()) }, ) - .ok_or_else(|| { - err!( - "requires date to format \ - ISO 8601 week-based year (2-digit)" - ) - })? + .ok_or(FE::RequiresDate)? .get(); let year = year % 100; ext.write_int(b'0', Some(2), year, self.wtr) @@ -868,7 +775,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.month_ranged()), ) - .ok_or_else(|| err!("requires date to format quarter"))? + .ok_or(FE::RequiresDate)? .get(); let quarter = match month { 1..=3 => 1, @@ -890,7 +797,7 @@ impl<'c, 'f, 't, 'w, W: Write, L: Custom> Formatter<'c, 'f, 't, 'w, W, L> { #[inline(never)] || self.tm.to_date().ok().map(|d| d.day_of_year()), ) - .ok_or_else(|| err!("requires date to format day of year"))?; + .ok_or(FE::RequiresDate)?; ext.write_int(b'0', Some(3), day, self.wtr) } @@ -1580,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)", ); } @@ -1593,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 32d3fda..8c1915e 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 @@ -1587,19 +1574,10 @@ impl BrokenDownTime { #[cold] #[inline(never)] fn fallback(tm: &BrokenDownTime) -> Result { - let dt = tm - .to_datetime() - .context("datetime required to parse timestamp")?; - let offset = tm - .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", - ) - }) + let dt = + tm.to_datetime().context(E::RequiredDateTimeForTimestamp)?; + let offset = tm.offset.ok_or(E::RequiredOffsetForTimestamp)?; + offset.to_timestamp(dt).context(E::RangeTimestamp) } if let Some(timestamp) = self.timestamp() { return Ok(timestamp); @@ -1607,16 +1585,6 @@ impl BrokenDownTime { fallback(self) } - #[inline] - fn to_offset(&self) -> Result { - let Some(offset) = self.offset else { - return Err(err!( - "parsing format did not include time zone offset directive", - )); - }; - Ok(offset) - } - /// Extracts a civil datetime from this broken down time. /// /// # Errors @@ -1644,10 +1612,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)) } @@ -1698,7 +1664,7 @@ impl BrokenDownTime { if let Some(date) = tm.to_date_from_iso()? { return Ok(date); } - return Err(err!("missing year, date cannot be created")); + return Err(Error::from(E::RequiredYearForDate)); }; let mut date = tm.to_date_from_gregorian(year)?; if date.is_none() { @@ -1714,19 +1680,14 @@ impl BrokenDownTime { date = tm.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", - )); + return Err(Error::from(E::RequiredSomeDayForDate)); }; if let Some(weekday) = tm.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) @@ -1735,12 +1696,12 @@ impl BrokenDownTime { // 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) + let (Some(year), Some(month), Some(day), None) = + (self.year, self.month, self.day, self.weekday) else { return to_date(self); }; - Ok(Date::new_ranged(year, month, day).context("invalid date")?) + Ok(Date::new_ranged(year, month, day).context(E::InvalidDate)?) } #[inline] @@ -1751,7 +1712,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] @@ -1767,7 +1728,7 @@ impl BrokenDownTime { .with() .day_of_year(doy.get()) .build() - .context("invalid date")? + .context(E::InvalidDate)? })) } @@ -1778,8 +1739,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())) } @@ -1794,28 +1755,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 { @@ -1827,7 +1780,7 @@ impl BrokenDownTime { .with() .day_of_year(doy) .build() - .context("invalid date")?; + .context(E::InvalidDate)?; Ok(Some(date)) } @@ -1842,28 +1795,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 { @@ -1875,7 +1820,7 @@ impl BrokenDownTime { .with() .day_of_year(doy) .build() - .context("invalid date")?; + .context(E::InvalidDate)?; Ok(Some(date)) } @@ -1957,52 +1902,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))); }; @@ -3500,7 +3421,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, @@ -3509,15 +3430,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 @@ -3541,16 +3459,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)) } @@ -3570,10 +3482,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 52fd799..0e0cf43 100644 --- a/src/fmt/temporal/mod.rs +++ b/src/fmt/temporal/mod.rs @@ -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>(()) @@ -2490,7 +2487,7 @@ mod tests { ); insta::assert_snapshot!( DateTimeParser::new().parse_date("-000000-01-01").unwrap_err(), - @"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 5444804..6a6083d 100644 --- a/src/fmt/temporal/parser.rs +++ b/src/fmt/temporal/parser.rs @@ -1,6 +1,6 @@ use crate::{ civil::{Date, DateTime, ISOWeekDate, Time, Weekday}, - error::{err, Error, ErrorContext}, + 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")] { @@ -569,50 +494,36 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - 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 'W' prefix before week num. - let Parsed { input, .. } = - self.parse_week_prefix(input).with_context(|| { - err!( - "failed to parse week number prefix \ - in date `{original}`" - ) - })?; + 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).with_context(|| { - err!("failed to parse week number in date `{original}`") - })?; + self.parse_week_num(input).context(E::FailedWeekNumberInDate)?; // Parse optional separator. let Parsed { input, .. } = self .parse_date_separator(input, extended) - .context("failed to parse separator after week number")?; + .context(E::FailedSeparatorAfterWeekNumber)?; // Parse day component. let Parsed { value: weekday, input } = - self.parse_weekday(input).with_context(|| { - err!("failed to parse weekday in date `{original}`") - })?; + self.parse_weekday(input).context(E::FailedWeekdayInDate)?; let iso_week_date = ISOWeekDate::new_ranged(year, week, weekday) - .with_context(|| { - err!("week date parsed from `{original}` is not valid") - })?; + .context(E::InvalidWeekDate)?; Ok(Parsed { value: iso_week_date, input: input }) } @@ -626,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 }) } @@ -676,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. @@ -703,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 } = @@ -725,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, @@ -773,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. @@ -816,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. @@ -862,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 ::: @@ -918,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 }) } @@ -943,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 }) } @@ -971,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 }) } @@ -1000,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 }) } @@ -1030,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 }) } @@ -1065,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 }); } @@ -1080,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 }) } @@ -1126,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 } } @@ -1152,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'+' { @@ -1164,8 +989,7 @@ 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 @@ -1173,18 +997,15 @@ impl DateTimeParser { #[cfg_attr(feature = "perf-inline", inline(always))] fn parse_week_prefix<'i>( &self, - mut input: &'i [u8], + input: &'i [u8], ) -> Result, Error> { - if input.is_empty() { - return Err(err!("expected `W` or `w`, but found end of input")); + 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, + })); } - if !matches!(input[0], b'W' | b'w') { - return Err(err!( - "expected `W` or `w`, but found `{found}` instead", - found = escape::Byte(input[0]), - )); - } - input = &input[1..]; Ok(Parsed { value: (), input }) } @@ -1194,21 +1015,12 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (week_num, input) = parse::split(input, 2).ok_or_else(|| { - err!("expected two digit week number, but found end of input") - })?; - let week_num = parse::i64(week_num).with_context(|| { - err!( - "failed to parse `{week_num}` as week number, \ - expected a two digit integer", - week_num = escape::Bytes(week_num), - ) - })?; + 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) - .with_context(|| { - err!("parsed week number `{week_num}` is not valid") - })?; - + .context(E::InvalidWeekNumber)?; Ok(Parsed { value: week_num, input }) } @@ -1219,21 +1031,12 @@ impl DateTimeParser { &self, input: &'i [u8], ) -> Result, Error> { - let (weekday, input) = parse::split(input, 1).ok_or_else(|| { - err!("expected one digit weekday, but found end of input") - })?; - let weekday = parse::i64(weekday).with_context(|| { - err!( - "failed to parse `{weekday}` as weekday (a one digit integer)", - weekday = escape::Bytes(weekday), - ) - })?; + 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) - .with_context(|| { - err!("parsed weekday `{weekday}` is not valid") - })?; + .context(E::InvalidWeekday)?; let weekday = Weekday::from_monday_one_offset_ranged(weekday); - Ok(Parsed { value: weekday, input }) } } @@ -1265,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))] @@ -1287,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))] @@ -1314,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))] @@ -1330,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) @@ -1338,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); @@ -1348,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); @@ -1365,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) @@ -1373,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 }) @@ -1466,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))] @@ -1493,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 @@ -1521,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 ::: @@ -1556,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 } + } } } @@ -1608,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", ); } @@ -1705,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", ); } @@ -1837,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", @@ -1848,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]", @@ -1866,7 +1617,6 @@ mod tests { time: None, offset: None, annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1877,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", @@ -1895,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", @@ -1925,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]", @@ -1955,7 +1703,6 @@ mod tests { }, ), annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1966,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]", @@ -1988,7 +1735,6 @@ mod tests { }, ), annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -1999,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]", @@ -2023,7 +1769,6 @@ mod tests { }, ), annotations: ParsedAnnotations { - input: "[America/New_York]", time_zone: Some( Named { critical: false, @@ -2034,7 +1779,7 @@ mod tests { }, input: "", } - "###); + "#); } #[test] @@ -2043,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", @@ -2060,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", @@ -2084,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", @@ -2108,13 +1851,12 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { - input: "", time_zone: None, }, }, input: "", } - "###); + "#); } #[test] @@ -2123,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", @@ -2140,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", @@ -2164,13 +1905,12 @@ mod tests { ), offset: None, annotations: ParsedAnnotations { - input: "", time_zone: None, }, }, input: "", } - "###); + "#); } #[test] @@ -2317,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", ); } @@ -2333,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]"), - @"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", ); } @@ -2359,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", ); } @@ -2430,7 +2170,7 @@ mod tests { fn err_date_empty() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"").unwrap_err(), - @"failed to parse year in date ``: expected four digit year (or leading sign for six digit year), but found end of input", + @"failed to parse year in date: expected four digit year (or leading sign for six digit year), but found end of input", ); } @@ -2438,40 +2178,40 @@ mod tests { fn err_date_year() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"123").unwrap_err(), - @"failed to parse year in date `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(), - @"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(), - @"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(), - @"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(), - @"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(), - @"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(), - @"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", ); } @@ -2479,19 +2219,19 @@ mod tests { fn err_date_month() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-").unwrap_err(), - @"failed to parse month in date `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(), - @"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(), - @"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(), - @"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", ); } @@ -2499,27 +2239,27 @@ mod tests { fn err_date_day() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-12-").unwrap_err(), - @"failed to parse day in date `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(), - @"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(), - @"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(), - @"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(), - @"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(), - @"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", ); } @@ -2527,11 +2267,11 @@ mod tests { fn err_date_separator() { insta::assert_snapshot!( DateTimeParser::new().parse_date_spec(b"2024-1231").unwrap_err(), - @"failed to parse separator after month: expected `-` separator, but found `3` 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", ); } @@ -2660,7 +2400,7 @@ mod tests { fn err_time_empty() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"").unwrap_err(), - @"failed to parse hour in time ``: expected two digit hour, but found end of input", + @"failed to parse hour in time: expected two digit hour, but found end of input", ); } @@ -2668,15 +2408,15 @@ mod tests { fn err_time_hour() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"a").unwrap_err(), - @"failed to parse hour in time `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(), - @"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", ); } @@ -2684,19 +2424,19 @@ mod tests { fn err_time_minute() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:").unwrap_err(), - @"failed to parse minute in time `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(), - @"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(), - @"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(), - @"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", ); } @@ -2704,19 +2444,19 @@ mod tests { fn err_time_second() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:").unwrap_err(), - @"failed to parse second in time `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(), - @"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(), - @"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(), - @"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", ); } @@ -2724,11 +2464,11 @@ mod tests { fn err_time_fractional() { insta::assert_snapshot!( DateTimeParser::new().parse_time_spec(b"01:02:03.").unwrap_err(), - @"failed to parse fractional 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(), - @"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", ); } @@ -2846,40 +2586,40 @@ mod tests { insta::assert_snapshot!( p("123"), - @"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!( p("123a"), - @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!( p("-9999"), - @"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!( p("+9999"), - @"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!( p("-99999"), - @"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!( p("+99999"), - @"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!( p("-99999a"), - @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!( p("+999999"), - @"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!( p("-010000"), - @"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", ); } @@ -2893,11 +2633,11 @@ mod tests { insta::assert_snapshot!( p("2024-"), - @"failed to parse week number prefix in date `2024-`: expected `W` or `w`, but found end of input", + @"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 `2024`: expected `W` or `w`, but found end of input", + @"failed to parse week number prefix in date: expected `W` or `w`, but found end of input", ); } @@ -2911,19 +2651,19 @@ mod tests { insta::assert_snapshot!( p("2024-W"), - @"failed to parse week number in date `2024-W`: expected two digit week number, but found end of input", + @"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 `2024-W1`: expected two digit week number, but found end of input", + @"failed to parse week number in date: expected two digit week number, but found end of input", ); insta::assert_snapshot!( p("2024-W53-1"), - @"week date parsed from `2024-W53-1` is not valid: ISO week number `53` is invalid for year `2024`", + @"parsed week date is not valid: ISO week number is invalid for given year", ); insta::assert_snapshot!( p("2030W531"), - @"week date parsed from `2030W531` is not valid: ISO week number `53` is invalid for year `2030`", + @"parsed week date is not valid: ISO week number is invalid for given year", ); } @@ -2937,11 +2677,11 @@ mod tests { insta::assert_snapshot!( p("2024-W53-1"), - @"week date parsed from `2024-W53-1` is not valid: ISO week number `53` is invalid for year `2024`", + @"parsed week date is not valid: ISO week number is invalid for given year", ); insta::assert_snapshot!( p("2025-W53-1"), - @"week date parsed from `2025-W53-1` is not valid: ISO week number `53` is invalid for year `2025`", + @"parsed week date is not valid: ISO week number is invalid for given year", ); } @@ -2954,15 +2694,15 @@ mod tests { }; insta::assert_snapshot!( p("2024-W12-"), - @"failed to parse weekday in date `2024-W12-`: expected one digit weekday, but found end of input", + @"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 `2024W12`: expected one digit weekday, but found end of input", + @"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 `2024-W11-8`: parsed weekday `8` is not valid: parameter 'weekday' with value 8 is not in the required range of 1..=7", + @"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", ); } @@ -2975,11 +2715,11 @@ mod tests { }; insta::assert_snapshot!( p("2024-W521"), - @"failed to parse separator after week number: expected `-` separator, but found `1` instead", + @"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 after month since none was found after the year, but found a `-` separator", + @"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 c65ad80..dbd98e9 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -1,6 +1,6 @@ use crate::{ civil::{Date, DateTime, ISOWeekDate, Time}, - error::{err, Error}, + error::{fmt::temporal::Error as E, Error}, fmt::{ temporal::{Pieces, PiecesOffset, TimeZoneAnnotationKind}, util::{DecimalFormatter, FractionalFormatter}, @@ -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( diff --git a/src/fmt/util.rs b/src/fmt/util.rs index 3e1773a..fe214de 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, }; @@ -462,14 +462,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 +499,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 +530,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 +629,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 +662,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 +672,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 +802,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 +810,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 +937,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 +950,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 +988,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 +1007,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 +1028,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 +1134,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 +1279,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 +1312,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 +1344,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 +1371,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 +1385,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) 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/util/escape.rs b/src/shared/util/escape.rs index 1593f90..dd0736d 100644 --- a/src/shared/util/escape.rs +++ b/src/shared/util/escape.rs @@ -15,6 +15,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, " "); @@ -35,6 +36,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)?; @@ -52,15 +54,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; } @@ -79,6 +82,37 @@ 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)?; + write!(f, "\"")?; + 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)?; diff --git a/src/shared/util/utf8.rs b/src/shared/util/utf8.rs index 1cccc47..33d920c 100644 --- a/src/shared/util/utf8.rs +++ b/src/shared/util/utf8.rs @@ -1,3 +1,57 @@ +/// 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 @@ -13,22 +67,20 @@ /// *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 diff --git a/src/signed_duration.rs b/src/signed_duration.rs index f70f1ab..c7c473a 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] @@ -2414,9 +2370,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 +2384,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 +2722,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 +2737,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 +2781,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 +2988,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 +3033,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..f79d74d 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 @@ -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..7592ea2 100644 --- a/src/tz/db/bundled/enabled.rs +++ b/src/tz/db/bundled/enabled.rs @@ -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..c1ae7c6 100644 --- a/src/tz/db/concatenated/disabled.rs +++ b/src/tz/db/concatenated/disabled.rs @@ -10,13 +10,10 @@ 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::tz::db::Error::DisabledConcatenated, )) } @@ -41,6 +38,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..8973cfe 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 diff --git a/src/tz/db/zoneinfo/disabled.rs b/src/tz/db/zoneinfo/disabled.rs index a9c79d4..f54d4e7 100644 --- a/src/tz/db/zoneinfo/disabled.rs +++ b/src/tz/db/zoneinfo/disabled.rs @@ -10,13 +10,10 @@ 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::tz::db::Error::DisabledZoneInfo, )) } @@ -41,6 +38,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..d48c6f1 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, @@ -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..9c28ff5 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] @@ -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..b682b89 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) + } } } } @@ -212,9 +211,7 @@ impl PosixTimeZone { 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))) - })?; + .context(E::InvalidPosixTz)?; Ok(PosixTimeZone { inner }) } @@ -228,12 +225,7 @@ impl PosixTimeZone { 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) - )) - })?; + .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..ae4cbf4 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,7 @@ 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(E::FailedSystem)) } #[cfg(feature = "tz-system")] { @@ -916,7 +913,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 +932,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 +1385,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 +1880,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), } } } 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/escape.rs b/src/util/escape.rs index 488785d..9deb81a 100644 --- a/src/util/escape.rs +++ b/src/util/escape.rs @@ -8,4 +8,4 @@ This was copied from `regex-automata` with a few light edits. // 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}; +pub(crate) use crate::shared::util::escape::{Byte, Bytes, RepeatByte}; 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 c1a811c..2884bb1 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] 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..dc62b6b 100644 --- a/src/util/utf8.rs +++ b/src/util/utf8.rs @@ -1,24 +1,5 @@ use core::cmp::Ordering; -/// 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> { - crate::shared::util::utf8::decode(bytes) -} - /// Like std's `eq_ignore_ascii_case`, but returns a full `Ordering`. #[inline] pub(crate) fn cmp_ignore_ascii_case(s1: &str, s2: &str) -> 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); From 3a832162bed7f1e170ac92e7770908db9d51a4f1 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 19 Dec 2025 13:11:17 -0500 Subject: [PATCH 13/30] fmt: some minor perf improvements to `strptime` This adds a little inlining back to help reclaim a small performance regression in `strptime`. This improves the relevant benchmark from 70ns to 64ns on my machine. --- src/fmt/strtime/mod.rs | 44 ++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/fmt/strtime/mod.rs b/src/fmt/strtime/mod.rs index 8c1915e..84e1c1c 100644 --- a/src/fmt/strtime/mod.rs +++ b/src/fmt/strtime/mod.rs @@ -1571,18 +1571,20 @@ impl BrokenDownTime { /// ``` #[inline] pub fn to_timestamp(&self) -> Result { - #[cold] - #[inline(never)] - fn fallback(tm: &BrokenDownTime) -> Result { - let dt = - tm.to_datetime().context(E::RequiredDateTimeForTimestamp)?; - let offset = tm.offset.ok_or(E::RequiredOffsetForTimestamp)?; - offset.to_timestamp(dt).context(E::RangeTimestamp) - } + // 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); } - fallback(self) + 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. @@ -1632,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, @@ -1696,12 +1702,22 @@ impl BrokenDownTime { // 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), None) = - (self.year, self.month, self.day, self.weekday) + let (Some(year), Some(month), Some(day)) = + (self.year, self.month, self.day) else { return to_date(self); }; - Ok(Date::new_ranged(year, month, day).context(E::InvalidDate)?) + let date = + Date::new_ranged(year, month, day).context(E::InvalidDate)?; + if let Some(weekday) = self.weekday { + if weekday != date.weekday() { + return Err(Error::from(E::MismatchWeekday { + parsed: weekday, + got: date.weekday(), + })); + } + } + Ok(date) } #[inline] From 973876a9f1188f7026ed40dc330bc97b03b6245d Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 19 Dec 2025 15:55:39 -0500 Subject: [PATCH 14/30] shared: use structured errors for `shared/itime` --- crates/jiff-cli/cmd/generate/shared.rs | 8 +- crates/jiff-static/src/shared/error/itime.rs | 100 +++++++++++++++++ crates/jiff-static/src/shared/error/mod.rs | 57 ++++++++++ crates/jiff-static/src/shared/mod.rs | 1 + crates/jiff-static/src/shared/util/itime.rs | 108 +++++-------------- src/civil/date.rs | 8 +- src/error/mod.rs | 14 ++- src/fmt/strtime/mod.rs | 4 +- src/shared/error/itime.rs | 98 +++++++++++++++++ src/shared/error/mod.rs | 56 ++++++++++ src/shared/mod.rs | 1 + src/shared/tzif.rs | 6 +- src/shared/util/itime.rs | 108 +++++-------------- 13 files changed, 394 insertions(+), 175 deletions(-) create mode 100644 crates/jiff-static/src/shared/error/itime.rs create mode 100644 crates/jiff-static/src/shared/error/mod.rs create mode 100644 src/shared/error/itime.rs create mode 100644 src/shared/error/mod.rs 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/mod.rs b/crates/jiff-static/src/shared/mod.rs index 067be4b..cfb528f 100644 --- a/crates/jiff-static/src/shared/mod.rs +++ b/crates/jiff-static/src/shared/mod.rs @@ -474,6 +474,7 @@ pub struct PosixOffset { // Does not require `alloc`, but is only used when `alloc` is enabled. pub(crate) mod crc32; +pub(crate) mod error; pub(crate) mod posix; pub(crate) mod tzif; pub(crate) mod util; diff --git a/crates/jiff-static/src/shared/util/itime.rs b/crates/jiff-static/src/shared/util/itime.rs index ba012c6..4e0a304 100644 --- a/crates/jiff-static/src/shared/util/itime.rs +++ b/crates/jiff-static/src/shared/util/itime.rs @@ -24,7 +24,7 @@ they are internal types. Specifically, to distinguish them from Jiff's public types. For example, `Date` versus `IDate`. */ -use super::error::{err, Error}; +use crate::shared::error::{itime::Error as E, Error}; #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] pub(crate) struct ITimestamp { @@ -144,10 +144,12 @@ impl IDateTime { &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"), - )?; + let day_second = self + .time + .to_second() + .second + .checked_add(seconds) + .ok_or_else(|| Error::from(E::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 +164,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. /// @@ -221,18 +223,12 @@ impl IEpochDay { #[inline] 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(|| Error::from(E::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(Error::from(E::EpochDayDays)); } Ok(ret) } @@ -264,10 +260,7 @@ impl IDate { 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(Error::from(E::DateInvalidDays { year, month })); } } Ok(IDate { year, month, day }) @@ -285,35 +278,19 @@ impl IDate { day: i16, ) -> 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(Error::from(E::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, - ) - })? + .map_err(|_| Error::from(E::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(Error::from(E::DateInvalidDayOfYear { year })); } Ok(end) } @@ -331,10 +308,7 @@ impl IDate { mut day: i16, ) -> 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(Error::from(E::DateInvalidDayOfYearNoLeap)); } if day >= 60 && is_leap_year(year) { day += 1; @@ -394,10 +368,7 @@ impl IDate { weekday: IWeekday, ) -> 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(Error::from(E::NthWeekdayOfMonth)); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -414,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(Error::from(E::DateInvalidDays { + year: self.year, + month: self.month, + })); } IDate::try_new(self.year, self.month, day) } @@ -433,11 +401,7 @@ impl IDate { 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(Error::from(E::Yesterday)); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -455,11 +419,7 @@ impl IDate { 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(Error::from(E::Tomorrow)); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -474,14 +434,7 @@ impl IDate { 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(Error::from(E::YearPrevious)); } Ok(year) } @@ -491,14 +444,7 @@ impl IDate { 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(Error::from(E::YearNext)); } Ok(year) } diff --git a/src/civil/date.rs b/src/civil/date.rs index 2728883..e691169 100644 --- a/src/civil/date.rs +++ b/src/civil/date.rs @@ -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::shared2)?, )) } @@ -3189,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::shared2)?; 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::shared2)?; return Ok(Date::from_idate_const(idate)); } }; diff --git a/src/error/mod.rs b/src/error/mod.rs index 7aad0ca..1d81b23 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -1,4 +1,9 @@ -use crate::{shared::util::error::Error as SharedError, util::sync::Arc}; +use crate::{ + shared::{ + error::Error as SharedError2, util::error::Error as SharedError, + }, + util::sync::Arc, +}; pub(crate) mod civil; pub(crate) mod duration; @@ -151,6 +156,11 @@ impl Error { Error::from(ErrorKind::Shared(err)) } + /// Creates a new error from the special "shared" error type. + pub(crate) fn shared2(err: SharedError2) -> Error { + Error::from(ErrorKind::Shared2(err)) + } + /// A convenience constructor for building an I/O error. /// /// This returns an error that is just a simple wrapper around the @@ -281,6 +291,7 @@ enum ErrorKind { Range(RangeError), RoundingIncrement(self::util::RoundingIncrementError), Shared(SharedError), + Shared2(SharedError2), SignedDuration(self::signed_duration::Error), SlimRange(SlimRangeError), Span(self::span::Error), @@ -324,6 +335,7 @@ impl core::fmt::Display for ErrorKind { Range(ref err) => err.fmt(f), RoundingIncrement(ref err) => err.fmt(f), Shared(ref err) => err.fmt(f), + Shared2(ref err) => err.fmt(f), SignedDuration(ref err) => err.fmt(f), SlimRange(ref err) => err.fmt(f), Span(ref err) => err.fmt(f), diff --git a/src/fmt/strtime/mod.rs b/src/fmt/strtime/mod.rs index 84e1c1c..6a2d1f6 100644 --- a/src/fmt/strtime/mod.rs +++ b/src/fmt/strtime/mod.rs @@ -2079,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: diff --git a/src/shared/error/itime.rs b/src/shared/error/itime.rs new file mode 100644 index 0000000..0340330 --- /dev/null +++ b/src/shared/error/itime.rs @@ -0,0 +1,98 @@ +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/src/shared/error/mod.rs b/src/shared/error/mod.rs new file mode 100644 index 0000000..49a4242 --- /dev/null +++ b/src/shared/error/mod.rs @@ -0,0 +1,56 @@ +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 } + } +} + +#[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") + } +} +*/ diff --git a/src/shared/mod.rs b/src/shared/mod.rs index a27c424..4c7ff7b 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -502,6 +502,7 @@ impl PosixTimeZone<&'static str> { // Does not require `alloc`, but is only used when `alloc` is enabled. #[cfg(feature = "alloc")] pub(crate) mod crc32; +pub(crate) mod error; pub(crate) mod posix; #[cfg(feature = "alloc")] pub(crate) mod tzif; diff --git a/src/shared/tzif.rs b/src/shared/tzif.rs index cef139e..587783d 100644 --- a/src/shared/tzif.rs +++ b/src/shared/tzif.rs @@ -230,8 +230,8 @@ impl TzifOwned { let clamped = timestamp.clamp(TIMESTAMP_MIN, TIMESTAMP_MAX); // only-jiff-start warn!( - "found Unix timestamp {timestamp} that is outside \ - Jiff's supported range, clamping to {clamped}", + "found Unix timestamp `{timestamp}` that is outside \ + Jiff's supported range, clamping to `{clamped}`", ); // only-jiff-end timestamp = clamped; @@ -378,7 +378,7 @@ impl TzifOwned { if !(TIMESTAMP_MIN <= occur && occur <= TIMESTAMP_MAX) { // only-jiff-start warn!( - "leap second occurrence {occur} is \ + "leap second occurrence `{occur}` is \ not in Jiff's supported range" ) // only-jiff-end diff --git a/src/shared/util/itime.rs b/src/shared/util/itime.rs index 5336eca..8a30943 100644 --- a/src/shared/util/itime.rs +++ b/src/shared/util/itime.rs @@ -22,7 +22,7 @@ they are internal types. Specifically, to distinguish them from Jiff's public types. For example, `Date` versus `IDate`. */ -use super::error::{err, Error}; +use crate::shared::error::{itime::Error as E, Error}; #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] pub(crate) struct ITimestamp { @@ -142,10 +142,12 @@ impl IDateTime { &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"), - )?; + let day_second = self + .time + .to_second() + .second + .checked_add(seconds) + .ok_or_else(|| Error::from(E::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 +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,18 +221,12 @@ impl IEpochDay { #[inline] 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(|| Error::from(E::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(Error::from(E::EpochDayDays)); } Ok(ret) } @@ -262,10 +258,7 @@ impl IDate { 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(Error::from(E::DateInvalidDays { year, month })); } } Ok(IDate { year, month, day }) @@ -283,35 +276,19 @@ impl IDate { day: i16, ) -> 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(Error::from(E::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, - ) - })? + .map_err(|_| Error::from(E::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(Error::from(E::DateInvalidDayOfYear { year })); } Ok(end) } @@ -329,10 +306,7 @@ impl IDate { mut day: i16, ) -> 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(Error::from(E::DateInvalidDayOfYearNoLeap)); } if day >= 60 && is_leap_year(year) { day += 1; @@ -392,10 +366,7 @@ impl IDate { weekday: IWeekday, ) -> 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(Error::from(E::NthWeekdayOfMonth)); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -412,13 +383,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(Error::from(E::DateInvalidDays { + year: self.year, + month: self.month, + })); } IDate::try_new(self.year, self.month, day) } @@ -431,11 +399,7 @@ impl IDate { 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(Error::from(E::Yesterday)); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -453,11 +417,7 @@ impl IDate { 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(Error::from(E::Tomorrow)); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -472,14 +432,7 @@ impl IDate { 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(Error::from(E::YearPrevious)); } Ok(year) } @@ -489,14 +442,7 @@ impl IDate { 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(Error::from(E::YearNext)); } Ok(year) } From d024322b2728181502d8c1812156348562a0fd9b Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 19 Dec 2025 19:20:40 -0500 Subject: [PATCH 15/30] shared: use structured errors for TZif parsing For this one, I defined the error type inline with it. This just felt right and it does stay reasonably encapsulated. I'll probably swing back and do the same for the time range error type? --- src/error/mod.rs | 7 + src/shared/tzif.rs | 802 +++++++++++++++++++++++++++++++++------------ src/tz/tzif.rs | 3 +- 3 files changed, 592 insertions(+), 220 deletions(-) diff --git a/src/error/mod.rs b/src/error/mod.rs index 1d81b23..85a158a 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -161,6 +161,11 @@ impl Error { Error::from(ErrorKind::Shared2(err)) } + /// Creates a new error from the special TZif error type. + pub(crate) fn tzif(err: crate::shared::tzif::TzifError) -> Error { + Error::from(ErrorKind::Tzif(err)) + } + /// A convenience constructor for building an I/O error. /// /// This returns an error that is just a simple wrapper around the @@ -305,6 +310,7 @@ enum ErrorKind { TzTimeZone(self::tz::timezone::Error), #[allow(dead_code)] TzZic(self::tz::zic::Error), + Tzif(crate::shared::tzif::TzifError), Unknown, Zoned(self::zoned::Error), } @@ -348,6 +354,7 @@ impl core::fmt::Display for ErrorKind { TzSystem(ref err) => err.fmt(f), TzTimeZone(ref err) => err.fmt(f), TzZic(ref err) => err.fmt(f), + Tzif(ref err) => err.fmt(f), Unknown => f.write_str("unknown jiff error"), Zoned(ref err) => err.fmt(f), } diff --git a/src/shared/tzif.rs b/src/shared/tzif.rs index 587783d..21de92e 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()?, )?; @@ -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()?, )?; @@ -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(()) } @@ -866,14 +789,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 +808,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 +874,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 +1391,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 +1413,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/tz/tzif.rs b/src/tz/tzif.rs index 6f2c815..ae3e032 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)) } From f2751642392ec2044754e2a122b8e23a5902b7d2 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 19 Dec 2025 20:24:08 -0500 Subject: [PATCH 16/30] shared: remove `alloc` cfgs I guess parsing a POSIX time zone doesn't actually require `alloc` any more. That's cool. --- crates/jiff-static/src/shared/posix.rs | 36 +- crates/jiff-static/src/shared/tzif.rs | 801 ++++++++++++++++++------- src/error/mod.rs | 3 + src/shared/posix.rs | 40 +- 4 files changed, 625 insertions(+), 255 deletions(-) diff --git a/crates/jiff-static/src/shared/posix.rs b/crates/jiff-static/src/shared/posix.rs index fef2428..1296adf 100644 --- a/crates/jiff-static/src/shared/posix.rs +++ b/crates/jiff-static/src/shared/posix.rs @@ -1577,10 +1577,8 @@ impl<'s> Parser<'s> { } } -// Tests require parsing, and parsing requires alloc. #[cfg(test)] mod tests { - use alloc::string::ToString; use super::*; @@ -1589,21 +1587,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..6406578 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(()) } @@ -846,14 +769,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 +788,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 +854,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 +1370,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 +1392,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/error/mod.rs b/src/error/mod.rs index 85a158a..6b5e386 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -162,6 +162,7 @@ impl Error { } /// 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)) } @@ -310,6 +311,7 @@ enum ErrorKind { 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), @@ -354,6 +356,7 @@ impl core::fmt::Display for ErrorKind { 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), diff --git a/src/shared/posix.rs b/src/shared/posix.rs index 2f5a7bd..c9a4014 100644 --- a/src/shared/posix.rs +++ b/src/shared/posix.rs @@ -17,7 +17,6 @@ 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> { // We enable the IANA v3+ extensions here. (Namely, that the time // specification hour value has the range `-167..=167` instead of @@ -31,7 +30,6 @@ 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> { @@ -1588,11 +1586,8 @@ impl<'s> Parser<'s> { } } -// Tests require parsing, and parsing requires alloc. -#[cfg(feature = "alloc")] #[cfg(test)] mod tests { - use alloc::string::ToString; use super::*; @@ -1601,21 +1596,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 } From ac0054c72f72f4ea131128200d543b0c99337e68 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 19 Dec 2025 20:36:03 -0500 Subject: [PATCH 17/30] shared: replace some uses of `write!` I believe this reduces code size. --- src/shared/posix.rs | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/shared/posix.rs b/src/shared/posix.rs index c9a4014..648ad39 100644 --- a/src/shared/posix.rs +++ b/src/shared/posix.rs @@ -342,12 +342,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)?; } @@ -361,22 +360,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(()) } } @@ -427,11 +433,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(()) } @@ -499,10 +506,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(()) } } } @@ -515,7 +531,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. } @@ -523,7 +539,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 { @@ -546,13 +562,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 { @@ -574,9 +590,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) } } } From 26202e5d4d6f95cd02a488294f70cf1865e60488 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 20 Dec 2025 08:46:07 -0500 Subject: [PATCH 18/30] shared: switch POSIX time zone parsing over to structured errors --- crates/jiff-static/src/shared/posix.rs | 1248 +++++++++++++++++------- src/error/mod.rs | 23 +- src/shared/posix.rs | 1195 ++++++++++++++++------- src/shared/util/error.rs | 46 - src/shared/util/mod.rs | 1 - src/tz/posix.rs | 4 +- 6 files changed, 1769 insertions(+), 748 deletions(-) delete mode 100644 src/shared/util/error.rs diff --git a/crates/jiff-static/src/shared/posix.rs b/crates/jiff-static/src/shared/posix.rs index 1296adf..eb6b29e 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,9 +1404,726 @@ impl<'s> Parser<'s> { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PosixTimeZoneError { + kind: ErrorKind, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum ErrorKind { + AbbreviationDst(AbbreviationError), + AbbreviationStd(AbbreviationError), + Empty, + ExpectedCommaAfterDst, + FoundDstNoRule, + FoundDstNoRuleWithOffset, + FoundEndAfterComma, + FoundRemaining, + OffsetDst(PosixOffsetError), + OffsetStd(PosixOffsetError), + Rule(PosixRuleError), +} + +impl core::fmt::Display for PosixTimeZoneError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::ErrorKind::*; + match self.kind { + AbbreviationDst(ref err) => { + f.write_str("failed to parse DST time zone abbreviation: ")?; + core::fmt::Display::fmt(err, f) + } + AbbreviationStd(ref err) => { + f.write_str( + "failed to parse standard time zone abbreviation: ", + )?; + core::fmt::Display::fmt(err, f) + } + Empty => f.write_str( + "an empty string is not a valid POSIX time zone \ + transition rule", + ), + ExpectedCommaAfterDst => f.write_str( + "expected `,` after parsing DST offset \ + in POSIX time zone string", + ), + FoundDstNoRule => f.write_str( + "found DST abbreviation in POSIX time zone string, \ + but no transition rule \ + (this is technically allowed by POSIX, but has \ + unspecified behavior)", + ), + FoundDstNoRuleWithOffset => f.write_str( + "found DST abbreviation and offset in POSIX time zone string, \ + but no transition rule \ + (this is technically allowed by POSIX, but has \ + unspecified behavior)", + ), + FoundEndAfterComma => f.write_str( + "after parsing DST offset in POSIX time zone string, \ + found end of string after a trailing `,`", + ), + FoundRemaining => f.write_str( + "expected entire POSIX TZ string to be a valid \ + time zone transition rule, but found data after \ + parsing a valid time zone transition rule", + ), + OffsetDst(ref err) => { + f.write_str("failed to parse DST offset: ")?; + core::fmt::Display::fmt(err, f) + } + OffsetStd(ref err) => { + f.write_str("failed to parse standard offset: ")?; + core::fmt::Display::fmt(err, f) + } + Rule(ref err) => core::fmt::Display::fmt(err, f), + } + } +} + +impl From for PosixTimeZoneError { + fn from(kind: ErrorKind) -> PosixTimeZoneError { + PosixTimeZoneError { kind } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixOffsetError { + HourPosix(HourPosixError), + IncompleteMinutes, + IncompleteSeconds, + Minute(MinuteError), + OptionalSign(OptionalSignError), + Second(SecondError), +} + +impl core::fmt::Display for PosixOffsetError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixOffsetError::*; + match *self { + HourPosix(ref err) => core::fmt::Display::fmt(err, f), + IncompleteMinutes => f.write_str( + "incomplete time in \ + POSIX time zone string (missing minutes)", + ), + IncompleteSeconds => f.write_str( + "incomplete time in \ + POSIX time zone string (missing seconds)", + ), + Minute(ref err) => core::fmt::Display::fmt(err, f), + Second(ref err) => core::fmt::Display::fmt(err, f), + OptionalSign(ref err) => { + f.write_str( + "failed to parse sign for time offset \ + POSIX time zone string", + )?; + core::fmt::Display::fmt(err, f) + } + } + } +} + +impl From for PosixOffsetError { + fn from(err: HourPosixError) -> PosixOffsetError { + PosixOffsetError::HourPosix(err) + } +} + +impl From for PosixOffsetError { + fn from(err: MinuteError) -> PosixOffsetError { + PosixOffsetError::Minute(err) + } +} + +impl From for PosixOffsetError { + fn from(err: OptionalSignError) -> PosixOffsetError { + PosixOffsetError::OptionalSign(err) + } +} + +impl From for PosixOffsetError { + fn from(err: SecondError) -> PosixOffsetError { + PosixOffsetError::Second(err) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixRuleError { + DateTimeEnd(PosixDateTimeError), + DateTimeStart(PosixDateTimeError), + ExpectedEnd, +} + +impl core::fmt::Display for PosixRuleError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixRuleError::*; + match *self { + DateTimeEnd(ref err) => { + f.write_str("failed to parse end of DST transition rule: ")?; + core::fmt::Display::fmt(err, f) + } + DateTimeStart(ref err) => { + f.write_str("failed to parse start of DST transition rule: ")?; + core::fmt::Display::fmt(err, f) + } + ExpectedEnd => f.write_str( + "expected end of DST rule after parsing the start \ + of the DST rule", + ), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixDateTimeError { + Date(PosixDateError), + ExpectedTime, + Time(PosixTimeError), +} + +impl core::fmt::Display for PosixDateTimeError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixDateTimeError::*; + match *self { + Date(ref err) => core::fmt::Display::fmt(err, f), + ExpectedTime => f.write_str( + "expected time specification after `/` following a date + specification in a POSIX time zone DST transition rule", + ), + Time(ref err) => core::fmt::Display::fmt(err, f), + } + } +} + +impl From for PosixDateTimeError { + fn from(err: PosixDateError) -> PosixDateTimeError { + PosixDateTimeError::Date(err) + } +} + +impl From for PosixDateTimeError { + fn from(err: PosixTimeError) -> PosixDateTimeError { + PosixDateTimeError::Time(err) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixDateError { + ExpectedJulianNoLeap, + ExpectedMonthWeekWeekday, + JulianLeap(PosixJulianLeapError), + JulianNoLeap(PosixJulianNoLeapError), + UnexpectedByte, + WeekdayOfMonth(WeekdayOfMonthError), +} + +impl core::fmt::Display for PosixDateError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixDateError::*; + match *self { + ExpectedJulianNoLeap => f.write_str( + "expected one-based Julian day after `J` in date \ + specification of a POSIX time zone DST \ + transition rule, but found the end of input", + ), + ExpectedMonthWeekWeekday => f.write_str( + "expected month-week-weekday after `M` in date \ + specification of a POSIX time zone DST \ + transition rule, but found the end of input", + ), + JulianLeap(ref err) => core::fmt::Display::fmt(err, f), + JulianNoLeap(ref err) => core::fmt::Display::fmt(err, f), + UnexpectedByte => f.write_str( + "expected `J`, a digit or `M` at the beginning of a date \ + specification of a POSIX time zone DST transition rule", + ), + WeekdayOfMonth(ref err) => core::fmt::Display::fmt(err, f), + } + } +} + +impl From for PosixDateError { + fn from(err: PosixJulianLeapError) -> PosixDateError { + PosixDateError::JulianLeap(err) + } +} + +impl From for PosixDateError { + fn from(err: PosixJulianNoLeapError) -> PosixDateError { + PosixDateError::JulianNoLeap(err) + } +} + +impl From for PosixDateError { + fn from(err: WeekdayOfMonthError) -> PosixDateError { + PosixDateError::WeekdayOfMonth(err) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixJulianNoLeapError { + Parse(NumberError), + Range, +} + +impl core::fmt::Display for PosixJulianNoLeapError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixJulianNoLeapError::*; + match *self { + Parse(ref err) => { + f.write_str("invalid one-based Julian day digits: ")?; + core::fmt::Display::fmt(err, f) + } + Range => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 super::*; fn posix_time_zone( diff --git a/src/error/mod.rs b/src/error/mod.rs index 6b5e386..6d2187c 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -1,9 +1,4 @@ -use crate::{ - shared::{ - error::Error as SharedError2, util::error::Error as SharedError, - }, - util::sync::Arc, -}; +use crate::{shared::error::Error as SharedError2, util::sync::Arc}; pub(crate) mod civil; pub(crate) mod duration; @@ -151,11 +146,6 @@ impl 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)) - } - /// Creates a new error from the special "shared" error type. pub(crate) fn shared2(err: SharedError2) -> Error { Error::from(ErrorKind::Shared2(err)) @@ -167,6 +157,13 @@ impl 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. /// /// This returns an error that is just a simple wrapper around the @@ -294,9 +291,9 @@ enum ErrorKind { OsStrUtf8(self::util::OsStrUtf8Error), ParseInt(self::util::ParseIntError), ParseFraction(self::util::ParseFractionError), + PosixTz(crate::shared::posix::PosixTimeZoneError), Range(RangeError), RoundingIncrement(self::util::RoundingIncrementError), - Shared(SharedError), Shared2(SharedError2), SignedDuration(self::signed_duration::Error), SlimRange(SlimRangeError), @@ -340,9 +337,9 @@ impl core::fmt::Display for ErrorKind { 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), - Shared(ref err) => err.fmt(f), Shared2(ref err) => err.fmt(f), SignedDuration(ref err) => err.fmt(f), SlimRange(ref err) => err.fmt(f), diff --git a/src/shared/posix.rs b/src/shared/posix.rs index 648ad39..55c9de6 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,7 +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. - 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 @@ -32,7 +32,8 @@ impl PosixTimeZone { /// is remaining. 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() } @@ -688,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) } @@ -705,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())) } @@ -716,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'<') @@ -748,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 }) } @@ -806,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) } } @@ -832,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; @@ -859,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`. @@ -888,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() @@ -898,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; @@ -918,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`. @@ -959,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()?; } @@ -1013,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 }) } @@ -1037,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, @@ -1046,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) @@ -1068,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()?)) } @@ -1086,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), } } @@ -1111,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) } @@ -1137,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) } @@ -1166,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)) @@ -1202,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 { @@ -1221,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()?; } @@ -1257,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) } @@ -1278,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) } @@ -1302,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) } @@ -1331,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) } @@ -1365,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) } @@ -1391,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) } @@ -1417,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) } @@ -1447,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)); @@ -1477,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) @@ -1494,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; } @@ -1509,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) @@ -1527,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) } @@ -1604,9 +1414,726 @@ impl<'s> Parser<'s> { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PosixTimeZoneError { + kind: ErrorKind, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum ErrorKind { + AbbreviationDst(AbbreviationError), + AbbreviationStd(AbbreviationError), + Empty, + ExpectedCommaAfterDst, + FoundDstNoRule, + FoundDstNoRuleWithOffset, + FoundEndAfterComma, + FoundRemaining, + OffsetDst(PosixOffsetError), + OffsetStd(PosixOffsetError), + Rule(PosixRuleError), +} + +impl core::fmt::Display for PosixTimeZoneError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::ErrorKind::*; + match self.kind { + AbbreviationDst(ref err) => { + f.write_str("failed to parse DST time zone abbreviation: ")?; + core::fmt::Display::fmt(err, f) + } + AbbreviationStd(ref err) => { + f.write_str( + "failed to parse standard time zone abbreviation: ", + )?; + core::fmt::Display::fmt(err, f) + } + Empty => f.write_str( + "an empty string is not a valid POSIX time zone \ + transition rule", + ), + ExpectedCommaAfterDst => f.write_str( + "expected `,` after parsing DST offset \ + in POSIX time zone string", + ), + FoundDstNoRule => f.write_str( + "found DST abbreviation in POSIX time zone string, \ + but no transition rule \ + (this is technically allowed by POSIX, but has \ + unspecified behavior)", + ), + FoundDstNoRuleWithOffset => f.write_str( + "found DST abbreviation and offset in POSIX time zone string, \ + but no transition rule \ + (this is technically allowed by POSIX, but has \ + unspecified behavior)", + ), + FoundEndAfterComma => f.write_str( + "after parsing DST offset in POSIX time zone string, \ + found end of string after a trailing `,`", + ), + FoundRemaining => f.write_str( + "expected entire POSIX TZ string to be a valid \ + time zone transition rule, but found data after \ + parsing a valid time zone transition rule", + ), + OffsetDst(ref err) => { + f.write_str("failed to parse DST offset: ")?; + core::fmt::Display::fmt(err, f) + } + OffsetStd(ref err) => { + f.write_str("failed to parse standard offset: ")?; + core::fmt::Display::fmt(err, f) + } + Rule(ref err) => core::fmt::Display::fmt(err, f), + } + } +} + +impl From for PosixTimeZoneError { + fn from(kind: ErrorKind) -> PosixTimeZoneError { + PosixTimeZoneError { kind } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixOffsetError { + HourPosix(HourPosixError), + IncompleteMinutes, + IncompleteSeconds, + Minute(MinuteError), + OptionalSign(OptionalSignError), + Second(SecondError), +} + +impl core::fmt::Display for PosixOffsetError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixOffsetError::*; + match *self { + HourPosix(ref err) => core::fmt::Display::fmt(err, f), + IncompleteMinutes => f.write_str( + "incomplete time in \ + POSIX time zone string (missing minutes)", + ), + IncompleteSeconds => f.write_str( + "incomplete time in \ + POSIX time zone string (missing seconds)", + ), + Minute(ref err) => core::fmt::Display::fmt(err, f), + Second(ref err) => core::fmt::Display::fmt(err, f), + OptionalSign(ref err) => { + f.write_str( + "failed to parse sign for time offset \ + POSIX time zone string", + )?; + core::fmt::Display::fmt(err, f) + } + } + } +} + +impl From for PosixOffsetError { + fn from(err: HourPosixError) -> PosixOffsetError { + PosixOffsetError::HourPosix(err) + } +} + +impl From for PosixOffsetError { + fn from(err: MinuteError) -> PosixOffsetError { + PosixOffsetError::Minute(err) + } +} + +impl From for PosixOffsetError { + fn from(err: OptionalSignError) -> PosixOffsetError { + PosixOffsetError::OptionalSign(err) + } +} + +impl From for PosixOffsetError { + fn from(err: SecondError) -> PosixOffsetError { + PosixOffsetError::Second(err) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixRuleError { + DateTimeEnd(PosixDateTimeError), + DateTimeStart(PosixDateTimeError), + ExpectedEnd, +} + +impl core::fmt::Display for PosixRuleError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixRuleError::*; + match *self { + DateTimeEnd(ref err) => { + f.write_str("failed to parse end of DST transition rule: ")?; + core::fmt::Display::fmt(err, f) + } + DateTimeStart(ref err) => { + f.write_str("failed to parse start of DST transition rule: ")?; + core::fmt::Display::fmt(err, f) + } + ExpectedEnd => f.write_str( + "expected end of DST rule after parsing the start \ + of the DST rule", + ), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixDateTimeError { + Date(PosixDateError), + ExpectedTime, + Time(PosixTimeError), +} + +impl core::fmt::Display for PosixDateTimeError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixDateTimeError::*; + match *self { + Date(ref err) => core::fmt::Display::fmt(err, f), + ExpectedTime => f.write_str( + "expected time specification after `/` following a date + specification in a POSIX time zone DST transition rule", + ), + Time(ref err) => core::fmt::Display::fmt(err, f), + } + } +} + +impl From for PosixDateTimeError { + fn from(err: PosixDateError) -> PosixDateTimeError { + PosixDateTimeError::Date(err) + } +} + +impl From for PosixDateTimeError { + fn from(err: PosixTimeError) -> PosixDateTimeError { + PosixDateTimeError::Time(err) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixDateError { + ExpectedJulianNoLeap, + ExpectedMonthWeekWeekday, + JulianLeap(PosixJulianLeapError), + JulianNoLeap(PosixJulianNoLeapError), + UnexpectedByte, + WeekdayOfMonth(WeekdayOfMonthError), +} + +impl core::fmt::Display for PosixDateError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixDateError::*; + match *self { + ExpectedJulianNoLeap => f.write_str( + "expected one-based Julian day after `J` in date \ + specification of a POSIX time zone DST \ + transition rule, but found the end of input", + ), + ExpectedMonthWeekWeekday => f.write_str( + "expected month-week-weekday after `M` in date \ + specification of a POSIX time zone DST \ + transition rule, but found the end of input", + ), + JulianLeap(ref err) => core::fmt::Display::fmt(err, f), + JulianNoLeap(ref err) => core::fmt::Display::fmt(err, f), + UnexpectedByte => f.write_str( + "expected `J`, a digit or `M` at the beginning of a date \ + specification of a POSIX time zone DST transition rule", + ), + WeekdayOfMonth(ref err) => core::fmt::Display::fmt(err, f), + } + } +} + +impl From for PosixDateError { + fn from(err: PosixJulianLeapError) -> PosixDateError { + PosixDateError::JulianLeap(err) + } +} + +impl From for PosixDateError { + fn from(err: PosixJulianNoLeapError) -> PosixDateError { + PosixDateError::JulianNoLeap(err) + } +} + +impl From for PosixDateError { + fn from(err: WeekdayOfMonthError) -> PosixDateError { + PosixDateError::WeekdayOfMonth(err) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PosixJulianNoLeapError { + Parse(NumberError), + Range, +} + +impl core::fmt::Display for PosixJulianNoLeapError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use self::PosixJulianNoLeapError::*; + match *self { + Parse(ref err) => { + f.write_str("invalid one-based Julian day digits: ")?; + core::fmt::Display::fmt(err, f) + } + Range => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 => write!( + f, + "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 super::*; fn posix_time_zone( 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/mod.rs b/src/shared/util/mod.rs index a31dc54..812a7f4 100644 --- a/src/shared/util/mod.rs +++ b/src/shared/util/mod.rs @@ -1,5 +1,4 @@ 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/tz/posix.rs b/src/tz/posix.rs index b682b89..ee65b71 100644 --- a/src/tz/posix.rs +++ b/src/tz/posix.rs @@ -210,7 +210,7 @@ impl PosixTimeZone { ) -> Result { let bytes = bytes.as_ref(); let inner = shared::PosixTimeZone::parse(bytes.as_ref()) - .map_err(Error::shared) + .map_err(Error::posix_tz) .context(E::InvalidPosixTz)?; Ok(PosixTimeZone { inner }) } @@ -224,7 +224,7 @@ impl PosixTimeZone { let bytes = bytes.as_ref(); let (inner, remaining) = shared::PosixTimeZone::parse_prefix(bytes.as_ref()) - .map_err(Error::shared) + .map_err(Error::posix_tz) .context(E::InvalidPosixTz)?; Ok((PosixTimeZone { inner }, remaining)) } From 831d3efb4da42bc70f7ac0294bc68a5c077bcf3d Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 20 Dec 2025 13:35:20 -0500 Subject: [PATCH 19/30] shared: move itime range error into itime module This follows the pattern I used for TZif and POSIX time zone parsing. --- crates/jiff-static/src/shared/mod.rs | 1 - crates/jiff-static/src/shared/util/itime.rs | 152 ++++++++++++++++---- crates/jiff-static/src/shared/util/mod.rs | 1 - src/civil/date.rs | 6 +- src/error/mod.rs | 12 +- src/shared/error/itime.rs | 98 ------------- src/shared/error/mod.rs | 56 -------- src/shared/mod.rs | 1 - src/shared/util/itime.rs | 152 ++++++++++++++++---- 9 files changed, 258 insertions(+), 221 deletions(-) delete mode 100644 src/shared/error/itime.rs delete mode 100644 src/shared/error/mod.rs diff --git a/crates/jiff-static/src/shared/mod.rs b/crates/jiff-static/src/shared/mod.rs index cfb528f..067be4b 100644 --- a/crates/jiff-static/src/shared/mod.rs +++ b/crates/jiff-static/src/shared/mod.rs @@ -474,7 +474,6 @@ pub struct PosixOffset { // Does not require `alloc`, but is only used when `alloc` is enabled. pub(crate) mod crc32; -pub(crate) mod error; pub(crate) mod posix; pub(crate) mod tzif; pub(crate) mod util; diff --git a/crates/jiff-static/src/shared/util/itime.rs b/crates/jiff-static/src/shared/util/itime.rs index 4e0a304..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 crate::shared::error::{itime::Error as E, Error}; - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] pub(crate) struct ITimestamp { pub(crate) second: i64, @@ -143,13 +141,13 @@ impl IDateTime { pub(crate) fn checked_add_seconds( &self, seconds: i32, - ) -> Result { + ) -> Result { let day_second = self .time .to_second() .second .checked_add(seconds) - .ok_or_else(|| Error::from(E::DateTimeSeconds))?; + .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)?; @@ -221,14 +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(|| Error::from(E::EpochDayI32))?; + .ok_or_else(|| RangeError::EpochDayI32)?; let ret = IEpochDay { epoch_day: sum }; if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) { - return Err(Error::from(E::EpochDayDays)); + return Err(RangeError::EpochDayDays); } Ok(ret) } @@ -256,11 +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(Error::from(E::DateInvalidDays { year, month })); + return Err(RangeError::DateInvalidDays { year, month }); } } Ok(IDate { year, month, day }) @@ -276,21 +277,22 @@ impl IDate { pub(crate) fn from_day_of_year( year: i16, day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 366) { - return Err(Error::from(E::DateInvalidDayOfYear { 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(|_| Error::from(E::DayOfYear))? + // 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(Error::from(E::DateInvalidDayOfYear { year })); + return Err(RangeError::DateInvalidDayOfYear { year }); } Ok(end) } @@ -306,9 +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(Error::from(E::DateInvalidDayOfYearNoLeap)); + return Err(RangeError::DateInvalidDayOfYearNoLeap); } if day >= 60 && is_leap_year(year) { day += 1; @@ -366,9 +368,9 @@ impl IDate { &self, nth: i8, weekday: IWeekday, - ) -> Result { + ) -> Result { if nth == 0 || !(-5 <= nth && nth <= 5) { - return Err(Error::from(E::NthWeekdayOfMonth)); + return Err(RangeError::NthWeekdayOfMonth); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -385,10 +387,10 @@ impl IDate { // of `Day`, we can't let this boundary condition escape. So we // check it here. if day < 1 { - return Err(Error::from(E::DateInvalidDays { + return Err(RangeError::DateInvalidDays { year: self.year, month: self.month, - })); + }); } IDate::try_new(self.year, self.month, day) } @@ -396,12 +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(Error::from(E::Yesterday)); + return Err(RangeError::Yesterday); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -414,12 +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(Error::from(E::Tomorrow)); + return Err(RangeError::Tomorrow); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -431,20 +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(Error::from(E::YearPrevious)); + 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(Error::from(E::YearNext)); + return Err(RangeError::YearNext); } Ok(year) } @@ -454,7 +456,7 @@ impl IDate { pub(crate) fn checked_add_days( &self, amount: i32, - ) -> Result { + ) -> Result { match amount { 0 => Ok(*self), -1 => self.yesterday(), @@ -666,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. @@ -868,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..98ff457 100644 --- a/crates/jiff-static/src/shared/util/mod.rs +++ b/crates/jiff-static/src/shared/util/mod.rs @@ -1,7 +1,6 @@ // 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/src/civil/date.rs b/src/civil/date.rs index e691169..5d53c6c 100644 --- a/src/civil/date.rs +++ b/src/civil/date.rs @@ -905,7 +905,7 @@ impl Date { Ok(Date::from_idate_const( idate .nth_weekday_of_month(nth, weekday) - .map_err(Error::shared2)?, + .map_err(Error::itime_range)?, )) } @@ -3191,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::shared2)?; + .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::shared2)?; + .map_err(Error::itime_range)?; return Ok(Date::from_idate_const(idate)); } }; diff --git a/src/error/mod.rs b/src/error/mod.rs index 6d2187c..3c9ddef 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -1,4 +1,4 @@ -use crate::{shared::error::Error as SharedError2, util::sync::Arc}; +use crate::util::sync::Arc; pub(crate) mod civil; pub(crate) mod duration; @@ -147,8 +147,10 @@ impl Error { } /// Creates a new error from the special "shared" error type. - pub(crate) fn shared2(err: SharedError2) -> Error { - Error::from(ErrorKind::Shared2(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. @@ -288,13 +290,13 @@ enum ErrorKind { 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), - Shared2(SharedError2), SignedDuration(self::signed_duration::Error), SlimRange(SlimRangeError), Span(self::span::Error), @@ -334,13 +336,13 @@ impl core::fmt::Display for ErrorKind { 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), - Shared2(ref err) => err.fmt(f), SignedDuration(ref err) => err.fmt(f), SlimRange(ref err) => err.fmt(f), Span(ref err) => err.fmt(f), diff --git a/src/shared/error/itime.rs b/src/shared/error/itime.rs deleted file mode 100644 index 0340330..0000000 --- a/src/shared/error/itime.rs +++ /dev/null @@ -1,98 +0,0 @@ -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/src/shared/error/mod.rs b/src/shared/error/mod.rs deleted file mode 100644 index 49a4242..0000000 --- a/src/shared/error/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -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 } - } -} - -#[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") - } -} -*/ diff --git a/src/shared/mod.rs b/src/shared/mod.rs index 4c7ff7b..a27c424 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -502,7 +502,6 @@ impl PosixTimeZone<&'static str> { // Does not require `alloc`, but is only used when `alloc` is enabled. #[cfg(feature = "alloc")] pub(crate) mod crc32; -pub(crate) mod error; pub(crate) mod posix; #[cfg(feature = "alloc")] pub(crate) mod tzif; diff --git a/src/shared/util/itime.rs b/src/shared/util/itime.rs index 8a30943..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 crate::shared::error::{itime::Error as E, Error}; - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] pub(crate) struct ITimestamp { pub(crate) second: i64, @@ -141,13 +139,13 @@ impl IDateTime { pub(crate) fn checked_add_seconds( &self, seconds: i32, - ) -> Result { + ) -> Result { let day_second = self .time .to_second() .second .checked_add(seconds) - .ok_or_else(|| Error::from(E::DateTimeSeconds))?; + .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)?; @@ -219,14 +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(|| Error::from(E::EpochDayI32))?; + .ok_or_else(|| RangeError::EpochDayI32)?; let ret = IEpochDay { epoch_day: sum }; if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) { - return Err(Error::from(E::EpochDayDays)); + return Err(RangeError::EpochDayDays); } Ok(ret) } @@ -254,11 +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(Error::from(E::DateInvalidDays { year, month })); + return Err(RangeError::DateInvalidDays { year, month }); } } Ok(IDate { year, month, day }) @@ -274,21 +275,22 @@ impl IDate { pub(crate) fn from_day_of_year( year: i16, day: i16, - ) -> Result { + ) -> Result { if !(1 <= day && day <= 366) { - return Err(Error::from(E::DateInvalidDayOfYear { 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(|_| Error::from(E::DayOfYear))? + // 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(Error::from(E::DateInvalidDayOfYear { year })); + return Err(RangeError::DateInvalidDayOfYear { year }); } Ok(end) } @@ -304,9 +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(Error::from(E::DateInvalidDayOfYearNoLeap)); + return Err(RangeError::DateInvalidDayOfYearNoLeap); } if day >= 60 && is_leap_year(year) { day += 1; @@ -364,9 +366,9 @@ impl IDate { &self, nth: i8, weekday: IWeekday, - ) -> Result { + ) -> Result { if nth == 0 || !(-5 <= nth && nth <= 5) { - return Err(Error::from(E::NthWeekdayOfMonth)); + return Err(RangeError::NthWeekdayOfMonth); } if nth > 0 { let first_weekday = self.first_of_month().weekday(); @@ -383,10 +385,10 @@ impl IDate { // of `Day`, we can't let this boundary condition escape. So we // check it here. if day < 1 { - return Err(Error::from(E::DateInvalidDays { + return Err(RangeError::DateInvalidDays { year: self.year, month: self.month, - })); + }); } IDate::try_new(self.year, self.month, day) } @@ -394,12 +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(Error::from(E::Yesterday)); + return Err(RangeError::Yesterday); } return Ok(IDate { year, month: 12, day: 31 }); } @@ -412,12 +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(Error::from(E::Tomorrow)); + return Err(RangeError::Tomorrow); } return Ok(IDate { year, month: 1, day: 1 }); } @@ -429,20 +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(Error::from(E::YearPrevious)); + 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(Error::from(E::YearNext)); + return Err(RangeError::YearNext); } Ok(year) } @@ -452,7 +454,7 @@ impl IDate { pub(crate) fn checked_add_days( &self, amount: i32, - ) -> Result { + ) -> Result { match amount { 0 => Ok(*self), -1 => self.yesterday(), @@ -664,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. @@ -866,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), + ); + } } From 259e8134ef07540beec9d1ed507b9a7a95aa9ba2 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 21 Dec 2025 08:13:11 -0500 Subject: [PATCH 20/30] error: implement an error chaining iterator We previously only used this in the `core::fmt::Display` trait implementation, so we just inlined it there. But we'll want to use it to get the root of the chain for error predicates, so this commit does some refactoring to provide a standalone internal iterator for the error chain. --- crates/jiff-static/src/shared/tzif.rs | 1 - src/error/mod.rs | 144 +++++++++++++++----------- src/shared/tzif.rs | 1 - src/tz/timezone.rs | 4 + 4 files changed, 89 insertions(+), 61 deletions(-) diff --git a/crates/jiff-static/src/shared/tzif.rs b/crates/jiff-static/src/shared/tzif.rs index 6406578..1b60a78 100644 --- a/crates/jiff-static/src/shared/tzif.rs +++ b/crates/jiff-static/src/shared/tzif.rs @@ -503,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); diff --git a/src/error/mod.rs b/src/error/mod.rs index 3c9ddef..8cdc895 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -74,6 +74,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 /// /// ``` @@ -85,37 +93,6 @@ impl Error { pub fn from_args<'a>(message: core::fmt::Arguments<'a>) -> Error { Error::from(ErrorKind::Adhoc(AdhocError::from_args(message))) } - - #[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`. :-( - consequent - } - } } impl Error { @@ -199,7 +176,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 @@ -209,6 +186,71 @@ 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`. :-( + consequent + } + } + + /// 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")] @@ -216,30 +258,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(()) } } @@ -357,7 +383,7 @@ impl core::fmt::Display for ErrorKind { TzZic(ref err) => err.fmt(f), #[cfg(feature = "alloc")] Tzif(ref err) => err.fmt(f), - Unknown => f.write_str("unknown jiff error"), + Unknown => f.write_str("unknown Jiff error"), Zoned(ref err) => err.fmt(f), } } @@ -378,10 +404,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")] diff --git a/src/shared/tzif.rs b/src/shared/tzif.rs index 21de92e..ef74b85 100644 --- a/src/shared/tzif.rs +++ b/src/shared/tzif.rs @@ -514,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); diff --git a/src/tz/timezone.rs b/src/tz/timezone.rs index ae4cbf4..1566826 100644 --- a/src/tz/timezone.rs +++ b/src/tz/timezone.rs @@ -1932,6 +1932,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; From 8e8033a29d6e7178167207e5604f632dd20e95a3 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 21 Dec 2025 09:09:15 -0500 Subject: [PATCH 21/30] error: add `Error::is_range` predicate I'm somewhat concerned that this doesn't cover all cases, but I think this should be a good start. --- src/civil/date.rs | 2 +- src/error/civil.rs | 2 -- src/error/mod.rs | 24 ++++++++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/civil/date.rs b/src/civil/date.rs index 5d53c6c..e56f66f 100644 --- a/src/civil/date.rs +++ b/src/civil/date.rs @@ -1059,7 +1059,7 @@ impl Date { let nth = t::SpanWeeks::try_new("nth weekday", nth)?; if nth == C(0) { - Err(Error::from(E::NthWeekdayNonZero)) + 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()); diff --git a/src/error/civil.rs b/src/error/civil.rs index 4f548b8..40fce64 100644 --- a/src/error/civil.rs +++ b/src/error/civil.rs @@ -13,7 +13,6 @@ pub(crate) enum Error { InvalidISOWeekNumber, OverflowDaysDuration, OverflowTimeNanoseconds, - NthWeekdayNonZero, RoundMustUseDaysOrBigger { unit: Unit }, RoundMustUseHoursOrSmaller { unit: Unit }, } @@ -68,7 +67,6 @@ impl core::fmt::Display for Error { OverflowTimeNanoseconds => { f.write_str("adding duration to time overflowed") } - NthWeekdayNonZero => f.write_str("nth weekday cannot be `0`"), RoundMustUseDaysOrBigger { unit } => write!( f, "rounding the span between two dates must use days \ diff --git a/src/error/mod.rs b/src/error/mod.rs index 8cdc895..14ae0c1 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -93,6 +93,23 @@ impl Error { pub fn from_args<'a>(message: core::fmt::Arguments<'a>) -> Error { Error::from(ErrorKind::Adhoc(AdhocError::from_args(message))) } + + /// Returns true when this error originated as a result of a value being + /// out of Jiff's supported range. + /// + /// # Example + /// + /// ``` + /// use jiff::civil::Date; + /// + /// assert!(Date::new(2025, 2, 29).unwrap_err().is_range()); + /// assert!("2025-02-29".parse::().unwrap_err().is_range()); + /// assert!(Date::strptime("%Y-%m-%d", "2025-02-29").unwrap_err().is_range()); + /// ``` + pub fn is_range(&self) -> bool { + use self::ErrorKind::*; + matches!(*self.root().kind(), Range(_) | SlimRange(_) | ITimeRange(_)) + } } impl Error { @@ -218,6 +235,13 @@ impl Error { } } + /// 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, From 3cb0b6aefa01f58498ab22fa18f8e0e1fe357697 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 21 Dec 2025 10:34:57 -0500 Subject: [PATCH 22/30] error: add `Error::is_crate_feature` predicate There's only a few of these, but they seem like a distinct category from other error types. It's also nice to do this and standarize on the specific error message. The other reason I wanted to do this was to distinguish it from a possible "is configuration error" category. (e.g., Trying to round a span to the nearest year without a relative datatime.) These could also be seen as configuration errors, but I want them to be in a separate category I think. --- src/error/mod.rs | 71 ++++++++++++++++++++++++++++-- src/error/tz/db.rs | 22 ++++----- src/error/tz/timezone.rs | 5 +-- src/tz/db/concatenated/disabled.rs | 5 ++- src/tz/db/zoneinfo/disabled.rs | 5 ++- src/tz/timezone.rs | 3 +- 6 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/error/mod.rs b/src/error/mod.rs index 14ae0c1..52edc89 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -110,6 +110,21 @@ impl Error { use self::ErrorKind::*; matches!(*self.root().kind(), Range(_) | SlimRange(_) | ITimeRange(_)) } + + /// 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 { @@ -211,10 +226,10 @@ impl Error { #[inline(never)] #[cold] - fn context_impl(self, consequent: Error) -> Error { + fn context_impl(self, _consequent: Error) -> Error { #[cfg(feature = "alloc")] { - let mut err = consequent; + let mut err = _consequent; if err.inner.is_none() { err = Error::from(ErrorKind::Unknown); } @@ -231,7 +246,12 @@ impl Error { #[cfg(not(feature = "alloc"))] { // We just completely drop `self`. :-( - consequent + // + // 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 } } @@ -325,6 +345,7 @@ impl core::fmt::Debug for Error { 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), @@ -373,6 +394,7 @@ impl core::fmt::Display for ErrorKind { match *self { Adhoc(ref msg) => msg.fmt(f), Civil(ref err) => err.fmt(f), + CrateFeature(ref err) => err.fmt(f), Duration(ref err) => err.fmt(f), FilePath(ref err) => err.fmt(f), Fmt(ref err) => err.fmt(f), @@ -559,6 +581,49 @@ impl core::fmt::Display for SlimRangeError { } } +/// 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 diff --git a/src/error/tz/db.rs b/src/error/tz/db.rs index e87dea2..70349c9 100644 --- a/src/error/tz/db.rs +++ b/src/error/tz/db.rs @@ -65,24 +65,24 @@ impl core::fmt::Display for Error { concatenated tzdata file", ), #[cfg(all(feature = "std", not(feature = "tzdb-concatenated")))] - DisabledConcatenated => f.write_str( - "system concatenated tzdb unavailable: \ - Jiff crate feature `tzdb-concatenated` is disabled, \ - opening tzdb at given path has therefore failed", - ), + DisabledConcatenated => { + f.write_str("system concatenated tzdb unavailable") + } #[cfg(all(feature = "std", not(feature = "tzdb-zoneinfo")))] - DisabledZoneInfo => f.write_str( - "system zoneinfo tzdb unavailable: \ - Jiff crate feature `tzdb-zoneinfo` is disabled, \ - opening tzdb at given path has therefore failed", - ), + 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") + write!( + f, + "failed to find time zone `{name}` \ + in time zone database", + ) } #[cfg(not(feature = "alloc"))] { diff --git a/src/error/tz/timezone.rs b/src/error/tz/timezone.rs index e649032..f3d66b1 100644 --- a/src/error/tz/timezone.rs +++ b/src/error/tz/timezone.rs @@ -34,10 +34,7 @@ impl core::fmt::Display for Error { without a timestamp or civil datetime", ), #[cfg(not(feature = "tz-system"))] - FailedSystem => f.write_str( - "failed to get system time zone since Jiff's \ - `tz-system` crate feature is not enabled", - ), + FailedSystem => f.write_str("failed to get system time zone"), } } } diff --git a/src/tz/db/concatenated/disabled.rs b/src/tz/db/concatenated/disabled.rs index c1ae7c6..06b8f78 100644 --- a/src/tz/db/concatenated/disabled.rs +++ b/src/tz/db/concatenated/disabled.rs @@ -13,8 +13,9 @@ impl Database { _path: &std::path::Path, ) -> Result { Err(crate::error::Error::from( - crate::error::tz::db::Error::DisabledConcatenated, - )) + crate::error::CrateFeatureError::TzdbConcatenated, + ) + .context(crate::error::tz::db::Error::DisabledConcatenated)) } pub(crate) fn none() -> Database { diff --git a/src/tz/db/zoneinfo/disabled.rs b/src/tz/db/zoneinfo/disabled.rs index f54d4e7..0290977 100644 --- a/src/tz/db/zoneinfo/disabled.rs +++ b/src/tz/db/zoneinfo/disabled.rs @@ -13,8 +13,9 @@ impl Database { _dir: &std::path::Path, ) -> Result { Err(crate::error::Error::from( - crate::error::tz::db::Error::DisabledZoneInfo, - )) + crate::error::CrateFeatureError::TzdbZoneInfo, + ) + .context(crate::error::tz::db::Error::DisabledZoneInfo)) } pub(crate) fn none() -> Database { diff --git a/src/tz/timezone.rs b/src/tz/timezone.rs index 1566826..ed54336 100644 --- a/src/tz/timezone.rs +++ b/src/tz/timezone.rs @@ -392,7 +392,8 @@ impl TimeZone { pub fn try_system() -> Result { #[cfg(not(feature = "tz-system"))] { - Err(Error::from(E::FailedSystem)) + Err(Error::from(crate::error::CrateFeatureError::TzSystem) + .context(E::FailedSystem)) } #[cfg(feature = "tz-system")] { From 523b55bc1a12d8c71fae36f50a644c7570c50133 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 09:40:20 -0500 Subject: [PATCH 23/30] error: add `Error::is_invalid_parameter` predicate --- src/error/mod.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/error/mod.rs b/src/error/mod.rs index 52edc89..461be2b 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -111,6 +111,80 @@ impl Error { 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. /// From 0392d43064e3b73ea84f84d2c5ae5bb2b46cdd6b Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 09:47:21 -0500 Subject: [PATCH 24/30] error: add note about introspection --- CHANGELOG.md | 2 ++ src/error/mod.rs | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e55473..914a7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ 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) diff --git a/src/error/mod.rs b/src/error/mod.rs index 461be2b..c320a6d 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -27,8 +27,11 @@ pub(crate) mod zoned; /// /// Other than implementing the [`std::error::Error`] trait when the /// `std` feature is enabled, the [`core::fmt::Debug`] trait and the -/// [`core::fmt::Display`] trait, this error type currently provides 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 /// From a50f6797ce06f78e1071acb653c4abc0f024b2c7 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 09:48:24 -0500 Subject: [PATCH 25/30] logging: small tweak to formatting --- src/tz/db/bundled/enabled.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tz/db/bundled/enabled.rs b/src/tz/db/bundled/enabled.rs index 7592ea2..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; } From 21f69521cdeba481b66ee8bf24bd065967363b04 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 14:15:40 -0500 Subject: [PATCH 26/30] shared: remove escaping and UTF-8 routines from `shared` module With the error refactor, these are no longer used. Namely, while switching to structured errors, I took that opportunity to slim down errors so that we are not repeating parts of the input as often. --- crates/jiff-static/src/shared/util/escape.rs | 1 + crates/jiff-static/src/shared/util/mod.rs | 2 - crates/jiff-static/src/shared/util/utf8.rs | 1 + src/error/fmt/offset.rs | 2 +- src/fmt/strtime/format.rs | 2 +- src/shared/util/escape.rs | 122 ------------------- src/shared/util/mod.rs | 2 - src/shared/util/utf8.rs | 89 -------------- src/util/escape.rs | 121 +++++++++++++++++- src/util/utf8.rs | 90 ++++++++++++++ 10 files changed, 210 insertions(+), 222 deletions(-) delete mode 100644 src/shared/util/escape.rs delete mode 100644 src/shared/util/utf8.rs diff --git a/crates/jiff-static/src/shared/util/escape.rs b/crates/jiff-static/src/shared/util/escape.rs index 5e3b8b3..e5b182b 100644 --- a/crates/jiff-static/src/shared/util/escape.rs +++ b/crates/jiff-static/src/shared/util/escape.rs @@ -122,3 +122,4 @@ impl core::fmt::Debug for RepeatByte { Ok(()) } } + diff --git a/crates/jiff-static/src/shared/util/mod.rs b/crates/jiff-static/src/shared/util/mod.rs index 98ff457..3630e73 100644 --- a/crates/jiff-static/src/shared/util/mod.rs +++ b/crates/jiff-static/src/shared/util/mod.rs @@ -1,6 +1,4 @@ // auto-generated by: jiff-cli generate shared pub(crate) mod array_str; -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 585c0e7..1738a20 100644 --- a/crates/jiff-static/src/shared/util/utf8.rs +++ b/crates/jiff-static/src/shared/util/utf8.rs @@ -89,3 +89,4 @@ pub(crate) fn decode(bytes: &[u8]) -> Option> { // yield at least one Unicode scalar value. Some(Ok(string.chars().next().unwrap())) } + diff --git a/src/error/fmt/offset.rs b/src/error/fmt/offset.rs index 459133f..af81804 100644 --- a/src/error/fmt/offset.rs +++ b/src/error/fmt/offset.rs @@ -1,4 +1,4 @@ -use crate::{error, shared::util::escape}; +use crate::{error, util::escape}; #[derive(Clone, Debug)] pub(crate) enum Error { diff --git a/src/fmt/strtime/format.rs b/src/fmt/strtime/format.rs index 65de289..ab7c981 100644 --- a/src/fmt/strtime/format.rs +++ b/src/fmt/strtime/format.rs @@ -12,8 +12,8 @@ use crate::{ util::{DecimalFormatter, FractionalFormatter}, Write, WriteExt, }, - shared::util::utf8, tz::Offset, + util::utf8, Error, }; diff --git a/src/shared/util/escape.rs b/src/shared/util/escape.rs deleted file mode 100644 index dd0736d..0000000 --- a/src/shared/util/escape.rs +++ /dev/null @@ -1,122 +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 { - #[inline(never)] - 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 { - #[inline(never)] - 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> { - #[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' => 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> { - #[inline(never)] - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "\"")?; - core::fmt::Display::fmt(self, f)?; - write!(f, "\"")?; - 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/src/shared/util/mod.rs b/src/shared/util/mod.rs index 812a7f4..971f365 100644 --- a/src/shared/util/mod.rs +++ b/src/shared/util/mod.rs @@ -1,4 +1,2 @@ pub(crate) mod array_str; -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 33d920c..0000000 --- a/src/shared/util/utf8.rs +++ /dev/null @@ -1,89 +0,0 @@ -/// 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 -/// 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 => { - // 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())) -} diff --git a/src/util/escape.rs b/src/util/escape.rs index 9deb81a..dd0736d 100644 --- a/src/util/escape.rs +++ b/src/util/escape.rs @@ -4,8 +4,119 @@ 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, RepeatByte}; +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 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 { + #[inline(never)] + 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> { + #[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' => 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> { + #[inline(never)] + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "\"")?; + core::fmt::Display::fmt(self, f)?; + write!(f, "\"")?; + 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/src/util/utf8.rs b/src/util/utf8.rs index dc62b6b..bd5b462 100644 --- a/src/util/utf8.rs +++ b/src/util/utf8.rs @@ -1,5 +1,95 @@ 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 +/// 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 => { + // 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`. #[inline] pub(crate) fn cmp_ignore_ascii_case(s1: &str, s2: &str) -> Ordering { From 4d2041567c2b98f9105444cd8e16f9cb616271b6 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 14:54:35 -0500 Subject: [PATCH 27/30] binary-size: remove many uses of `write!` Using `write!` unnecessarily when a simple `f.write_str` would do ends up generating more LLVM lines on my Biff `cargo llvm-lines` benchmark. This PR replaces those uses of `write!` with `Formatter::write_str`. --- crates/jiff-static/src/shared/posix.rs | 27 +++++++++----------------- src/fmt/offset.rs | 13 +++---------- src/shared/posix.rs | 27 +++++++++----------------- src/signed_duration.rs | 17 +++++++++------- src/span.rs | 2 +- src/tz/db/mod.rs | 14 ++++++------- src/tz/db/zoneinfo/enabled.rs | 8 ++++---- src/tz/offset.rs | 2 +- src/tz/timezone.rs | 12 ++++++++---- src/tz/tzif.rs | 6 +++--- src/util/c.rs | 2 +- src/util/cache.rs | 18 ++++++++--------- src/util/escape.rs | 26 +++++++++++++------------ src/util/rangeint.rs | 2 +- 14 files changed, 79 insertions(+), 97 deletions(-) diff --git a/crates/jiff-static/src/shared/posix.rs b/crates/jiff-static/src/shared/posix.rs index eb6b29e..6297171 100644 --- a/crates/jiff-static/src/shared/posix.rs +++ b/crates/jiff-static/src/shared/posix.rs @@ -1673,8 +1673,7 @@ impl core::fmt::Display for PosixJulianNoLeapError { f.write_str("invalid one-based Julian day digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed one-based Julian day, but it's not in supported \ range of `1..=365`", ), @@ -1696,8 +1695,7 @@ impl core::fmt::Display for PosixJulianLeapError { f.write_str("invalid zero-based Julian day digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed zero-based Julian day, but it's not in supported \ range of `0..=365`", ), @@ -1928,8 +1926,7 @@ impl core::fmt::Display for MonthError { f.write_str("invalid month digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed month, but it's not in supported \ range of `1..=12`", ), @@ -1951,8 +1948,7 @@ impl core::fmt::Display for WeekOfMonthError { f.write_str("invalid week-of-month digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed week-of-month, but it's not in supported \ range of `1..=5`", ), @@ -1974,8 +1970,7 @@ impl core::fmt::Display for WeekdayError { f.write_str("invalid weekday digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed weekday, but it's not in supported \ range of `0..=6` (with `0` corresponding to Sunday)", ), @@ -1997,8 +1992,7 @@ impl core::fmt::Display for HourIanaError { f.write_str("invalid hour digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed hours, but it's not in supported \ range of `-167..=167`", ), @@ -2020,8 +2014,7 @@ impl core::fmt::Display for HourPosixError { f.write_str("invalid hour digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed hours, but it's not in supported \ range of `0..=24`", ), @@ -2043,8 +2036,7 @@ impl core::fmt::Display for MinuteError { f.write_str("invalid minute digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed minutes, but it's not in supported \ range of `0..=59`", ), @@ -2066,8 +2058,7 @@ impl core::fmt::Display for SecondError { f.write_str("invalid second digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed seconds, but it's not in supported \ range of `0..=59`", ), diff --git a/src/fmt/offset.rs b/src/fmt/offset.rs index b73e11f..f918098 100644 --- a/src/fmt/offset.rs +++ b/src/fmt/offset.rs @@ -248,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)?; @@ -262,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(()) } diff --git a/src/shared/posix.rs b/src/shared/posix.rs index 55c9de6..62c58ef 100644 --- a/src/shared/posix.rs +++ b/src/shared/posix.rs @@ -1683,8 +1683,7 @@ impl core::fmt::Display for PosixJulianNoLeapError { f.write_str("invalid one-based Julian day digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed one-based Julian day, but it's not in supported \ range of `1..=365`", ), @@ -1706,8 +1705,7 @@ impl core::fmt::Display for PosixJulianLeapError { f.write_str("invalid zero-based Julian day digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed zero-based Julian day, but it's not in supported \ range of `0..=365`", ), @@ -1938,8 +1936,7 @@ impl core::fmt::Display for MonthError { f.write_str("invalid month digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed month, but it's not in supported \ range of `1..=12`", ), @@ -1961,8 +1958,7 @@ impl core::fmt::Display for WeekOfMonthError { f.write_str("invalid week-of-month digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed week-of-month, but it's not in supported \ range of `1..=5`", ), @@ -1984,8 +1980,7 @@ impl core::fmt::Display for WeekdayError { f.write_str("invalid weekday digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed weekday, but it's not in supported \ range of `0..=6` (with `0` corresponding to Sunday)", ), @@ -2007,8 +2002,7 @@ impl core::fmt::Display for HourIanaError { f.write_str("invalid hour digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed hours, but it's not in supported \ range of `-167..=167`", ), @@ -2030,8 +2024,7 @@ impl core::fmt::Display for HourPosixError { f.write_str("invalid hour digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed hours, but it's not in supported \ range of `0..=24`", ), @@ -2053,8 +2046,7 @@ impl core::fmt::Display for MinuteError { f.write_str("invalid minute digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed minutes, but it's not in supported \ range of `0..=59`", ), @@ -2076,8 +2068,7 @@ impl core::fmt::Display for SecondError { f.write_str("invalid second digits: ")?; core::fmt::Display::fmt(err, f) } - Range => write!( - f, + Range => f.write_str( "parsed seconds, but it's not in supported \ range of `0..=59`", ), diff --git a/src/signed_duration.rs b/src/signed_duration.rs index c7c473a..852ea93 100644 --- a/src/signed_duration.rs +++ b/src/signed_duration.rs @@ -2347,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 diff --git a/src/span.rs b/src/span.rs index f79d74d..48e9826 100644 --- a/src/span.rs +++ b/src/span.rs @@ -3256,7 +3256,7 @@ impl Span { if self.nanoseconds != C(0) { write!(buf, ", nanoseconds: {:?}", self.nanoseconds).unwrap(); } - write!(buf, " }}").unwrap(); + buf.push_str(" }}"); buf } diff --git a/src/tz/db/mod.rs b/src/tz/db/mod.rs index 8973cfe..8c004d3 100644 --- a/src/tz/db/mod.rs +++ b/src/tz/db/mod.rs @@ -560,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(")") } } @@ -675,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/enabled.rs b/src/tz/db/zoneinfo/enabled.rs index d48c6f1..f4237d4 100644 --- a/src/tz/db/zoneinfo/enabled.rs +++ b/src/tz/db/zoneinfo/enabled.rs @@ -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(")") } } diff --git a/src/tz/offset.rs b/src/tz/offset.rs index 9c28ff5..f081cf8 100644 --- a/src/tz/offset.rs +++ b/src/tz/offset.rs @@ -1110,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 { diff --git a/src/tz/timezone.rs b/src/tz/timezone.rs index ed54336..297ecff 100644 --- a/src/tz/timezone.rs +++ b/src/tz/timezone.rs @@ -2269,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"); @@ -2282,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 ae3e032..4271ff5 100644 --- a/src/tz/tzif.rs +++ b/src/tz/tzif.rs @@ -570,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/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 dd0736d..8aec114 100644 --- a/src/util/escape.rs +++ b/src/util/escape.rs @@ -18,7 +18,7 @@ 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, " "); + return f.write_str(" "); } // 10 bytes is enough for any output from ascii::escape_default. let mut bytes = [0u8; 10]; @@ -31,16 +31,16 @@ impl core::fmt::Display for Byte { bytes[len] = b; len += 1; } - write!(f, "{}", core::str::from_utf8(&bytes[..len]).unwrap()) + 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 { - write!(f, "\"")?; + f.write_str("\"")?; core::fmt::Display::fmt(self, f)?; - write!(f, "\"")?; + f.write_str("\"")?; Ok(()) } } @@ -70,11 +70,13 @@ impl<'a> core::fmt::Display for Bytes<'a> { }; bytes = &bytes[ch.len_utf8()..]; match ch { - '\0' => write!(f, "\\0")?, + '\0' => f.write_str(r"\0")?, '\x01'..='\x7f' => { - write!(f, "{}", (ch as u8).escape_ascii())?; + core::fmt::Display::fmt(&(ch as u8).escape_ascii(), f)?; + } + _ => { + core::fmt::Display::fmt(&ch.escape_debug(), f)?; } - _ => write!(f, "{}", ch.escape_debug())?, } } Ok(()) @@ -84,9 +86,9 @@ 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, "\"")?; + f.write_str("\"")?; core::fmt::Display::fmt(self, f)?; - write!(f, "\"")?; + f.write_str("\"")?; Ok(()) } } @@ -105,7 +107,7 @@ 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))?; + core::fmt::Display::fmt(&Byte(self.byte), f)?; } Ok(()) } @@ -114,9 +116,9 @@ impl core::fmt::Display for RepeatByte { impl core::fmt::Debug for RepeatByte { #[inline(never)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "\"")?; + f.write_str("\"")?; core::fmt::Display::fmt(self, f)?; - write!(f, "\"")?; + f.write_str("\"")?; Ok(()) } } diff --git a/src/util/rangeint.rs b/src/util/rangeint.rs index 2884bb1..82bf3a4 100644 --- a/src/util/rangeint.rs +++ b/src/util/rangeint.rs @@ -2093,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), } } } From 5b27b22096a39e1b022ef362c17b777f41688600 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 20:02:11 -0500 Subject: [PATCH 28/30] fmt: simplify decimal formatter I don't think we need to keep track of maximum digits separately for signed and unsigned integer formatting. I think this might mean that signed integer formatting uses an extra byte of stack space, but, it can also have a sign. Unlike unsigned integer formatting. So no space is wasted here. We also remove the `force_sign` option, which Jiff does not use. And overall simplify the signed integer formatting to just reuse the unsigned integer formatting. And also get rid of the dumb `checked_abs()` usage. Not sure what I was thinking. --- src/fmt/util.rs | 130 ++++++------------------------------------------ 1 file changed, 16 insertions(+), 114 deletions(-) diff --git a/src/fmt/util.rs b/src/fmt/util.rs index fe214de..fc0ddbf 100644 --- a/src/fmt/util.rs +++ b/src/fmt/util.rs @@ -14,11 +14,9 @@ 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, minimum_digits: u8, padding_byte: u8, } @@ -26,11 +24,7 @@ pub(crate) struct DecimalFormatter { 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', - } + DecimalFormatter { minimum_digits: 0, padding_byte: b'0' } } /// Format the given value using this configuration as a signed decimal @@ -47,21 +41,6 @@ impl DecimalFormatter { Decimal::unsigned(self, value) } - /// Forces the sign to be rendered, even if it's positive. - /// - /// When `zero_is_positive` is true, then a zero value is formatted with a - /// positive sign. Otherwise, it is formatted with a negative sign. - /// - /// Regardless of this setting, a sign is never emitted when formatting an - /// unsigned integer. - #[cfg(test)] - pub(crate) const fn force_sign( - self, - zero_is_positive: bool, - ) -> DecimalFormatter { - DecimalFormatter { force_sign: Some(zero_is_positive), ..self } - } - /// The minimum number of digits/padding that this number should be /// formatted with. If the number would have fewer digits than this, then /// it is padded out with the padding byte (which is zero by default) until @@ -70,8 +49,8 @@ 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; + if digits > Decimal::MAX_LEN { + digits = Decimal::MAX_LEN; } DecimalFormatter { minimum_digits: digits, ..self } } @@ -83,21 +62,12 @@ impl DecimalFormatter { DecimalFormatter { 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 <= Decimal::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 + Decimal::MAX_LEN } } } @@ -120,10 +90,6 @@ impl Decimal { /// Discovered via /// `i64::MIN.to_string().len().max(u64::MAX.to_string().len())`. const MAX_LEN: u8 = 20; - /// Discovered via `i64::MAX.to_string().len()`. - const MAX_I64_DIGITS: u8 = 19; - /// Discovered via `u64::MAX.to_string().len()`. - const MAX_U64_DIGITS: u8 = 20; /// Using the given formatter, turn the value given into an unsigned /// decimal representation using ASCII bytes. @@ -148,7 +114,7 @@ impl Decimal { } } - while decimal.len() < formatter.get_unsigned_minimum_digits() { + while decimal.len() < formatter.get_minimum_digits() { decimal.start -= 1; decimal.buf[decimal.start as usize] = formatter.padding_byte; } @@ -158,30 +124,10 @@ impl Decimal { /// Using the given formatter, turn the value given into a signed decimal /// representation using ASCII bytes. #[cfg_attr(feature = "perf-inline", inline(always))] - const fn signed(formatter: &DecimalFormatter, mut value: i64) -> Decimal { + const fn signed(formatter: &DecimalFormatter, value: i64) -> Decimal { // 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 Decimal::unsigned(formatter, value.unsigned_abs()); } Decimal::signed_cold(formatter, value) } @@ -189,41 +135,10 @@ impl Decimal { #[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; - } - } - while decimal.len() < formatter.get_signed_minimum_digits() { - decimal.start -= 1; - decimal.buf[decimal.start as usize] = formatter.padding_byte; - } - if sign < 0 { + let mut decimal = Decimal::unsigned(formatter, value.unsigned_abs()); + if value < 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 } @@ -1410,19 +1325,9 @@ mod tests { let x = DecimalFormatter::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); 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); assert_eq!(x.as_str(), "0000"); @@ -1432,11 +1337,8 @@ mod tests { let x = DecimalFormatter::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 = DecimalFormatter::new().padding(4).format_signed(789); + assert_eq!(x.as_str(), "0789"); } #[test] From 1467f47e36ea69c6db079df4e7233943225a348d Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 20:44:21 -0500 Subject: [PATCH 29/30] fmt: get rid of `Decimal::end` It's not used. I believe it was being used when `Decimal` was also responsible for formatting the fractional part of a floating point number. But it isn't needed any more. --- src/fmt/util.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/fmt/util.rs b/src/fmt/util.rs index fc0ddbf..a51e39e 100644 --- a/src/fmt/util.rs +++ b/src/fmt/util.rs @@ -83,7 +83,6 @@ impl Default for DecimalFormatter { pub(crate) struct Decimal { buf: [u8; Self::MAX_LEN as usize], start: u8, - end: u8, } impl Decimal { @@ -98,11 +97,8 @@ impl Decimal { formatter: &DecimalFormatter, mut value: u64, ) -> Decimal { - let mut decimal = Decimal { - buf: [0; Self::MAX_LEN as usize], - start: Self::MAX_LEN, - end: Self::MAX_LEN, - }; + let mut decimal = + Decimal { buf: [0; Self::MAX_LEN as usize], start: Self::MAX_LEN }; loop { decimal.start -= 1; @@ -147,7 +143,7 @@ impl Decimal { /// used to represent this decimal 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. @@ -155,7 +151,7 @@ impl Decimal { /// 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. From 61dc9bd8aa1cec759a35983e2f5984f47e5d8bf7 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 22 Dec 2025 20:46:31 -0500 Subject: [PATCH 30/30] fmt: rename `Decimal` to `Integer` And similarly for `DecimalFormatter`. I believe I originally called this `Decimal` because it was also used to do fractional formatting. --- src/fmt/friendly/printer.rs | 16 ++--- src/fmt/mod.rs | 8 +-- src/fmt/rfc2822.rs | 18 +++--- src/fmt/strtime/format.rs | 6 +- src/fmt/temporal/printer.rs | 36 ++++++------ src/fmt/util.rs | 114 ++++++++++++++++++------------------ 6 files changed, 99 insertions(+), 99 deletions(-) diff --git a/src/fmt/friendly/printer.rs b/src/fmt/friendly/printer.rs index f61bf4c..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, @@ -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(":")?; @@ -1488,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(); @@ -1618,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, } @@ -1633,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 { @@ -1733,7 +1733,7 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> { struct FractionalPrinter { integer: u64, fraction: u32, - fmtint: DecimalFormatter, + fmtint: IntegerFormatter, fmtfraction: FractionalFormatter, } @@ -1750,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)); @@ -1762,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 e32b724..ec247bb 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -170,7 +170,7 @@ use crate::{ util::escape, }; -use self::util::{Decimal, DecimalFormatter, Fractional, FractionalFormatter}; +use self::util::{Fractional, FractionalFormatter, Integer, IntegerFormatter}; pub mod friendly; mod offset; @@ -434,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())) @@ -445,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())) @@ -464,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/rfc2822.rs b/src/fmt/rfc2822.rs index 36dd7f0..d199a81 100644 --- a/src/fmt/rfc2822.rs +++ b/src/fmt/rfc2822.rs @@ -44,7 +44,7 @@ general interchange format for new applications. use crate::{ civil::{Date, DateTime, Time, Weekday}, error::{fmt::rfc2822::Error as E, ErrorContext}, - fmt::{util::DecimalFormatter, Parsed, Write, WriteExt}, + fmt::{util::IntegerFormatter, Parsed, Write, WriteExt}, tz::{Offset, TimeZone}, util::{ parse, @@ -1292,10 +1292,10 @@ 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 @@ -1349,10 +1349,10 @@ 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 { diff --git a/src/fmt/strtime/format.rs b/src/fmt/strtime/format.rs index ab7c981..74cb1a8 100644 --- a/src/fmt/strtime/format.rs +++ b/src/fmt/strtime/format.rs @@ -9,7 +9,7 @@ use crate::{ weekday_name_full, BrokenDownTime, Config, Custom, Extension, Flag, }, - util::{DecimalFormatter, FractionalFormatter}, + util::{FractionalFormatter, IntegerFormatter}, Write, WriteExt, }, tz::Offset, @@ -850,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(); @@ -946,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); } diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs index dbd98e9..b01c332 100644 --- a/src/fmt/temporal/printer.rs +++ b/src/fmt/temporal/printer.rs @@ -3,7 +3,7 @@ use crate::{ 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())?; @@ -254,12 +254,12 @@ impl DateTimePrinter { iso_week_date: &ISOWeekDate, 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_ONE: DecimalFormatter = DecimalFormatter::new().padding(1); + 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())?; @@ -304,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(); @@ -339,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(); @@ -427,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() { @@ -541,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; @@ -590,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; diff --git a/src/fmt/util.rs b/src/fmt/util.rs index a51e39e..23662ea 100644 --- a/src/fmt/util.rs +++ b/src/fmt/util.rs @@ -16,29 +16,29 @@ use crate::{ /// /// This only includes the sign when formatting a negative signed integer. #[derive(Clone, Copy, Debug)] -pub(crate) struct DecimalFormatter { +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 { 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) + 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 @@ -48,105 +48,105 @@ 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_LEN { - digits = Decimal::MAX_LEN; + 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 an integer value. const fn get_minimum_digits(&self) -> u8 { - if self.minimum_digits <= Decimal::MAX_LEN { + if self.minimum_digits <= Integer::MAX_LEN { self.minimum_digits } else { - Decimal::MAX_LEN + 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, } -impl Decimal { +impl Integer { /// Discovered via /// `i64::MIN.to_string().len().max(u64::MAX.to_string().len())`. const MAX_LEN: 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 }; + ) -> 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_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, value: i64) -> Decimal { + const fn signed(formatter: &IntegerFormatter, value: i64) -> Integer { // Specialize the common case to generate tighter codegen. if value >= 0 { - return Decimal::unsigned(formatter, value.unsigned_abs()); + 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 mut decimal = Decimal::unsigned(formatter, value.unsigned_abs()); + const fn signed_cold(formatter: &IntegerFormatter, value: i64) -> Integer { + let mut integer = Integer::unsigned(formatter, value.unsigned_abs()); if value < 0 { - decimal.start -= 1; - decimal.buf[decimal.start as usize] = b'-'; + integer.start -= 1; + integer.buf[integer.start as usize] = b'-'; } - 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::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] @@ -154,7 +154,7 @@ impl Decimal { &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 @@ -1311,29 +1311,29 @@ 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().format_signed(0); + let x = IntegerFormatter::new().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().padding(4).format_signed(789); + let x = IntegerFormatter::new().padding(4).format_signed(789); assert_eq!(x.as_str(), "0789"); }