Skip to content
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

Fix out-of-range panics of Date #1075

Merged
merged 9 commits into from
Jan 20, 2021
Merged
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
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