Skip to content

Allow parsers to accept unvalidated UTF8 #295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 10, 2025
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
29 changes: 17 additions & 12 deletions src/builtins/core/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,22 @@ impl PlainDate {
partial.calendar.date_from_partial(&partial, overflow)
}

// Converts a UTF-8 encoded string into a `PlainDate`.
pub fn from_utf8(s: &[u8]) -> TemporalResult<Self> {
let parse_record = parse_date_time(s)?;

let calendar = parse_record
.calendar
.map(Calendar::try_from_utf8)
.transpose()?
.unwrap_or_default();

// Assertion: PlainDate must exist on a DateTime parse.
let date = parse_record.date.temporal_unwrap()?;

Self::try_new(date.year, date.month, date.day, calendar)
}

/// Creates a date time with values from a `PartialDate`.
pub fn with(
&self,
Expand Down Expand Up @@ -726,18 +742,7 @@ impl FromStr for PlainDate {
type Err = TemporalError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse_record = parse_date_time(s)?;

let calendar = parse_record
.calendar
.map(Calendar::try_from_utf8)
.transpose()?
.unwrap_or_default();

// Assertion: PlainDate must exist on a DateTime parse.
let date = parse_record.date.temporal_unwrap()?;

Self::try_new(date.year, date.month, date.day, calendar)
Self::from_utf8(s.as_bytes())
}
}

Expand Down
53 changes: 29 additions & 24 deletions src/builtins/core/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,34 @@ impl PlainDateTime {
Self::from_date_and_time(date, PlainTime::new_unchecked(iso_time))
}

// Converts a UTF-8 encoded string into a `PlainDateTime`.
pub fn from_utf8(s: &[u8]) -> TemporalResult<Self> {
let parse_record = parse_date_time(s)?;

let calendar = parse_record
.calendar
.map(Calendar::try_from_utf8)
.transpose()?
.unwrap_or_default();

let time = parse_record
.time
.map(IsoTime::from_time_record)
.transpose()?
.unwrap_or_default();

let parsed_date = parse_record.date.temporal_unwrap()?;

let date = IsoDate::new_with_overflow(
parsed_date.year,
parsed_date.month,
parsed_date.day,
ArithmeticOverflow::Reject,
)?;

Ok(Self::new_unchecked(IsoDateTime::new(date, time)?, calendar))
}

/// Creates a new `DateTime` with the fields of a `PartialDateTime`.
///
/// ```rust
Expand Down Expand Up @@ -816,30 +844,7 @@ impl FromStr for PlainDateTime {
type Err = TemporalError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse_record = parse_date_time(s)?;

let calendar = parse_record
.calendar
.map(Calendar::try_from_utf8)
.transpose()?
.unwrap_or_default();

let time = parse_record
.time
.map(IsoTime::from_time_record)
.transpose()?
.unwrap_or_default();

let parsed_date = parse_record.date.temporal_unwrap()?;

let date = IsoDate::new_with_overflow(
parsed_date.year,
parsed_date.month,
parsed_date.day,
ArithmeticOverflow::Reject,
)?;

Ok(Self::new_unchecked(IsoDateTime::new(date, time)?, calendar))
Self::from_utf8(s.as_bytes())
}
}

Expand Down
107 changes: 107 additions & 0 deletions src/builtins/core/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,113 @@ impl Duration {
)
}

// Converts a UTF-8 encoded string into a `Duration`.
pub fn from_utf8(s: &[u8]) -> TemporalResult<Self> {
let parse_record = IsoDurationParser::from_utf8(s)
.parse()
.map_err(|e| TemporalError::range().with_message(format!("{e}")))?;

let (hours, minutes, seconds, millis, micros, nanos) = match parse_record.time {
Some(TimeDurationRecord::Hours { hours, fraction }) => {
let unadjusted_fraction =
fraction.and_then(|x| x.to_nanoseconds()).unwrap_or(0) as u64;
let fractional_hours_ns = unadjusted_fraction * 3600;
let minutes = fractional_hours_ns.div_euclid(60 * 1_000_000_000);
let fractional_minutes_ns = fractional_hours_ns.rem_euclid(60 * 1_000_000_000);

let seconds = fractional_minutes_ns.div_euclid(1_000_000_000);
let fractional_seconds = fractional_minutes_ns.rem_euclid(1_000_000_000);

let milliseconds = fractional_seconds.div_euclid(1_000_000);
let rem = fractional_seconds.rem_euclid(1_000_000);

let microseconds = rem.div_euclid(1_000);
let nanoseconds = rem.rem_euclid(1_000);

(
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
)
}
// Minutes variant is defined as { hours: u32, minutes: u32, fraction: u64 }
Some(TimeDurationRecord::Minutes {
hours,
minutes,
fraction,
}) => {
let unadjusted_fraction =
fraction.and_then(|x| x.to_nanoseconds()).unwrap_or(0) as u64;
let fractional_minutes_ns = unadjusted_fraction * 60;
let seconds = fractional_minutes_ns.div_euclid(1_000_000_000);
let fractional_seconds = fractional_minutes_ns.rem_euclid(1_000_000_000);

let milliseconds = fractional_seconds.div_euclid(1_000_000);
let rem = fractional_seconds.rem_euclid(1_000_000);

let microseconds = rem.div_euclid(1_000);
let nanoseconds = rem.rem_euclid(1_000);

(
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
)
}
// Seconds variant is defined as { hours: u32, minutes: u32, seconds: u32, fraction: u32 }
Some(TimeDurationRecord::Seconds {
hours,
minutes,
seconds,
fraction,
}) => {
let ns = fraction.and_then(|x| x.to_nanoseconds()).unwrap_or(0);
let milliseconds = ns.div_euclid(1_000_000);
let rem = ns.rem_euclid(1_000_000);

let microseconds = rem.div_euclid(1_000);
let nanoseconds = rem.rem_euclid(1_000);

(
hours,
minutes,
seconds,
milliseconds as u64,
microseconds as u64,
nanoseconds as u64,
)
}
None => (0, 0, 0, 0, 0, 0),
};

let (years, months, weeks, days) = if let Some(date) = parse_record.date {
(date.years, date.months, date.weeks, date.days)
} else {
(0, 0, 0, 0)
};

let sign = parse_record.sign as i64;

Self::new(
years as i64 * sign,
months as i64 * sign,
weeks as i64 * sign,
days as i64 * sign,
hours as i64 * sign,
minutes as i64 * sign,
seconds as i64 * sign,
millis as i64 * sign,
micros as i128 * sign as i128,
nanos as i128 * sign as i128,
)
}

/// Return if the Durations values are within their valid ranges.
#[inline]
#[must_use]
Expand Down
88 changes: 47 additions & 41 deletions src/builtins/core/instant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,51 @@ impl Instant {
Self::try_new(epoch_nanos)
}

// Converts a UTF-8 encoded string into a `Instant`.
pub fn from_utf8(s: &[u8]) -> TemporalResult<Self> {
let ixdtf_record = parse_instant(s)?;

// Find the offset
let ns_offset = match ixdtf_record.offset {
UtcOffsetRecordOrZ::Offset(offset) => {
let ns = offset
.fraction
.and_then(|x| x.to_nanoseconds())
.unwrap_or(0);
(offset.hour as i64 * NANOSECONDS_PER_HOUR
+ i64::from(offset.minute) * NANOSECONDS_PER_MINUTE
+ i64::from(offset.second) * NANOSECONDS_PER_SECOND
+ i64::from(ns))
* offset.sign as i64
}
UtcOffsetRecordOrZ::Z => 0,
};

let time_nanoseconds = ixdtf_record
.time
.fraction
.and_then(|x| x.to_nanoseconds())
.unwrap_or(0);
let (millisecond, rem) = time_nanoseconds.div_rem_euclid(&1_000_000);
let (microsecond, nanosecond) = rem.div_rem_euclid(&1_000);

let balanced = IsoDateTime::balance(
ixdtf_record.date.year,
ixdtf_record.date.month.into(),
ixdtf_record.date.day.into(),
ixdtf_record.time.hour.into(),
ixdtf_record.time.minute.into(),
ixdtf_record.time.second.clamp(0, 59).into(),
millisecond.into(),
microsecond.into(),
i128::from(nanosecond) - i128::from(ns_offset),
);

let nanoseconds = balanced.as_nanoseconds()?;

Ok(Self(nanoseconds))
}

/// Adds a `Duration` to the current `Instant`, returning an error if the `Duration`
/// contains a `DateDuration`.
#[inline]
Expand Down Expand Up @@ -274,48 +319,9 @@ impl Instant {

impl FromStr for Instant {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let ixdtf_record = parse_instant(s)?;

// Find the offset
let ns_offset = match ixdtf_record.offset {
UtcOffsetRecordOrZ::Offset(offset) => {
let ns = offset
.fraction
.and_then(|x| x.to_nanoseconds())
.unwrap_or(0);
(offset.hour as i64 * NANOSECONDS_PER_HOUR
+ i64::from(offset.minute) * NANOSECONDS_PER_MINUTE
+ i64::from(offset.second) * NANOSECONDS_PER_SECOND
+ i64::from(ns))
* offset.sign as i64
}
UtcOffsetRecordOrZ::Z => 0,
};

let time_nanoseconds = ixdtf_record
.time
.fraction
.and_then(|x| x.to_nanoseconds())
.unwrap_or(0);
let (millisecond, rem) = time_nanoseconds.div_rem_euclid(&1_000_000);
let (microsecond, nanosecond) = rem.div_rem_euclid(&1_000);

let balanced = IsoDateTime::balance(
ixdtf_record.date.year,
ixdtf_record.date.month.into(),
ixdtf_record.date.day.into(),
ixdtf_record.time.hour.into(),
ixdtf_record.time.minute.into(),
ixdtf_record.time.second.clamp(0, 59).into(),
millisecond.into(),
microsecond.into(),
i128::from(nanosecond) - i128::from(ns_offset),
);

let nanoseconds = balanced.as_nanoseconds()?;

Ok(Self(nanoseconds))
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_utf8(s.as_bytes())
}
}

Expand Down
Loading