Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions crates/jiff-static/src/shared/util/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, " ");
Expand All @@ -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)?;
Expand All @@ -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;
}
Expand All @@ -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)?;
Expand Down
64 changes: 58 additions & 6 deletions crates/jiff-static/src/shared/util/utf8.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Result<char, &[u8]>> {
pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, Utf8Error>> {
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
Expand Down
22 changes: 7 additions & 15 deletions src/civil/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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));
Expand Down
70 changes: 26 additions & 44 deletions src/civil/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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),
Expand All @@ -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))
}

Expand All @@ -1751,13 +1738,9 @@ impl DateTime {
) -> Result<DateTime, Error> {
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))
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
}

Expand Down
6 changes: 2 additions & 4 deletions src/civil/iso_week_date.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading