Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* JS date methods.
*
* "For example, OS/360 devotes 26 bytes of the permanently
* resident date-turnover routine to the proper handling of
* December 31 on leap years (when it is Day 366). That
* might have been left to the operator."
*
* Frederick Brooks, 'The Second-System Effect'.
*/
#include "jsdate.h"
#include "mozilla/Atomics.h"
#include "mozilla/Casting.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/Sprintf.h"
#include "mozilla/TextUtils.h"
#include <algorithm>
#include <iterator>
#include <math.h>
#include <string.h>
#include "jsapi.h"
#include "jsfriendapi.h"
#include "jsnum.h"
#include "jstypes.h"
#include "js/CallAndConstruct.h" // JS::IsCallable
#include "js/Conversions.h"
#include "js/Date.h"
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/LocaleSensitive.h"
#include "js/Object.h" // JS::GetBuiltinClass
#include "js/PropertySpec.h"
#include "js/Wrapper.h"
#include "util/DifferentialTesting.h"
#include "util/StringBuffer.h"
#include "util/Text.h"
#include "vm/DateObject.h"
#include "vm/DateTime.h"
#include "vm/GlobalObject.h"
#include "vm/Interpreter.h"
#include "vm/JSContext.h"
#include "vm/JSObject.h"
#include "vm/StringType.h"
#include "vm/Time.h"
#include "vm/WellKnownAtom.h" // js_*_str
#include "vm/Compartment-inl.h" // For js::UnwrapAndTypeCheckThis
#include "vm/GeckoProfiler-inl.h"
#include "vm/JSObject-inl.h"
using namespace js;
using mozilla::Atomic;
using mozilla::BitwiseCast;
using mozilla::IsAsciiAlpha;
using mozilla::IsAsciiDigit;
using mozilla::IsAsciiLowercaseAlpha;
using mozilla::IsFinite;
using mozilla::IsNaN;
using mozilla::NumbersAreIdentical;
using mozilla::Relaxed;
using JS::AutoCheckCannotGC;
using JS::ClippedTime;
using JS::GenericNaN;
using JS::GetBuiltinClass;
using JS::TimeClip;
using JS::ToInteger;
// When this value is non-zero, we'll round the time by this resolution.
static Atomic<uint32_t, Relaxed> sResolutionUsec;
// This is not implemented yet, but we will use this to know to jitter the time
// in the JS shell
static Atomic<bool, Relaxed> sJitter;
// The callback we will use for the Gecko implementation of Timer
// Clamping/Jittering
static Atomic<JS::ReduceMicrosecondTimePrecisionCallback, Relaxed>
sReduceMicrosecondTimePrecisionCallback;
/*
* The JS 'Date' object is patterned after the Java 'Date' object.
* Here is a script:
*
* today = new Date();
*
* print(today.toLocaleString());
*
* weekDay = today.getDay();
*
*
* These Java (and ECMA-262) methods are supported:
*
* UTC
* getDate (getUTCDate)
* getDay (getUTCDay)
* getHours (getUTCHours)
* getMinutes (getUTCMinutes)
* getMonth (getUTCMonth)
* getSeconds (getUTCSeconds)
* getMilliseconds (getUTCMilliseconds)
* getTime
* getTimezoneOffset
* getYear
* getFullYear (getUTCFullYear)
* parse
* setDate (setUTCDate)
* setHours (setUTCHours)
* setMinutes (setUTCMinutes)
* setMonth (setUTCMonth)
* setSeconds (setUTCSeconds)
* setMilliseconds (setUTCMilliseconds)
* setTime
* setYear (setFullYear, setUTCFullYear)
* toGMTString (toUTCString)
* toLocaleString
* toString
*
*
* These Java methods are not supported
*
* setDay
* before
* after
* equals
* hashCode
*/
namespace {
class DateTimeHelper {
private:
#if JS_HAS_INTL_API
static double localTZA(DateTimeInfo::ShouldRFP shouldRFP, double t,
DateTimeInfo::TimeZoneOffset offset);
#else
static int equivalentYearForDST(int year);
static bool isRepresentableAsTime32(double t);
static double daylightSavingTA(DateTimeInfo::ShouldRFP shouldRFP, double t);
static double adjustTime(DateTimeInfo::ShouldRFP shouldRFP, double date);
static PRMJTime toPRMJTime(DateTimeInfo::ShouldRFP shouldRFP,
double localTime, double utcTime);
#endif
public:
static double localTime(DateTimeInfo::ShouldRFP shouldRFP, double t);
static double UTC(DateTimeInfo::ShouldRFP shouldRFP, double t);
static JSString* timeZoneComment(JSContext* cx,
DateTimeInfo::ShouldRFP shouldRFP,
double utcTime, double localTime);
#if !JS_HAS_INTL_API
static size_t formatTime(DateTimeInfo::ShouldRFP shouldRFP, char* buf,
size_t buflen, const char* fmt, double utcTime,
double localTime);
#endif
};
} // namespace
static DateTimeInfo::ShouldRFP ShouldRFP(const Realm* realm) {
return realm->behaviors().shouldResistFingerprinting()
? DateTimeInfo::ShouldRFP::Yes
: DateTimeInfo::ShouldRFP::No;
}
// ES2019 draft rev 0ceb728a1adbffe42b26972a6541fd7f398b1557
// 5.2.5 Mathematical Operations
static inline double PositiveModulo(double dividend, double divisor) {
MOZ_ASSERT(divisor > 0);
MOZ_ASSERT(IsFinite(divisor));
double result = fmod(dividend, divisor);
if (result < 0) {
result += divisor;
}
return result + (+0.0);
}
static inline double Day(double t) { return floor(t / msPerDay); }
static double TimeWithinDay(double t) { return PositiveModulo(t, msPerDay); }
/* ES5 15.9.1.3. */
static inline bool IsLeapYear(double year) {
MOZ_ASSERT(ToInteger(year) == year);
return fmod(year, 4) == 0 && (fmod(year, 100) != 0 || fmod(year, 400) == 0);
}
static inline double DaysInYear(double year) {
if (!IsFinite(year)) {
return GenericNaN();
}
return IsLeapYear(year) ? 366 : 365;
}
static inline double DayFromYear(double y) {
return 365 * (y - 1970) + floor((y - 1969) / 4.0) -
floor((y - 1901) / 100.0) + floor((y - 1601) / 400.0);
}
static inline double TimeFromYear(double y) {
return DayFromYear(y) * msPerDay;
}
static double YearFromTime(double t) {
if (!IsFinite(t)) {
return GenericNaN();
}
MOZ_ASSERT(ToInteger(t) == t);
double y = floor(t / (msPerDay * 365.2425)) + 1970;
double t2 = TimeFromYear(y);
/*
* Adjust the year if the approximation was wrong. Since the year was
* computed using the average number of ms per year, it will usually
* be wrong for dates within several hours of a year transition.
*/
if (t2 > t) {
y--;
} else {
if (t2 + msPerDay * DaysInYear(y) <= t) {
y++;
}
}
return y;
}
static inline int DaysInFebruary(double year) {
return IsLeapYear(year) ? 29 : 28;
}
/* ES5 15.9.1.4. */
static inline double DayWithinYear(double t, double year) {
MOZ_ASSERT_IF(IsFinite(t), YearFromTime(t) == year);
return Day(t) - DayFromYear(year);
}
static double MonthFromTime(double t) {
if (!IsFinite(t)) {
return GenericNaN();
}
double year = YearFromTime(t);
double d = DayWithinYear(t, year);
int step;
if (d < (step = 31)) {
return 0;
}
if (d < (step += DaysInFebruary(year))) {
return 1;
}
if (d < (step += 31)) {
return 2;
}
if (d < (step += 30)) {
return 3;
}
if (d < (step += 31)) {
return 4;
}
if (d < (step += 30)) {
return 5;
}
if (d < (step += 31)) {
return 6;
}
if (d < (step += 31)) {
return 7;
}
if (d < (step += 30)) {
return 8;
}
if (d < (step += 31)) {
return 9;
}
if (d < (step += 30)) {
return 10;
}
return 11;
}
/* ES5 15.9.1.5. */
static double DateFromTime(double t) {
if (!IsFinite(t)) {
return GenericNaN();
}
double year = YearFromTime(t);
double d = DayWithinYear(t, year);
int next;
if (d <= (next = 30)) {
return d + 1;
}
int step = next;
if (d <= (next += DaysInFebruary(year))) {
return d - step;
}
step = next;
if (d <= (next += 31)) {
return d - step;
}
step = next;
if (d <= (next += 30)) {
return d - step;
}
step = next;
if (d <= (next += 31)) {
return d - step;
}
step = next;
if (d <= (next += 30)) {
return d - step;
}
step = next;
if (d <= (next += 31)) {
return d - step;
}
step = next;
if (d <= (next += 31)) {
return d - step;
}
step = next;
if (d <= (next += 30)) {
return d - step;
}
step = next;
if (d <= (next += 31)) {
return d - step;
}
step = next;
if (d <= (next += 30)) {
return d - step;
}
step = next;
return d - step;
}
/* ES5 15.9.1.6. */
static int WeekDay(double t) {
/*
* We can't assert TimeClip(t) == t because we call this function with
* local times, which can be offset outside TimeClip's permitted range.
*/
MOZ_ASSERT(ToInteger(t) == t);
int result = (int(Day(t)) + 4) % 7;
if (result < 0) {
result += 7;
}
return result;
}
static inline int DayFromMonth(int month, bool isLeapYear) {
/*
* The following array contains the day of year for the first day of
* each month, where index 0 is January, and day 0 is January 1.
*/
static const int firstDayOfMonth[2][13] = {
{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
{0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}};
MOZ_ASSERT(0 <= month && month <= 12);
return firstDayOfMonth[isLeapYear][month];
}
template <typename T>
static inline int DayFromMonth(T month, bool isLeapYear) = delete;
/* ES5 15.9.1.12 (out of order to accommodate DaylightSavingTA). */
static double MakeDay(double year, double month, double date) {
/* Step 1. */
if (!IsFinite(year) || !IsFinite(month) || !IsFinite(date)) {
return GenericNaN();
}
/* Steps 2-4. */
double y = ToInteger(year);
double m = ToInteger(month);
double dt = ToInteger(date);
/* Step 5. */
double ym = y + floor(m / 12);
/* Step 6. */
int mn = int(PositiveModulo(m, 12));
/* Steps 7-8. */
bool leap = IsLeapYear(ym);
double yearday = floor(TimeFromYear(ym) / msPerDay);
double monthday = DayFromMonth(mn, leap);
return yearday + monthday + dt - 1;
}
/* ES5 15.9.1.13 (out of order to accommodate DaylightSavingTA). */
static inline double MakeDate(double day, double time) {
/* Step 1. */
if (!IsFinite(day) || !IsFinite(time)) {
return GenericNaN();
}
/* Step 2. */
return day * msPerDay + time;
}
JS_PUBLIC_API double JS::MakeDate(double year, unsigned month, unsigned day) {
MOZ_ASSERT(month <= 11);
MOZ_ASSERT(day >= 1 && day <= 31);
return ::MakeDate(MakeDay(year, month, day), 0);
}
JS_PUBLIC_API double JS::MakeDate(double year, unsigned month, unsigned day,
double time) {
MOZ_ASSERT(month <= 11);
MOZ_ASSERT(day >= 1 && day <= 31);
return ::MakeDate(MakeDay(year, month, day), time);
}
JS_PUBLIC_API double JS::YearFromTime(double time) {
return ::YearFromTime(time);
}
JS_PUBLIC_API double JS::MonthFromTime(double time) {
return ::MonthFromTime(time);
}
JS_PUBLIC_API double JS::DayFromTime(double time) { return DateFromTime(time); }
JS_PUBLIC_API double JS::DayFromYear(double year) {
return ::DayFromYear(year);
}
JS_PUBLIC_API double JS::DayWithinYear(double time, double year) {
return ::DayWithinYear(time, year);
}
JS_PUBLIC_API void JS::SetReduceMicrosecondTimePrecisionCallback(
JS::ReduceMicrosecondTimePrecisionCallback callback) {
sReduceMicrosecondTimePrecisionCallback = callback;
}
JS_PUBLIC_API void JS::SetTimeResolutionUsec(uint32_t resolution, bool jitter) {
sResolutionUsec = resolution;
sJitter = jitter;
}
#if JS_HAS_INTL_API
// ES2019 draft rev 0ceb728a1adbffe42b26972a6541fd7f398b1557
// 20.3.1.7 LocalTZA ( t, isUTC )
double DateTimeHelper::localTZA(DateTimeInfo::ShouldRFP shouldRFP, double t,
DateTimeInfo::TimeZoneOffset offset) {
MOZ_ASSERT(IsFinite(t));
int64_t milliseconds = static_cast<int64_t>(t);
int32_t offsetMilliseconds =
DateTimeInfo::getOffsetMilliseconds(shouldRFP, milliseconds, offset);
return static_cast<double>(offsetMilliseconds);
}
// ES2019 draft rev 0ceb728a1adbffe42b26972a6541fd7f398b1557
// 20.3.1.8 LocalTime ( t )
double DateTimeHelper::localTime(DateTimeInfo::ShouldRFP shouldRFP, double t) {
if (!IsFinite(t)) {
return GenericNaN();
}
MOZ_ASSERT(StartOfTime <= t && t <= EndOfTime);
return t + localTZA(shouldRFP, t, DateTimeInfo::TimeZoneOffset::UTC);
}
// ES2019 draft rev 0ceb728a1adbffe42b26972a6541fd7f398b1557
// 20.3.1.9 UTC ( t )
double DateTimeHelper::UTC(DateTimeInfo::ShouldRFP shouldRFP, double t) {
if (!IsFinite(t)) {
return GenericNaN();
}
if (t < (StartOfTime - msPerDay) || t > (EndOfTime + msPerDay)) {
return GenericNaN();
}
return t - localTZA(shouldRFP, t, DateTimeInfo::TimeZoneOffset::Local);
}
#else
/*
* Find a year for which any given date will fall on the same weekday.
*
* This function should be used with caution when used other than
* for determining DST; it hasn't been proven not to produce an
* incorrect year for times near year boundaries.
*/
int DateTimeHelper::equivalentYearForDST(int year) {
/*
* Years and leap years on which Jan 1 is a Sunday, Monday, etc.
*
* yearStartingWith[0][i] is an example non-leap year where
* Jan 1 appears on Sunday (i == 0), Monday (i == 1), etc.
*
* yearStartingWith[1][i] is an example leap year where
* Jan 1 appears on Sunday (i == 0), Monday (i == 1), etc.
*
* Keep two different mappings, one for past years (< 1970), and a
* different one for future years (> 2037).
*/
static const int pastYearStartingWith[2][7] = {
{1978, 1973, 1974, 1975, 1981, 1971, 1977},
{1984, 1996, 1980, 1992, 1976, 1988, 1972}};
static const int futureYearStartingWith[2][7] = {
{2034, 2035, 2030, 2031, 2037, 2027, 2033},
{2012, 2024, 2036, 2020, 2032, 2016, 2028}};
int day = int(DayFromYear(year) + 4) % 7;
if (day < 0) {
day += 7;
}
const auto& yearStartingWith =
year < 1970 ? pastYearStartingWith : futureYearStartingWith;
return yearStartingWith[IsLeapYear(year)][day];
}
// Return true if |t| is representable as a 32-bit time_t variable, that means
// the year is in [1970, 2038).
bool DateTimeHelper::isRepresentableAsTime32(double t) {
return 0.0 <= t && t < 2145916800000.0;
}
/* ES5 15.9.1.8. */
double DateTimeHelper::daylightSavingTA(DateTimeInfo::ShouldRFP shouldRFP,
double t) {
if (!IsFinite(t)) {
return GenericNaN();
}
/*
* If earlier than 1970 or after 2038, potentially beyond the ken of
* many OSes, map it to an equivalent year before asking.
*/
if (!isRepresentableAsTime32(t)) {
int year = equivalentYearForDST(int(YearFromTime(t)));
double day = MakeDay(year, MonthFromTime(t), DateFromTime(t));
t = MakeDate(day, TimeWithinDay(t));
}
int64_t utcMilliseconds = static_cast<int64_t>(t);
int32_t offsetMilliseconds =
DateTimeInfo::getDSTOffsetMilliseconds(shouldRFP, utcMilliseconds);
return static_cast<double>(offsetMilliseconds);
}
double DateTimeHelper::adjustTime(DateTimeInfo::ShouldRFP shouldRFP,
double date) {
double localTZA = DateTimeInfo::localTZA(shouldRFP);
double t = daylightSavingTA(shouldRFP, date) + localTZA;
t = (localTZA >= 0) ? fmod(t, msPerDay) : -fmod(msPerDay - t, msPerDay);
return t;
}
/* ES5 15.9.1.9. */
double DateTimeHelper::localTime(DateTimeInfo::ShouldRFP shouldRFP, double t) {
return t + adjustTime(shouldRFP, t);
}
double DateTimeHelper::UTC(DateTimeInfo::ShouldRFP shouldRFP, double t) {
// Following the ES2017 specification creates undesirable results at DST
// transitions. For example when transitioning from PST to PDT,
// |new Date(2016,2,13,2,0,0).toTimeString()| returns the string value
// "01:00:00 GMT-0800 (PST)" instead of "03:00:00 GMT-0700 (PDT)". Follow
// V8 and subtract one hour before computing the offset.
return t - adjustTime(shouldRFP,
t - DateTimeInfo::localTZA(shouldRFP) - msPerHour);
}
#endif /* JS_HAS_INTL_API */
static double LocalTime(DateTimeInfo::ShouldRFP shouldRFP, double t) {
return DateTimeHelper::localTime(shouldRFP, t);
}
static double UTC(DateTimeInfo::ShouldRFP shouldRFP, double t) {
return DateTimeHelper::UTC(shouldRFP, t);
}
/* ES5 15.9.1.10. */
static double HourFromTime(double t) {
return PositiveModulo(floor(t / msPerHour), HoursPerDay);
}
static double MinFromTime(double t) {
return PositiveModulo(floor(t / msPerMinute), MinutesPerHour);
}
static double SecFromTime(double t) {
return PositiveModulo(floor(t / msPerSecond), SecondsPerMinute);
}
static double msFromTime(double t) { return PositiveModulo(t, msPerSecond); }
/* ES5 15.9.1.11. */
static double MakeTime(double hour, double min, double sec, double ms) {
/* Step 1. */
if (!IsFinite(hour) || !IsFinite(min) || !IsFinite(sec) || !IsFinite(ms)) {
return GenericNaN();
}
/* Step 2. */
double h = ToInteger(hour);
/* Step 3. */
double m = ToInteger(min);
/* Step 4. */
double s = ToInteger(sec);
/* Step 5. */
double milli = ToInteger(ms);
/* Steps 6-7. */
return h * msPerHour + m * msPerMinute + s * msPerSecond + milli;
}
/**
* end of ECMA 'support' functions
*/
// ES2017 draft rev (TODO: Add git hash when PR 642 is merged.)
// 20.3.3.4
// Date.UTC(year [, month [, date [, hours [, minutes [, seconds [, ms]]]]]])
static bool date_UTC(JSContext* cx, unsigned argc, Value* vp) {
AutoJSMethodProfilerEntry pseudoFrame(cx, "Date", "UTC");
CallArgs args = CallArgsFromVp(argc, vp);
// Step 1.
double y;
if (!ToNumber(cx, args.get(0), &y)) {
return false;
}
// Step 2.
double m;
if (args.length() >= 2) {
if (!ToNumber(cx, args[1], &m)) {
return false;
}
} else {
m = 0;
}
// Step 3.
double dt;
if (args.length() >= 3) {
if (!ToNumber(cx, args[2], &dt)) {
return false;
}
} else {
dt = 1;
}
// Step 4.
double h;
if (args.length() >= 4) {
if (!ToNumber(cx, args[3], &h)) {
return false;
}
} else {
h = 0;
}
// Step 5.
double min;
if (args.length() >= 5) {
if (!ToNumber(cx, args[4], &min)) {
return false;
}
} else {
min = 0;
}
// Step 6.
double s;
if (args.length() >= 6) {
if (!ToNumber(cx, args[5], &s)) {
return false;
}
} else {
s = 0;
}
// Step 7.
double milli;
if (args.length() >= 7) {
if (!ToNumber(cx, args[6], &milli)) {
return false;
}
} else {
milli = 0;
}
// Step 8.
double yr = y;
if (!IsNaN(y)) {
double yint = ToInteger(y);
if (0 <= yint && yint <= 99) {
yr = 1900 + yint;
}
}
// Step 9.
ClippedTime time =
TimeClip(MakeDate(MakeDay(yr, m, dt), MakeTime(h, min, s, milli)));
args.rval().set(TimeValue(time));
return true;
}
/*
* Read and convert decimal digits from s[*i] into *result
* while *i < limit.
*
* Succeed if any digits are converted. Advance *i only
* as digits are consumed.
*/
template <typename CharT>
static bool ParseDigits(size_t* result, const CharT* s, size_t* i,
size_t limit) {
size_t init = *i;
*result = 0;
while (*i < limit && ('0' <= s[*i] && s[*i] <= '9')) {
*result *= 10;
*result += (s[*i] - '0');
++(*i);
}
return *i != init;
}
/*
* Read and convert decimal digits to the right of a decimal point,
* representing a fractional integer, from s[*i] into *result
* while *i < limit.
*
* Succeed if any digits are converted. Advance *i only
* as digits are consumed.
*/
template <typename CharT>
static bool ParseFractional(double* result, const CharT* s, size_t* i,
size_t limit) {
double factor = 0.1;
size_t init = *i;
*result = 0.0;
while (*i < limit && ('0' <= s[*i] && s[*i] <= '9')) {
*result += (s[*i] - '0') * factor;
factor *= 0.1;
++(*i);
}
return *i != init;
}
/*
* Read and convert exactly n decimal digits from s[*i]
* to s[min(*i+n,limit)] into *result.
*
* Succeed if exactly n digits are converted. Advance *i only
* on success.
*/
template <typename CharT>
static bool ParseDigitsN(size_t n, size_t* result, const CharT* s, size_t* i,
size_t limit) {
size_t init = *i;
if (ParseDigits(result, s, i, std::min(limit, init + n))) {
return (*i - init) == n;
}
*i = init;
return false;
}
/*
* Read and convert n or less decimal digits from s[*i]
* to s[min(*i+n,limit)] into *result.
*
* Succeed only if greater than zero but less than or equal to n digits are
* converted. Advance *i only on success.
*/
template <typename CharT>
static bool ParseDigitsNOrLess(size_t n, size_t* result, const CharT* s,
size_t* i, size_t limit) {
size_t init = *i;
if (ParseDigits(result, s, i, std::min(limit, init + n))) {
return ((*i - init) > 0) && ((*i - init) <= n);
}
*i = init;
return false;
}
static int DaysInMonth(int year, int month) {
bool leap = IsLeapYear(year);
int result = int(DayFromMonth(month, leap) - DayFromMonth(month - 1, leap));
return result;
}
/*
* Parse a string according to the formats specified in section 20.3.1.16
* of the ECMAScript standard. These formats are based upon a simplification
* of the ISO 8601 Extended Format. As per the spec omitted month and day
* values are defaulted to '01', omitted HH:mm:ss values are defaulted to '00'
* and an omitted sss field is defaulted to '000'.
*
* For cross compatibility we allow the following extensions.
*
* These are:
*
* Standalone time part:
* Any of the time formats below can be parsed without a date part.
* E.g. "T19:00:00Z" will parse successfully. The date part will then
* default to 1970-01-01.
*
* 'T' from the time part may be replaced with a space character:
* "1970-01-01 12:00:00Z" will parse successfully. Note that only a single
* space is permitted and this is not permitted in the standalone
* version above.
*
* One or more decimal digits for milliseconds:
* The specification requires exactly three decimal digits for
* the fractional part but we allow for one or more digits.
*
* Time zone specifier without ':':
* We allow the time zone to be specified without a ':' character.
* E.g. "T19:00:00+0700" is equivalent to "T19:00:00+07:00".
*
* One or two digits for months, days, hours, minutes and seconds:
* The specification requires exactly two decimal digits for the fields
* above. We allow for one or two decimal digits. I.e. "1970-1-1" is
* equivalent to "1970-01-01".
*
* Date part:
*
* Year:
* YYYY (eg 1997)
*
* Year and month:
* YYYY-MM (eg 1997-07)
*
* Complete date:
* YYYY-MM-DD (eg 1997-07-16)
*
* Time part:
*
* Hours and minutes:
* Thh:mmTZD (eg T19:20+01:00)
*
* Hours, minutes and seconds:
* Thh:mm:ssTZD (eg T19:20:30+01:00)
*
* Hours, minutes, seconds and a decimal fraction of a second:
* Thh:mm:ss.sTZD (eg T19:20:30.45+01:00)
*
* where:
*
* YYYY = four-digit year or six digit year as +YYYYYY or -YYYYYY
* MM = one or two-digit month (01=January, etc.)
* DD = one or two-digit day of month (01 through 31)
* hh = one or two digits of hour (00 through 23) (am/pm NOT allowed)
* mm = one or two digits of minute (00 through 59)
* ss = one or two digits of second (00 through 59)
* sss = one or more digits representing a decimal fraction of a second
* TZD = time zone designator (Z or +hh:mm or -hh:mm or missing for local)
*/
template <typename CharT>
static bool ParseISOStyleDate(DateTimeInfo::ShouldRFP shouldRFP, const CharT* s,
size_t length, ClippedTime* result) {
size_t i = 0;
size_t pre = 0;
int tzMul = 1;
int dateMul = 1;
size_t year = 1970;
size_t month = 1;
size_t day = 1;
size_t hour = 0;
size_t min = 0;
size_t sec = 0;
double frac = 0;
bool isLocalTime = false;
size_t tzHour = 0;
size_t tzMin = 0;
bool isPermissive = false;
bool isStrict = false;
#define PEEK(ch) (i < length && s[i] == ch)
#define NEED(ch) \
if (i >= length || s[i] != ch) { \
return false; \
} else { \
++i; \
}
#define DONE_DATE_UNLESS(ch) \
if (i >= length || s[i] != ch) { \
goto done_date; \
} else { \
++i; \
}
#define DONE_UNLESS(ch) \
if (i >= length || s[i] != ch) { \
goto done; \
} else { \
++i; \
}
#define NEED_NDIGITS(n, field) \
if (!ParseDigitsN(n, &field, s, &i, length)) { \
return false; \
}
#define NEED_NDIGITS_OR_LESS(n, field) \
pre = i; \
if (!ParseDigitsNOrLess(n, &field, s, &i, length)) { \
return false; \
} \
if (i < pre + (n)) { \
if (isStrict) { \
return false; \
} else { \
isPermissive = true; \
} \
}
if (PEEK('+') || PEEK('-')) {
if (PEEK('-')) {
dateMul = -1;
}
++i;
NEED_NDIGITS(6, year);
} else {
NEED_NDIGITS(4, year);
}
DONE_DATE_UNLESS('-');
NEED_NDIGITS_OR_LESS(2, month);
DONE_DATE_UNLESS('-');
NEED_NDIGITS_OR_LESS(2, day);
done_date:
if (PEEK('T')) {
if (isPermissive) {
// Require standard format "[+00]1970-01-01" if a time part marker "T"
// exists
return false;
}
isStrict = true;
i++;
} else if (PEEK(' ')) {
i++;
} else {
goto done;
}
NEED_NDIGITS_OR_LESS(2, hour);
NEED(':');
NEED_NDIGITS_OR_LESS(2, min);
if (PEEK(':')) {
++i;
NEED_NDIGITS_OR_LESS(2, sec);
if (PEEK('.')) {
++i;
if (!ParseFractional(&frac, s, &i, length)) {
return false;
}
}
}
if (PEEK('Z')) {
++i;
} else if (PEEK('+') || PEEK('-')) {
if (PEEK('-')) {
tzMul = -1;
}
++i;
NEED_NDIGITS(2, tzHour);
/*
* Non-standard extension to the ISO date format:
* allow two digits for the time zone offset.
*/
if (i >= length && !isStrict) {
goto done;
}
/*
* Non-standard extension to the ISO date format (permitted by ES5):
* allow "-0700" as a time zone offset, not just "-07:00".
*/
if (PEEK(':')) {
++i;
}
NEED_NDIGITS(2, tzMin);
} else {
isLocalTime = true;
}
done:
if (year > 275943 // ceil(1e8/365) + 1970
|| (month == 0 || month > 12) ||
(day == 0 || day > size_t(DaysInMonth(year, month))) || hour > 24 ||
((hour == 24) && (min > 0 || sec > 0 || frac > 0)) || min > 59 ||
sec > 59 || tzHour > 23 || tzMin > 59) {
return false;
}
if (i != length) {
return false;
}
month -= 1; /* convert month to 0-based */
double msec = MakeDate(MakeDay(dateMul * double(year), month, day),
MakeTime(hour, min, sec, frac * 1000.0));
if (isLocalTime) {
msec = UTC(shouldRFP, msec);
} else {
msec -= tzMul * (tzHour * msPerHour + tzMin * msPerMinute);
}
*result = TimeClip(msec);
return NumbersAreIdentical(msec, result->toDouble());
#undef PEEK
#undef NEED
#undef DONE_UNLESS
#undef NEED_NDIGITS
#undef NEED_NDIGITS_OR_LESS
}
struct CharsAndAction {
const char* chars;
int action;
};
static constexpr CharsAndAction keywords[] = {
// clang-format off
// AM/PM
{ "am", -1 },
{ "pm", -2 },
// Days of week.
{ "monday", 0 },
{ "tuesday", 0 },
{ "wednesday", 0 },
{ "thursday", 0 },
{ "friday", 0 },
{ "saturday", 0 },
{ "sunday", 0 },
// Months.
{ "january", 1 },
{ "february", 2 },
{ "march", 3 },
{ "april", 4, },
{ "may", 5 },
{ "june", 6 },
{ "july", 7 },
{ "august", 8 },
{ "september", 9 },
{ "october", 10 },
{ "november", 11 },
{ "december", 12 },
// Time zone abbreviations.
{ "gmt", 10000 + 0 },
{ "ut", 10000 + 0 },
{ "utc", 10000 + 0 },
{ "est", 10000 + 5 * 60 },
{ "edt", 10000 + 4 * 60 },
{ "cst", 10000 + 6 * 60 },
{ "cdt", 10000 + 5 * 60 },
{ "mst", 10000 + 7 * 60 },
{ "mdt", 10000 + 6 * 60 },
{ "pst", 10000 + 8 * 60 },
{ "pdt", 10000 + 7 * 60 },
// clang-format on
};
template <size_t N>
constexpr size_t MinKeywordLength(const CharsAndAction (&keywords)[N]) {
size_t min = size_t(-1);
for (const CharsAndAction& keyword : keywords) {
min = std::min(min, std::char_traits<char>::length(keyword.chars));
}
return min;
}
template <typename CharT>
static bool ParseDate(DateTimeInfo::ShouldRFP shouldRFP, const CharT* s,
size_t length, ClippedTime* result) {
if (ParseISOStyleDate(shouldRFP, s, length, result)) {
return true;
}
if (length == 0) {
return false;
}
int year = -1;
int mon = -1;
int mday = -1;
int hour = -1;
int min = -1;
int sec = -1;
int tzOffset = -1;
// One of '+', '-', ':', '/', or 0 (the default value).
int prevc = 0;
bool seenPlusMinus = false;
bool seenMonthName = false;
bool seenFullYear = false;
bool negativeYear = false;
size_t index = 0;
while (index < length) {
int c = s[index];
index++;
// Normalize U+202F (NARROW NO-BREAK SPACE). This character appears between
// the AM/PM markers for |date.toLocaleString("en")|. We have to normalize
// it for backward compatibility reasons.
if (c == 0x202F) {
c = ' ';
}
// Spaces, ASCII control characters, and commas are simply ignored.
if (c <= ' ' || c == ',') {
continue;
}
// Parse delimiter characters. Save them to the side for future use.
if (c == '/' || c == ':' || c == '+') {
prevc = c;
continue;
}
// Dashes are delimiters if they're immediately followed by a number field.
// If they're not followed by a number field, they're simply ignored.
if (c == '-') {
if (index < length && IsAsciiDigit(s[index])) {
prevc = c;
}
continue;
}
// Skip over comments -- text inside matching parentheses. (Comments
// themselves may contain comments as long as all the parentheses properly
// match up. And apparently comments, including nested ones, may validly be
// terminated by end of input...)
if (c == '(') {
int depth = 1;
while (index < length) {
c = s[index];
index++;
if (c == '(') {
depth++;
} else if (c == ')') {
if (--depth <= 0) {
break;
}
}
}
continue;
}
// Parse a number field.
if (IsAsciiDigit(c)) {
size_t partStart = index - 1;
uint32_t u = c - '0';
while (index < length) {
c = s[index];
if (!IsAsciiDigit(c)) {
break;
}
u = u * 10 + (c - '0');
index++;
}
size_t partLength = index - partStart;
// See above for why we have to normalize U+202F.
if (c == 0x202F) {
c = ' ';
}
int n = int(u);
/*
* Allow TZA before the year, so 'Wed Nov 05 21:49:11 GMT-0800 1997'
* works.
*
* Uses of seenPlusMinus allow ':' in TZA, so Java no-timezone style
* of GMT+4:30 works.
*/
if (prevc == '-' && (tzOffset != 0 || seenPlusMinus) && partLength >= 4 &&
year < 0) {
// Parse as a negative, possibly zero-padded year if
// 1. the preceding character is '-',
// 2. the TZA is not 'GMT' (tested by |tzOffset != 0|),
// 3. or a TZA was already parsed |seenPlusMinus == true|,
// 4. the part length is at least 4 (to parse '-08' as a TZA),
// 5. and we did not already parse a year |year < 0|.
year = n;
seenFullYear = true;
negativeYear = true;
} else if ((prevc == '+' || prevc == '-') /* && year>=0 */) {
/* Make ':' case below change tzOffset. */
seenPlusMinus = true;
/* offset */
if (n < 24 && partLength <= 2) {
n = n * 60; /* EG. "GMT-3" */
} else {
n = n % 100 + n / 100 * 60; /* eg "GMT-0430" */
}
if (prevc == '+') /* plus means east of GMT */
n = -n;
// Reject if not preceded by 'GMT' or if a time zone offset
// was already parsed.
if (tzOffset != 0 && tzOffset != -1) {
return false;
}
tzOffset = n;
} else if (prevc == '/' && mon >= 0 && mday >= 0 && year < 0) {
if (c <= ' ' || c == ',' || c == '/' || index >= length) {
year = n;
} else {
return false;
}
} else if (c == ':') {
if (hour < 0) {
hour = /*byte*/ n;
} else if (min < 0) {
min = /*byte*/ n;
} else {
return false;
}
} else if (c == '/') {
/*
* Until it is determined that mon is the actual month, keep
* it as 1-based rather than 0-based.
*/
if (mon < 0) {
mon = /*byte*/ n;
} else if (mday < 0) {
mday = /*byte*/ n;
} else {
return false;
}
} else if (index < length && c != ',' && c > ' ' && c != '-' &&
c != '(') {
return false;
} else if (seenPlusMinus && n < 60) { /* handle GMT-3:30 */
if (tzOffset < 0) {
tzOffset -= n;
} else {
tzOffset += n;
}
} else if (hour >= 0 && min < 0) {
min = /*byte*/ n;
} else if (prevc == ':' && min >= 0 && sec < 0) {
sec = /*byte*/ n;
} else if (mon < 0) {
mon = /*byte*/ n;
} else if (mon >= 0 && mday < 0) {
mday = /*byte*/ n;
} else if (mon >= 0 && mday >= 0 && year < 0) {
year = n;
seenFullYear = partLength >= 4;
} else {
return false;
}
prevc = 0;
continue;
}
// Parse fields that are words: ASCII letters spelling out in English AM/PM,
// day of week, month, or an extremely limited set of legacy time zone
// abbreviations.
if (IsAsciiAlpha(c)) {
size_t start = index - 1;
while (index < length) {
c = s[index];
if (!IsAsciiAlpha(c)) {
break;
}
index++;
}
// There must be at least as many letters as in the shortest keyword.
constexpr size_t MinLength = MinKeywordLength(keywords);
if (index - start < MinLength) {
return false;
}
auto IsPrefixOfKeyword = [](const CharT* s, size_t len,
const char* keyword) {
while (len > 0 && *keyword) {
MOZ_ASSERT(IsAsciiAlpha(*s));
MOZ_ASSERT(IsAsciiLowercaseAlpha(*keyword));
if (unicode::ToLowerCase(static_cast<Latin1Char>(*s)) != *keyword) {
break;
}
s++, keyword++;
len--;
}
return len == 0;
};
size_t k = std::size(keywords);
while (k-- > 0) {
const CharsAndAction& keyword = keywords[k];
// If the field isn't a prefix of the keyword (an exact match is *not*
// required), try the next one.
if (!IsPrefixOfKeyword(s + start, index - start, keyword.chars)) {
continue;
}
int action = keyword.action;
// Completely ignore days of the week, and don't derive any semantics
// from them.
if (action == 0) {
break;
}
// Perform action tests from smallest action values to largest.
// Adjust a previously-specified hour for AM/PM accordingly (taking care
// to treat 12:xx AM as 00:xx, 12:xx PM as 12:xx).
if (action < 0) {
MOZ_ASSERT(action == -1 || action == -2);
if (hour > 12 || hour < 0) {
return false;
}
if (action == -1 && hour == 12) {
hour = 0;
} else if (action == -2 && hour != 12) {
hour += 12;
}
break;
}
// Record a month if none has been seen before. (Note that some numbers
// are initially treated as months; if a numeric field has already been
// interpreted as a month, store that value to the actually appropriate
// date component and set the month here.
if (action <= 12) {
if (seenMonthName) {
return false;
}
seenMonthName = true;
if (mon < 0) {
mon = action;
} else if (mday < 0) {
mday = mon;
mon = action;
} else if (year < 0) {
if (mday > 0) {
// If the date is of the form f l month, then when month is
// reached we have f in mon and l in mday. In order to be
// consistent with the f month l and month f l forms, we need to
// swap so that f is in mday and l is in year.
year = mday;
mday = mon;
} else {
year = mon;
}
mon = action;
} else {
return false;
}
break;
}
// Finally, record a time zone offset.
MOZ_ASSERT(action >= 10000);
tzOffset = action - 10000;
break;
}
if (k == size_t(-1)) {
return false;
}
prevc = 0;
continue;
}
// Any other character fails to parse.
return false;
}
if (year < 0 || mon < 0 || mday < 0) {
return false;
}
/*
* Case 1. The input string contains an English month name.
* The form of the string can be month f l, or f month l, or
* f l month which each evaluate to the same date.
* If f and l are both greater than or equal to 100 the date
* is invalid.
*
* The year is taken to be either l, f if f > 31, or whichever
* is set to zero.
*
* Case 2. The input string is of the form "f/m/l" where f, m and l are
* integers, e.g. 7/16/45. mon, mday and year values are adjusted
* to achieve Chrome compatibility.
*
* a. If 0 < f <= 12 and 0 < l <= 31, f/m/l is interpreted as
* month/day/year.
* b. If 31 < f and 0 < m <= 12 and 0 < l <= 31 f/m/l is
* interpreted as year/month/day
*/
if (seenMonthName) {
if (mday >= 100 && mon >= 100) {
return false;
}
if (year > 0 && (mday == 0 || mday > 31) && !seenFullYear) {
int temp = year;
year = mday;
mday = temp;
}
if (mday <= 0 || mday > 31) {
return false;
}
} else if (0 < mon && mon <= 12 && 0 < mday && mday <= 31) {
/* (a) month/day/year */
} else {
/* (b) year/month/day */
if (mon > 31 && mday <= 12 && year <= 31 && !seenFullYear) {
int temp = year;
year = mon;
mon = mday;
mday = temp;
} else {
return false;
}
}
// If the year is greater than or equal to 50 and less than 100, it is
// considered to be the number of years after 1900. If the year is less
// than 50 it is considered to be the number of years after 2000,
// otherwise it is considered to be the number of years after 0.
if (!seenFullYear) {
if (year < 50) {
year += 2000;
} else if (year >= 50 && year < 100) {
year += 1900;
}
}
if (negativeYear) {
year = -year;
}
mon -= 1; /* convert month to 0-based */
if (sec < 0) {
sec = 0;
}
if (min < 0) {
min = 0;
}
if (hour < 0) {
hour = 0;
}
double msec = MakeDate(MakeDay(year, mon, mday), MakeTime(hour, min, sec, 0));
if (tzOffset == -1) { /* no time zone specified, have to use local */
msec = UTC(shouldRFP, msec);
} else {
msec += tzOffset * msPerMinute;
}
*result = TimeClip(msec);
return true;
}
static bool ParseDate(DateTimeInfo::ShouldRFP shouldRFP, JSLinearString* s,
ClippedTime* result) {
AutoCheckCannotGC nogc;
return s->hasLatin1Chars()
? ParseDate(shouldRFP, s->latin1Chars(nogc), s->length(), result)
: ParseDate(shouldRFP, s->twoByteChars(nogc), s->length(), result);
}
static bool date_parse(JSContext* cx, unsigned argc, Value* vp) {
AutoJSMethodProfilerEntry pseudoFrame(cx, "Date", "parse");
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() == 0) {
args.rval().setNaN();
return true;
}
JSString* str = ToString<CanGC>(cx, args[0]);
if (!str) {
return false;
}
JSLinearString* linearStr = str->ensureLinear(cx);
if (!linearStr) {
return false;
}
ClippedTime result;
if (!ParseDate(ShouldRFP(cx->realm()), linearStr, &result)) {
args.rval().setNaN();
return true;
}
args.rval().set(TimeValue(result));
return true;
}
static ClippedTime NowAsMillis(JSContext* cx) {
if (js::SupportDifferentialTesting()) {
return TimeClip(0);
}
double now = PRMJ_Now();
bool clampAndJitter = cx->realm()->behaviors().clampAndJitterTime();
bool shouldResistFingerprinting =
cx->realm()->behaviors().shouldResistFingerprinting();
if (clampAndJitter && sReduceMicrosecondTimePrecisionCallback) {
now = sReduceMicrosecondTimePrecisionCallback(
now, shouldResistFingerprinting, cx);
} else if (clampAndJitter && sResolutionUsec) {
double clamped = floor(now / sResolutionUsec) * sResolutionUsec;
if (sJitter) {
// Calculate a random midpoint for jittering. In the browser, we are
// adversarial: Web Content may try to calculate the midpoint themselves
// and use that to bypass it's security. In the JS Shell, we are not
// adversarial, we want to jitter the time to recreate the operating
// environment, but we do not concern ourselves with trying to prevent an
// attacker from calculating the midpoint themselves. So we use a very
// simple, very fast CRC with a hardcoded seed.
uint64_t midpoint = BitwiseCast<uint64_t>(clamped);
midpoint ^= 0x0F00DD1E2BAD2DED; // XOR in a 'secret'
// MurmurHash3 internal component from
midpoint ^= midpoint >> 33;
midpoint *= uint64_t{0xFF51AFD7ED558CCD};
midpoint ^= midpoint >> 33;
midpoint *= uint64_t{0xC4CEB9FE1A85EC53};
midpoint ^= midpoint >> 33;
midpoint %= sResolutionUsec;
if (now > clamped + midpoint) { // We're jittering up to the next step
now = clamped + sResolutionUsec;
} else { // We're staying at the clamped value
now = clamped;
}
} else { // No jitter, only clamping
now = clamped;
}
}
return TimeClip(now / PRMJ_USEC_PER_MSEC);
}
bool js::date_now(JSContext* cx, unsigned argc, Value* vp) {
AutoJSMethodProfilerEntry pseudoFrame(cx, "Date", "now");
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().set(TimeValue(NowAsMillis(cx)));
return true;
}
DateTimeInfo::ShouldRFP DateObject::shouldRFP() const {
return ShouldRFP(realm());
}
void DateObject::setUTCTime(ClippedTime t) {
for (size_t ind = COMPONENTS_START_SLOT; ind < RESERVED_SLOTS; ind++) {
setReservedSlot(ind, UndefinedValue());
}
setFixedSlot(UTC_TIME_SLOT, TimeValue(t));
}
void DateObject::setUTCTime(ClippedTime t, MutableHandleValue vp) {
setUTCTime(t);
vp.set(TimeValue(t));
}
void DateObject::fillLocalTimeSlots() {
const int32_t utcTZOffset =
DateTimeInfo::utcToLocalStandardOffsetSeconds(shouldRFP());
/* Check if the cache is already populated. */
if (!getReservedSlot(LOCAL_TIME_SLOT).isUndefined() &&
getReservedSlot(UTC_TIME_ZONE_OFFSET_SLOT).toInt32() == utcTZOffset) {
return;
}
/* Remember time zone used to generate the local cache. */
setReservedSlot(UTC_TIME_ZONE_OFFSET_SLOT, Int32Value(utcTZOffset));
double utcTime = UTCTime().toNumber();
if (!IsFinite(utcTime)) {
for (size_t ind = COMPONENTS_START_SLOT; ind < RESERVED_SLOTS; ind++) {
setReservedSlot(ind, DoubleValue(utcTime));
}
return;
}
double localTime = LocalTime(shouldRFP(), utcTime);
setReservedSlot(LOCAL_TIME_SLOT, DoubleValue(localTime));
int year = (int)floor(localTime / (msPerDay * 365.2425)) + 1970;
double yearStartTime = TimeFromYear(year);
/* Adjust the year in case the approximation was wrong, as in YearFromTime. */
int yearDays;
if (yearStartTime > localTime) {
year--;
yearStartTime -= (msPerDay * DaysInYear(year));
yearDays = DaysInYear(year);
} else {
yearDays = DaysInYear(year);
double nextStart = yearStartTime + (msPerDay * yearDays);
if (nextStart <= localTime) {
year++;
yearStartTime = nextStart;
yearDays = DaysInYear(year);
}
}
setReservedSlot(LOCAL_YEAR_SLOT, Int32Value(year));
uint64_t yearTime = uint64_t(localTime - yearStartTime);
int yearSeconds = uint32_t(yearTime / 1000);
int day = yearSeconds / int(SecondsPerDay);
int step = -1, next = 30;
int month;
do {
if (day <= next) {
month = 0;
break;
}
step = next;
next += ((yearDays == 366) ? 29 : 28);
if (day <= next) {
month = 1;
break;
}
step = next;