From db68c125d4f690f176bbf6ad649733dc36a62db1 Mon Sep 17 00:00:00 2001 From: Jevan Chan Date: Wed, 20 Jan 2021 14:07:51 -0800 Subject: [PATCH] Fix out-of-range panics of Date (#1075) * Add overflow check when fixing year, month, and day * Add timeclip * Add timeclip to `Date.UTC()` * Minor fix * Fix comment * Add more range check * Add timeclip to all date creation * Remove redundant overflow check * Fix clippy --- boa/src/builtins/date/mod.rs | 139 +++++++++++++++++++++++------------ 1 file changed, 91 insertions(+), 48 deletions(-) diff --git a/boa/src/builtins/date/mod.rs b/boa/src/builtins/date/mod.rs index f19a2791ec..3f49aa6ae3 100644 --- a/boa/src/builtins/date/mod.rs +++ b/boa/src/builtins/date/mod.rs @@ -12,7 +12,14 @@ use crate::{ use chrono::{prelude::*, Duration, LocalResult}; use std::fmt::Display; -const NANOS_IN_MS: f64 = 1_000_000f64; +/// The number of nanoseconds in a millisecond. +const NANOS_PER_MS: i64 = 1_000_000; +/// The number of milliseconds in an hour. +const MILLIS_PER_HOUR: i64 = 3_600_000; +/// The number of milliseconds in a minute. +const MILLIS_PER_MINUTE: i64 = 60_000; +/// The number of milliseconds in a second. +const MILLIS_PER_SECOND: i64 = 1000; #[inline] fn is_zero_or_normal_opt(value: Option) -> bool { @@ -191,6 +198,22 @@ impl Date { /// The amount of arguments this function object takes. pub(crate) const LENGTH: usize = 7; + /// Check if the time (number of miliseconds) is in the expected range. + /// Returns None if the time is not in the range, otherwise returns the time itself in option. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-timeclip + #[inline] + pub fn time_clip(time: f64) -> Option { + if time.abs() > 8.64e15 { + None + } else { + Some(time) + } + } + /// Converts the `Date` to a local `DateTime`. /// /// If the `Date` is invalid (i.e. NAN), this function will return `None`. @@ -224,53 +247,64 @@ impl Date { millisecond: Option, ) { #[inline] - fn num_days_in(year: i32, month: u32) -> i32 { + fn num_days_in(year: i32, month: u32) -> Option { let month = month + 1; // zero-based for calculations - NaiveDate::from_ymd( - match month { - 12 => year + 1, - _ => year, - }, - match month { - 12 => 1, - _ => month + 1, - }, - 1, + + Some( + NaiveDate::from_ymd_opt( + match month { + 12 => year.checked_add(1)?, + _ => year, + }, + match month { + 12 => 1, + _ => month + 1, + }, + 1, + )? + .signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1)?) + .num_days() as u32, ) - .signed_duration_since(NaiveDate::from_ymd(year, month, 1)) - .num_days() as i32 } #[inline] - fn fix_month(year: &mut i32, month: &mut i32) { - *year += *month / 12; - *month = if *month < 0 { - *year -= 1; - 11 + (*month + 1) % 12 + fn fix_month(year: i32, month: i32) -> Option<(i32, u32)> { + let year = year.checked_add(month / 12)?; + + if month < 0 { + let year = year.checked_sub(1)?; + let month = (11 + (month + 1) % 12) as u32; + Some((year, month)) } else { - *month % 12 + let month = (month % 12) as u32; + Some((year, month)) } } #[inline] - fn fix_day(year: &mut i32, month: &mut i32, day: &mut i32) { - fix_month(year, month); + fn fix_day(mut year: i32, mut month: i32, mut day: i32) -> Option<(i32, u32, u32)> { loop { - if *day < 0 { - *month -= 1; - fix_month(year, month); - *day += num_days_in(*year, *month as u32); + if day < 0 { + let (fixed_year, fixed_month) = fix_month(year, month.checked_sub(1)?)?; + + year = fixed_year; + month = fixed_month as i32; + day += num_days_in(fixed_year, fixed_month)? as i32; } else { - let num_days = num_days_in(*year, *month as u32); - if *day >= num_days { - *day -= num_days_in(*year, *month as u32); - *month += 1; - fix_month(year, month); + let (fixed_year, fixed_month) = fix_month(year, month)?; + let num_days = num_days_in(fixed_year, fixed_month)? as i32; + + if day >= num_days { + day -= num_days; + month = month.checked_add(1)?; } else { break; } } } + + let (fixed_year, fixed_month) = fix_month(year, month)?; + Some((fixed_year, fixed_month, day as u32)) } // If any of the args are infinity or NaN, return an invalid date. @@ -286,22 +320,29 @@ impl Date { }; self.0 = naive.and_then(|naive| { - let mut year = year.unwrap_or_else(|| naive.year() as f64) as i32; - let mut month = month.unwrap_or_else(|| naive.month0() as f64) as i32; - let mut day = day.unwrap_or_else(|| naive.day() as f64) as i32 - 1; + let year = year.unwrap_or_else(|| naive.year() as f64) as i32; + let month = month.unwrap_or_else(|| naive.month0() as f64) as i32; + let day = (day.unwrap_or_else(|| naive.day() as f64) as i32).checked_sub(1)?; let hour = hour.unwrap_or_else(|| naive.hour() as f64) as i64; let minute = minute.unwrap_or_else(|| naive.minute() as f64) as i64; let second = second.unwrap_or_else(|| naive.second() as f64) as i64; - let millisecond = - millisecond.unwrap_or_else(|| naive.nanosecond() as f64 / NANOS_IN_MS) as i64; + let millisecond = millisecond + .unwrap_or_else(|| naive.nanosecond() as f64 / NANOS_PER_MS as f64) + as i64; + + let (year, month, day) = fix_day(year, month, day)?; + + let duration_hour = Duration::milliseconds(hour.checked_mul(MILLIS_PER_HOUR)?); + let duration_minute = Duration::milliseconds(minute.checked_mul(MILLIS_PER_MINUTE)?); + let duration_second = Duration::milliseconds(second.checked_mul(MILLIS_PER_SECOND)?); + let duration_milisecond = Duration::milliseconds(millisecond); - fix_day(&mut year, &mut month, &mut day); + let duration = duration_hour + .checked_add(&duration_minute)? + .checked_add(&duration_second)? + .checked_add(&duration_milisecond)?; - let duration = Duration::hours(hour) - + Duration::minutes(minute) - + Duration::seconds(second) - + Duration::milliseconds(millisecond); - NaiveDate::from_ymd_opt(year, month as u32 + 1, day as u32 + 1) + NaiveDate::from_ymd_opt(year, month + 1, day + 1) .and_then(|dt| dt.and_hms(0, 0, 0).checked_add_signed(duration)) .and_then(|dt| { if utc { @@ -310,6 +351,7 @@ impl Date { ignore_ambiguity(Local.from_local_datetime(&dt)).map(|dt| dt.naive_utc()) } }) + .filter(|dt| Self::time_clip(dt.timestamp_millis() as f64).is_some()) }); } @@ -415,6 +457,7 @@ impl Date { }, }; + let tv = tv.filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); let date = Date(tv); this.set_data(ObjectData::Date(date)); Ok(this.clone()) @@ -477,7 +520,8 @@ impl Date { let final_date = NaiveDate::from_ymd_opt(year, month + 1, day) .and_then(|naive_date| naive_date.and_hms_milli_opt(hour, min, sec, milli)) .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) - .map(|local| local.naive_utc()); + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); let date = Date(final_date); this.set_data(ObjectData::Date(date)); @@ -557,7 +601,7 @@ impl Date { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMilliseconds pub fn get_milliseconds(&self) -> f64 { self.to_local() - .map_or(f64::NAN, |dt| dt.nanosecond() as f64 / NANOS_IN_MS) + .map_or(f64::NAN, |dt| dt.nanosecond() as f64 / NANOS_PER_MS as f64) } /// `Date.prototype.getMinutes()` @@ -724,7 +768,7 @@ impl Date { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMilliseconds pub fn get_utc_milliseconds(&self) -> f64 { self.to_utc() - .map_or(f64::NAN, |dt| dt.nanosecond() as f64 / NANOS_IN_MS) + .map_or(f64::NAN, |dt| dt.nanosecond() as f64 / NANOS_PER_MS as f64) } /// `Date.prototype.getUTCMinutes()` @@ -1352,9 +1396,8 @@ impl Date { NaiveDate::from_ymd_opt(year, month + 1, day) .and_then(|f| f.and_hms_milli_opt(hour, min, sec, milli)) - .map_or(Ok(Value::number(f64::NAN)), |f| { - Ok(Value::number(f.timestamp_millis() as f64)) - }) + .and_then(|f| Self::time_clip(f.timestamp_millis() as f64)) + .map_or(Ok(Value::number(f64::NAN)), |time| Ok(Value::number(time))) } }