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} ;
66use crate :: options:: { DateAddOptions , DateDifferenceOptions } ;
77use crate :: options:: { DateFromFieldsOptions , MissingFieldsStrategy , Overflow } ;
88use crate :: types:: { DateDuration , DateDurationUnit , DateFields , DayOfYear , MonthCode } ;
@@ -12,8 +12,12 @@ use core::convert::TryInto;
1212use core:: fmt:: Debug ;
1313use core:: hash:: { Hash , Hasher } ;
1414use core:: marker:: PhantomData ;
15+ use core:: ops:: RangeInclusive ;
1516use 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
1923pub ( 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+
644665impl < YearInfo > ArithmeticDateBuilder < YearInfo >
645666where
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 }
0 commit comments