From 9dc4dd14100d82725453ef2d9f7a311aa4be6662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Sat, 15 Apr 2023 17:23:20 +0000 Subject: [PATCH] Add hooks to get the current time and timezone (#2824) This Pull Request changes the following: - Adds two new hooks to `HostHooks` to access the current UTC time and the current timezone offset. - Replaces usages of `Local` with the host hook. - Replaces usages of `Utc::now` and `Local::now` with the hooks. cc @lastmjs --- boa_engine/src/builtins/date/mod.rs | 215 +++++++++++++---------- boa_engine/src/builtins/date/tests.rs | 15 -- boa_engine/src/builtins/date/utils.rs | 12 +- boa_engine/src/context/hooks.rs | 21 +++ boa_engine/src/object/builtins/jsdate.rs | 7 +- 5 files changed, 153 insertions(+), 117 deletions(-) diff --git a/boa_engine/src/builtins/date/mod.rs b/boa_engine/src/builtins/date/mod.rs index bae0a3207c..cad6ac3dd7 100644 --- a/boa_engine/src/builtins/date/mod.rs +++ b/boa_engine/src/builtins/date/mod.rs @@ -15,10 +15,14 @@ mod tests; use crate::{ builtins::BuiltInObject, - context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + context::{ + intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + HostHooks, + }, error::JsNativeError, js_string, object::{internal_methods::get_prototype_from_constructor, JsObject, ObjectData}, + property::Attribute, realm::Realm, string::utf16, symbol::JsSymbol, @@ -27,7 +31,6 @@ use crate::{ }; use boa_profiler::Profiler; use chrono::prelude::*; -use std::fmt::Display; use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject}; @@ -76,6 +79,11 @@ impl Date { Self(dt) } + /// Creates a new `Date` from the current UTC time of the host. + pub(crate) fn utc_now(hooks: &dyn HostHooks) -> Self { + Self(Some(hooks.utc_now())) + } + /// Converts the `Date` into a `JsValue`, mapping `None` to `NaN` and `Some(datetime)` to /// `JsValue::from(datetime.timestamp_millis())`. fn as_value(&self) -> JsValue { @@ -84,26 +92,16 @@ impl Date { } } -impl Display for Date { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(v) = self.to_local() { - write!(f, "{}", v.format("%a %b %d %Y %H:%M:%S GMT%:z")) - } else { - write!(f, "Invalid Date") - } - } -} - -impl Default for Date { - fn default() -> Self { - Self(Some(Utc::now().naive_utc())) - } -} - impl IntrinsicObject for Date { fn init(realm: &Realm) { let _timer = Profiler::global().start_event(Self::NAME, "init"); + let to_utc_string = BuiltInBuilder::new(realm) + .callable(Self::to_utc_string) + .name("toUTCString") + .length(0) + .build(); + BuiltInBuilder::from_standard_constructor::(realm) .method(Self::get_date::, "getDate", 0) .method(Self::get_day::, "getDay", 0) @@ -143,7 +141,6 @@ impl IntrinsicObject for Date { .method(Self::set_seconds::, "setUTCSeconds", 2) .method(Self::set_year, "setYear", 1) .method(Self::to_date_string, "toDateString", 0) - .method(Self::to_gmt_string, "toGMTString", 0) .method(Self::to_iso_string, "toISOString", 0) .method(Self::to_json, "toJSON", 1) .method(Self::to_locale_date_string, "toLocaleDateString", 0) @@ -151,7 +148,16 @@ impl IntrinsicObject for Date { .method(Self::to_locale_time_string, "toLocaleTimeString", 0) .method(Self::to_string, "toString", 0) .method(Self::to_time_string, "toTimeString", 0) - .method(Self::to_utc_string, "toUTCString", 0) + .property( + "toGMTString", + to_utc_string.clone(), + Attribute::WRITABLE | Attribute::CONFIGURABLE, + ) + .property( + "toUTCString", + to_utc_string, + Attribute::WRITABLE | Attribute::CONFIGURABLE, + ) .static_method(Self::utc, "UTC", 7) .method(Self::value_of, "valueOf", 0) .method( @@ -196,9 +202,12 @@ impl BuiltInConstructor for Date { // a. Let now be the time value (UTC) identifying the current time. // b. Return ToDateString(now). return Ok(JsValue::new( - Local::now() - .format("%a %b %d %Y %H:%M:%S GMT%:z") - .to_string(), + DateTime::::from_utc( + context.host_hooks().utc_now(), + context.host_hooks().tz_offset(), + ) + .format("%a %b %d %Y %H:%M:%S GMT%:z") + .to_string(), )); } // 2. Let numberOfArgs be the number of elements in values. @@ -206,7 +215,7 @@ impl BuiltInConstructor for Date { // 3. If numberOfArgs = 0, then [] => { // a. Let dv be the time value (UTC) identifying the current time. - Self::default() + Self::utc_now(&*context.host_hooks()) } // 4. Else if numberOfArgs = 1, then // a. Let value be values[0]. @@ -257,7 +266,13 @@ impl BuiltInConstructor for Date { // Separating this into its own function to simplify the logic. Self( Self::construct_date(args, context)? - .and_then(|dt| Local.from_local_datetime(&dt).earliest()) + .and_then(|dt| { + context + .host_hooks() + .tz_offset() + .from_local_datetime(&dt) + .earliest() + }) .map(|dt| dt.naive_utc()), ) } @@ -372,8 +387,10 @@ impl Date { /// [spec]: https://tc39.es/ecma262/#sec-date.now /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now #[allow(clippy::unnecessary_wraps)] - pub(crate) fn now(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { - Ok(JsValue::new(Utc::now().timestamp_millis())) + pub(crate) fn now(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult { + Ok(JsValue::new( + context.host_hooks().utc_now().timestamp_millis(), + )) } /// `Date.parse()` @@ -438,13 +455,17 @@ impl Date { pub(crate) fn get_date( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return DateFromTime(LocalTime(t)). @@ -462,13 +483,17 @@ impl Date { pub(crate) fn get_day( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return WeekDay(LocalTime(t)). @@ -489,14 +514,14 @@ impl Date { pub(crate) fn get_year( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let t = some_or_nan!(this_time_value(this)?); // 3. Return YearFromTime(LocalTime(t)) - 1900𝔽. - let local = Local.from_utc_datetime(&t); + let local = context.host_hooks().tz_offset().from_utc_datetime(&t); Ok(JsValue::from(local.year() - 1900)) } @@ -510,13 +535,17 @@ impl Date { pub(crate) fn get_full_year( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return YearFromTime(LocalTime(t)). @@ -533,13 +562,17 @@ impl Date { pub(crate) fn get_hours( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return HourFromTime(LocalTime(t)). @@ -556,13 +589,17 @@ impl Date { pub(crate) fn get_milliseconds( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return msFromTime(LocalTime(t)). @@ -579,13 +616,17 @@ impl Date { pub(crate) fn get_minutes( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return MinFromTime(LocalTime(t)). @@ -603,13 +644,17 @@ impl Date { pub(crate) fn get_month( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return MonthFromTime(LocalTime(t)). @@ -626,13 +671,17 @@ impl Date { pub(crate) fn get_seconds( this: &JsValue, _args: &[JsValue], - _context: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. let mut t = some_or_nan!(this_time_value(this)?); if LOCAL { - t = Local.from_utc_datetime(&t).naive_local(); + t = context + .host_hooks() + .tz_offset() + .from_utc_datetime(&t) + .naive_local(); } // 3. Return SecFromTime(LocalTime(t)) @@ -673,14 +722,16 @@ impl Date { pub(crate) fn get_timezone_offset( this: &JsValue, _: &[JsValue], - _: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let t be ? thisTimeValue(this value). // 2. If t is NaN, return NaN. some_or_nan!(this_time_value(this)?); // 3. Return (t - LocalTime(t)) / msPerMinute. - Ok(JsValue::from(-Local::now().offset().local_minus_utc() / 60)) + Ok(JsValue::from( + -context.host_hooks().tz_offset().local_minus_utc() / 60, + )) } /// [`Date.prototype.setDate ( date )`][local] and @@ -714,7 +765,7 @@ impl Date { date: Some(date), ..Default::default() }, - LOCAL, + LOCAL.then(|| context.host_hooks().tz_offset()), ); // 7. Set the [[DateValue]] internal slot of this Date object to u. @@ -744,7 +795,7 @@ impl Date { let datetime = match t.0 { Some(dt) => dt, None if LOCAL => { - let Some(datetime) = Local + let Some(datetime) = context.host_hooks().tz_offset() .from_local_datetime(&NaiveDateTime::default()) .earliest() .as_ref() @@ -782,7 +833,7 @@ impl Date { date, ..Default::default() }, - LOCAL, + LOCAL.then(|| context.host_hooks().tz_offset()), ); // 8. Set the [[DateValue]] internal slot of this Date object to u. @@ -848,7 +899,7 @@ impl Date { millisecond, ..Default::default() }, - LOCAL, + LOCAL.then(|| context.host_hooks().tz_offset()), ); // 13. Set the [[DateValue]] internal slot of this Date object to u. @@ -889,7 +940,7 @@ impl Date { millisecond: Some(ms), ..Default::default() }, - LOCAL, + LOCAL.then(|| context.host_hooks().tz_offset()), ); // 7. Set the [[DateValue]] internal slot of this Date object to u. @@ -945,7 +996,7 @@ impl Date { millisecond, ..Default::default() }, - LOCAL, + LOCAL.then(|| context.host_hooks().tz_offset()), ); // 11. Set the [[DateValue]] internal slot of this Date object to u. @@ -994,7 +1045,7 @@ impl Date { date, ..Default::default() }, - LOCAL, + LOCAL.then(|| context.host_hooks().tz_offset()), ); // 9. Set the [[DateValue]] internal slot of this Date object to u. @@ -1042,7 +1093,7 @@ impl Date { millisecond, ..Default::default() }, - LOCAL, + LOCAL.then(|| context.host_hooks().tz_offset()), ); // 9. Set the [[DateValue]] internal slot of this Date object to u. @@ -1080,7 +1131,7 @@ impl Date { // 3. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). let Some(datetime) = t.0.or_else(|| { - Local + context.host_hooks().tz_offset() .from_local_datetime(&NaiveDateTime::default()) .earliest() .as_ref() @@ -1113,7 +1164,7 @@ impl Date { year: Some(IntegerOrNan::Integer(year)), ..Default::default() }, - true, + Some(context.host_hooks().tz_offset()), ); // 10. Set the [[DateValue]] internal slot of this Date object to TimeClip(date). @@ -1169,7 +1220,7 @@ impl Date { pub(crate) fn to_date_string( this: &JsValue, _: &[JsValue], - _: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let O be this Date object. // 2. Let tv be ? thisTimeValue(O). @@ -1180,8 +1231,9 @@ impl Date { // 4. Let t be LocalTime(tv). // 5. Return DateString(t). - Ok(Local::now() - .timezone() + Ok(context + .host_hooks() + .tz_offset() .from_utc_datetime(&tv) .format("%a %b %d %Y") .to_string() @@ -1206,8 +1258,7 @@ impl Date { ) -> JsResult { let t = this_time_value(this)? .ok_or_else(|| JsNativeError::range().with_message("Invalid time value"))?; - Ok(Utc::now() - .timezone() + Ok(Utc .from_utc_datetime(&t) .format("%Y-%m-%dT%H:%M:%S.%3fZ") .to_string() @@ -1315,15 +1366,16 @@ impl Date { pub(crate) fn to_string( this: &JsValue, _: &[JsValue], - _: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let tv be ? thisTimeValue(this value). // 2. Return ToDateString(tv). let Some(t) = this_time_value(this)? else { return Ok(js_string!("Invalid Date").into()); }; - Ok(Local::now() - .timezone() + Ok(context + .host_hooks() + .tz_offset() .from_utc_datetime(&t) .format("%a %b %d %Y %H:%M:%S GMT%z") .to_string() @@ -1343,7 +1395,7 @@ impl Date { pub(crate) fn to_time_string( this: &JsValue, _: &[JsValue], - _: &mut Context<'_>, + context: &mut Context<'_>, ) -> JsResult { // 1. Let O be this Date object. // 2. Let tv be ? thisTimeValue(O). @@ -1354,8 +1406,9 @@ impl Date { // 4. Let t be LocalTime(tv). // 5. Return the string-concatenation of TimeString(t) and TimeZoneString(tv). - Ok(Local::now() - .timezone() + Ok(context + .host_hooks() + .tz_offset() .from_utc_datetime(&t) .format("%H:%M:%S GMT%z") .to_string() @@ -1456,30 +1509,4 @@ impl Date { // 6. Return ? OrdinaryToPrimitive(O, tryFirst). o.ordinary_to_primitive(context, try_first) } - - /// [`Date.prototype.toGMTString ( )`][spec]. - /// - /// The `toGMTString()` method converts a date to a string, using Internet Greenwich Mean Time - /// (GMT) conventions. - /// - /// More information: - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.togmtstring - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toGMTString - pub(crate) fn to_gmt_string( - this: &JsValue, - _args: &[JsValue], - context: &mut Context<'_>, - ) -> JsResult { - // The initial value of the "toGMTString" property is %Date.prototype.toUTCString% - Self::to_utc_string(this, &[], context) - } - - /// Converts the `Date` to a local `DateTime`. - /// - /// If the `Date` is invalid (i.e. NAN), this function will return `None`. - pub(crate) fn to_local(self) -> Option> { - self.0.map(|utc| Local.from_utc_datetime(&utc)) - } } diff --git a/boa_engine/src/builtins/date/tests.rs b/boa_engine/src/builtins/date/tests.rs index 7bdfc8a32e..3650caf0dc 100644 --- a/boa_engine/src/builtins/date/tests.rs +++ b/boa_engine/src/builtins/date/tests.rs @@ -43,21 +43,6 @@ fn timestamp_from_utc( .timestamp_millis() } -#[test] -fn date_display() { - let dt = super::Date(None); - assert_eq!("[Invalid Date]", format!("[{dt}]")); - - let cd = super::Date::default(); - assert_eq!( - format!( - "[{}]", - cd.to_local().unwrap().format("%a %b %d %Y %H:%M:%S GMT%:z") - ), - format!("[{cd}]") - ); -} - #[test] fn date_this_time_value() { run_test_actions([TestAction::assert_native_error( diff --git a/boa_engine/src/builtins/date/utils.rs b/boa_engine/src/builtins/date/utils.rs index 9cd142cfe6..4803fcb788 100644 --- a/boa_engine/src/builtins/date/utils.rs +++ b/boa_engine/src/builtins/date/utils.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, Local, NaiveDateTime, TimeZone, Timelike}; +use chrono::{Datelike, FixedOffset, NaiveDateTime, TimeZone, Timelike}; use crate::value::IntegerOrNan; @@ -136,7 +136,7 @@ pub(super) struct DateParameters { pub(super) fn replace_params( datetime: NaiveDateTime, params: DateParameters, - local: bool, + offset: Option, ) -> Option { let DateParameters { year, @@ -148,8 +148,8 @@ pub(super) fn replace_params( millisecond, } = params; - let datetime = if local { - Local.from_utc_datetime(&datetime).naive_local() + let datetime = if let Some(offset) = offset { + offset.from_utc_datetime(&datetime).naive_local() } else { datetime }; @@ -187,8 +187,8 @@ pub(super) fn replace_params( let new_time = make_time(hour, minute, second, millisecond)?; let mut ts = make_date(new_day, new_time)?; - if local { - ts = Local + if let Some(offset) = offset { + ts = offset .from_local_datetime(&NaiveDateTime::from_timestamp_millis(ts)?) .earliest()? .naive_utc() diff --git a/boa_engine/src/context/hooks.rs b/boa_engine/src/context/hooks.rs index f1204474b8..d3479bba6e 100644 --- a/boa_engine/src/context/hooks.rs +++ b/boa_engine/src/context/hooks.rs @@ -5,6 +5,7 @@ use crate::{ realm::Realm, Context, JsResult, JsValue, }; +use chrono::{FixedOffset, Local, NaiveDateTime, Utc}; use super::intrinsics::Intrinsics; @@ -170,6 +171,26 @@ pub trait HostHooks { fn create_global_this(&self, _intrinsics: &Intrinsics) -> Option { None } + + /// Gets the current UTC time of the host. + /// + /// Defaults to using [`Utc::now`] on all targets, which can cause panics if your platform + /// doesn't support [`SystemTime::now`][time]. + /// + /// [time]: std::time::SystemTime::now + fn utc_now(&self) -> NaiveDateTime { + Utc::now().naive_utc() + } + + /// Gets the current timezone as a fixed offset from UTC. + /// + /// Defaults to using [`Local::now`] on all targets, which can cause panics if your platform + /// doesn't support [`SystemTime::now`][time]. + /// + /// [time]: std::time::SystemTime::now + fn tz_offset(&self) -> FixedOffset { + *Local::now().offset() + } } /// Default implementation of [`HostHooks`], which doesn't carry any state. diff --git a/boa_engine/src/object/builtins/jsdate.rs b/boa_engine/src/object/builtins/jsdate.rs index 14a40f4cd8..f6297cc308 100644 --- a/boa_engine/src/object/builtins/jsdate.rs +++ b/boa_engine/src/object/builtins/jsdate.rs @@ -43,7 +43,10 @@ impl JsDate { #[inline] pub fn new(context: &mut Context<'_>) -> Self { let prototype = context.intrinsics().constructors().date().prototype(); - let inner = JsObject::from_proto_and_data(prototype, ObjectData::date(Date::default())); + let inner = JsObject::from_proto_and_data( + prototype, + ObjectData::date(Date::utc_now(&*context.host_hooks())), + ); Self { inner } } @@ -468,7 +471,7 @@ impl JsDate { #[deprecated] #[inline] pub fn to_gmt_string(&self, context: &mut Context<'_>) -> JsResult { - Date::to_gmt_string(&self.inner.clone().into(), &[JsValue::Null], context) + Date::to_utc_string(&self.inner.clone().into(), &[JsValue::Null], context) } /// Returns the given date in the ISO 8601 format according to universal