Skip to content

Commit

Permalink
Fix out-of-range panics of Date (#1075)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jevancc authored and Razican committed May 22, 2021
1 parent 8912be2 commit e29dfd7
Showing 1 changed file with 91 additions and 48 deletions.
139 changes: 91 additions & 48 deletions boa/src/builtins/date/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>) -> bool {
Expand Down Expand Up @@ -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<f64> {
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`.
Expand Down Expand Up @@ -224,53 +247,64 @@ impl Date {
millisecond: Option<f64>,
) {
#[inline]
fn num_days_in(year: i32, month: u32) -> i32 {
fn num_days_in(year: i32, month: u32) -> Option<u32> {
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.
Expand All @@ -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 {
Expand All @@ -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())
});
}

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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()`
Expand Down Expand Up @@ -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()`
Expand Down Expand Up @@ -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)))
}
}

Expand Down

0 comments on commit e29dfd7

Please sign in to comment.