Skip to content

Commit 0a2a4cd

Browse files
authored
Throw error in Date::try_from_fields for very large year values. (#7062)
Fixes #7049 I put some effort into making sure that type errors are returned before range errors, which makes it easier for Temporal to use this directly (otherwise it would have to hoist type errors out to be 100% spec compliant). After discussion with @sffc, I decided not to have this check everywhere: only `from_fields` and `from_codes` have this check; calendar-specific constructors do not, and constructing from RataDie does not either. This is fine, it lets people stretch calendars to their limits if they really want.
1 parent c3c3a96 commit 0a2a4cd

File tree

4 files changed

+129
-44
lines changed

4 files changed

+129
-44
lines changed

components/calendar/src/cal/chinese.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ mod test {
10681068
use crate::types::DateFields;
10691069
use crate::types::MonthCode;
10701070
use calendrical_calculations::{gregorian::fixed_from_gregorian, rata_die::RataDie};
1071+
use core::num::NonZero;
10711072
use std::collections::BTreeMap;
10721073
use tinystr::tinystr;
10731074

@@ -1639,7 +1640,7 @@ mod test {
16391640
#[test]
16401641
fn test_from_fields_constrain() {
16411642
let fields = DateFields {
1642-
day: core::num::NonZero::new(31),
1643+
day: NonZero::new(31),
16431644
month_code: Some(MonthCode("M01".parse().unwrap())),
16441645
extended_year: Some(1972),
16451646
..Default::default()
@@ -1659,7 +1660,7 @@ mod test {
16591660

16601661
// 2022 did not have M01L, the month should be constrained back down
16611662
let fields = DateFields {
1662-
day: core::num::NonZero::new(1),
1663+
day: NonZero::new(1),
16631664
month_code: Some(MonthCode("M01L".parse().unwrap())),
16641665
extended_year: Some(2022),
16651666
..Default::default()
@@ -1672,6 +1673,28 @@ mod test {
16721673
);
16731674
}
16741675

1676+
#[test]
1677+
fn test_from_fields_regress_7049() {
1678+
// We want to make sure that overly large years do not panic
1679+
// (we just reject them in Date::try_from_fields)
1680+
let fields = DateFields {
1681+
extended_year: Some(889192448),
1682+
ordinal_month: NonZero::new(1),
1683+
day: NonZero::new(1),
1684+
..Default::default()
1685+
};
1686+
let options = DateFromFieldsOptions {
1687+
overflow: Some(Overflow::Reject),
1688+
..Default::default()
1689+
};
1690+
1691+
let cal = LunarChinese::new_china();
1692+
assert!(matches!(
1693+
Date::try_from_fields(fields, options, cal).unwrap_err(),
1694+
DateError::Range { .. }
1695+
));
1696+
}
1697+
16751698
#[test]
16761699
#[ignore] // slow, network
16771700
fn test_against_hong_kong_observatory_data() {

components/calendar/src/calendar_arithmetic.rs

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// called LICENSE at the top level of the ICU4X source tree
33
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
44

5-
use crate::error::range_check_with_overflow;
5+
use crate::error::{range_check, range_check_with_overflow};
66
use crate::options::{DateAddOptions, DateDifferenceOptions};
77
use crate::options::{DateFromFieldsOptions, MissingFieldsStrategy, Overflow};
88
use crate::types::{DateDuration, DateDurationUnit, DateFields, DayOfYear, MonthCode};
@@ -12,8 +12,12 @@ use core::convert::TryInto;
1212
use core::fmt::Debug;
1313
use core::hash::{Hash, Hasher};
1414
use core::marker::PhantomData;
15+
use core::ops::RangeInclusive;
1516
use tinystr::tinystr;
1617

18+
/// The range ±2²⁷. We use i32::MIN since it is -2³¹
19+
const VALID_YEAR_RANGE: RangeInclusive<i32> = (i32::MIN / 16)..=-(i32::MIN / 16);
20+
1721
#[derive(Debug)]
1822
#[allow(clippy::exhaustive_structs)] // this type is stable
1923
pub(crate) struct ArithmeticDate<C: CalendarArithmetic> {
@@ -641,6 +645,23 @@ pub(crate) struct ArithmeticDateBuilder<YearInfo> {
641645
pub(crate) day: u8,
642646
}
643647

648+
fn extended_year_as_year_info<YearInfo, C>(
649+
extended_year: i32,
650+
cal: &C,
651+
) -> Result<YearInfo, DateError>
652+
where
653+
C: DateFieldsResolver<YearInfo = YearInfo>,
654+
{
655+
// Check that the year is in range to avoid any arithmetic overflow.
656+
//
657+
// This range is currently global, but may be replaced with
658+
// a per-calendar check in the future.
659+
//
660+
// <https://github.com/unicode-org/icu4x/issues/7076>
661+
range_check(extended_year, "year", VALID_YEAR_RANGE)?;
662+
Ok(cal.year_info_from_extended(extended_year))
663+
}
664+
644665
impl<YearInfo> ArithmeticDateBuilder<YearInfo>
645666
where
646667
YearInfo: PartialEq + Debug,
@@ -654,49 +675,69 @@ where
654675
C: DateFieldsResolver<YearInfo = YearInfo>,
655676
{
656677
let missing_fields_strategy = options.missing_fields_strategy.unwrap_or_default();
657-
let maybe_year = {
658-
let extended_year_as_year_info = fields
659-
.extended_year
660-
.map(|extended_year| cal.year_info_from_extended(extended_year));
678+
679+
let day = match fields.day {
680+
Some(day) => day.get(),
681+
None => match missing_fields_strategy {
682+
MissingFieldsStrategy::Reject => return Err(DateError::NotEnoughFields),
683+
MissingFieldsStrategy::Ecma => {
684+
if fields.extended_year.is_some() || fields.era_year.is_some() {
685+
// The ECMAScript strategy is to pick day 1, always, regardless of whether
686+
// that day exists for the month/year combo
687+
1
688+
} else {
689+
return Err(DateError::NotEnoughFields);
690+
}
691+
}
692+
},
693+
};
694+
695+
if fields.month_code.is_none() && fields.ordinal_month.is_none() {
696+
// We're returning this error early so that we return structural type
697+
// errors before range errors, see comment in the year code below.
698+
return Err(DateError::NotEnoughFields);
699+
}
700+
701+
let year = {
702+
// NOTE: The year/extendedyear range check is important to avoid arithmetic
703+
// overflow in `year_info_from_era` and `year_info_from_extended`. It
704+
// must happen before they are called.
705+
//
706+
// To better match the Temporal specification's order of operations, we try
707+
// to return structural type errors (`NotEnoughFields`) before checking for range errors.
708+
// This isn't behavior we *must* have, but it is not much additional work to maintain
709+
// so we make an attempt.
661710
match (fields.era, fields.era_year) {
662-
(None, None) => extended_year_as_year_info,
711+
(None, None) => match fields.extended_year {
712+
Some(extended_year) => extended_year_as_year_info(extended_year, cal)?,
713+
None => match missing_fields_strategy {
714+
MissingFieldsStrategy::Reject => return Err(DateError::NotEnoughFields),
715+
MissingFieldsStrategy::Ecma => {
716+
match (fields.month_code, fields.ordinal_month) {
717+
(Some(month_code), None) => {
718+
cal.reference_year_from_month_day(month_code, day)?
719+
}
720+
_ => return Err(DateError::NotEnoughFields),
721+
}
722+
}
723+
},
724+
},
663725
(Some(era), Some(era_year)) => {
726+
range_check(era_year, "year", VALID_YEAR_RANGE)?;
664727
let era_year_as_year_info = cal.year_info_from_era(era, era_year)?;
665-
if let Some(other) = extended_year_as_year_info {
666-
if era_year_as_year_info != other {
728+
if let Some(extended_year) = fields.extended_year {
729+
if era_year_as_year_info != extended_year_as_year_info(extended_year, cal)?
730+
{
667731
return Err(DateError::InconsistentYear);
668732
}
669733
}
670-
Some(era_year_as_year_info)
734+
era_year_as_year_info
671735
}
672736
// Era and Era Year must be both or neither
673737
(Some(_), None) | (None, Some(_)) => return Err(DateError::NotEnoughFields),
674738
}
675739
};
676-
let day = match fields.day {
677-
Some(day) => day.get(),
678-
None => match missing_fields_strategy {
679-
MissingFieldsStrategy::Reject => return Err(DateError::NotEnoughFields),
680-
MissingFieldsStrategy::Ecma => match maybe_year {
681-
// The ECMAScript strategy is to pick day 1, always, regardless of whether
682-
// that day exists for the month/year combo
683-
Some(_) => 1,
684-
None => return Err(DateError::NotEnoughFields),
685-
},
686-
},
687-
};
688-
let year = match maybe_year {
689-
Some(year) => year,
690-
None => match missing_fields_strategy {
691-
MissingFieldsStrategy::Reject => return Err(DateError::NotEnoughFields),
692-
MissingFieldsStrategy::Ecma => match (fields.month_code, fields.ordinal_month) {
693-
(Some(month_code), None) => {
694-
cal.reference_year_from_month_day(month_code, day)?
695-
}
696-
_ => return Err(DateError::NotEnoughFields),
697-
},
698-
},
699-
};
740+
700741
let month = {
701742
let ordinal_month_as_u8 = fields.ordinal_month.map(|x| x.get());
702743
match fields.month_code {
@@ -711,6 +752,7 @@ where
711752
}
712753
None => match ordinal_month_as_u8 {
713754
Some(month) => month,
755+
// This is technically unreachable since it's checked early above
714756
None => return Err(DateError::NotEnoughFields),
715757
},
716758
}

components/calendar/src/date.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,11 @@ pub struct Date<A: AsCalendar> {
124124
impl<A: AsCalendar> Date<A> {
125125
/// Construct a date from from era/month codes and fields, and some calendar representation
126126
///
127-
/// The year is `extended_year` if no era is provided
127+
/// The year is `extended_year` if no era is provided.
128+
///
129+
/// This function will not accept year/extended_year values that are outside of the range `[-2²⁷, 2²⁷]`,
130+
/// regardless of the calendar, instead returning a [`DateError::Range`]. See [`Date::try_from_fields()`] for more
131+
/// information.
128132
#[inline]
129133
pub fn try_new_from_codes(
130134
era: Option<&str>,
@@ -145,6 +149,13 @@ impl<A: AsCalendar> Date<A> {
145149
/// and the month as either ordinal or month code. It can constrain out-of-bounds values
146150
/// and fill in missing fields. See [`DateFromFieldsOptions`] for more information.
147151
///
152+
/// This function will not accept year/extended_year values that are outside of the range `[-2²⁷, 2²⁷]`,
153+
/// regardless of the calendar, instead returning a [`DateError::Range`]. This is to prevent
154+
/// overflowing behaviors near the extreme values of the `i32` range.
155+
/// Currently, calendar-specific `Date::try_new_calendarname()` constructors
156+
/// do not do this, and it is possible to obtain such extreme dates via calendar conversion or arithmetic,
157+
/// though [we may change that behavior in the future](https://github.com/unicode-org/icu4x/issues/7076).
158+
///
148159
/// # Examples
149160
///
150161
/// ```

components/calendar/src/error.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,24 @@ pub(crate) fn range_check_with_overflow<T: Ord + Into<i32> + Copy>(
260260
if matches!(overflow, Overflow::Constrain) {
261261
Ok(value.clamp(*bounds.start(), *bounds.end()))
262262
} else {
263-
if !bounds.contains(&value) {
264-
return Err(RangeError {
265-
field,
266-
value: value.into(),
267-
min: (*bounds.start()).into(),
268-
max: (*bounds.end()).into(),
269-
});
270-
}
263+
range_check(value, field, bounds)?;
271264

272265
Ok(value)
273266
}
274267
}
268+
269+
pub(crate) fn range_check<T: Ord + Into<i32> + Copy>(
270+
value: T,
271+
field: &'static str,
272+
bounds: core::ops::RangeInclusive<T>,
273+
) -> Result<(), RangeError> {
274+
if !bounds.contains(&value) {
275+
return Err(RangeError {
276+
field,
277+
value: value.into(),
278+
min: (*bounds.start()).into(),
279+
max: (*bounds.end()).into(),
280+
});
281+
}
282+
Ok(())
283+
}

0 commit comments

Comments
 (0)