Permalink
Fetching contributors…
Cannot retrieve contributors at this time
1877 lines (1647 sloc) 59.3 KB
//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
#include "RuntimeLibraryPch.h"
#include "DateImplementationData.h"
#include "CharClassifier.h"
namespace Js {
static double ConvertToInteger(double dbl)
{
Assert(Js::NumberUtilities::IsFinite(dbl));
if (Js::NumberUtilities::LuHiDbl(dbl) & 0x80000000)
{
Js::NumberUtilities::LuHiDbl(dbl) &= 0x7FFFFFFF;
dbl = floor(dbl);
Js::NumberUtilities::LuHiDbl(dbl) |= 0x80000000;
}
else
{
dbl = floor(dbl);
// We have to do this because some implementations map 0.5 to -0.
Js::NumberUtilities::LuHiDbl(dbl) &= 0x7FFFFFFF;
}
return dbl;
}
static const double kdblHalfSecond = 0.5 / 86400.0;
struct SZS
{
const char16 *psz; // string
short cch; // length of string
short szst; // type of entry
int32 lwVal; // value
};
BEGIN_ENUM_BYTE(ParseStringTokenType)
AmPm,
Month,
Day,
Zone,
BcAd,
END_ENUM_BYTE()
const static SZS g_rgszs[] =
{
#define Szs(sz, val) { _u(sz), _countof(_u(sz)) - 1, ParseStringTokenType::##szst, val }
// bc and ad
#undef szst
#define szst BcAd
Szs("bc", -1),
Szs("b.c", -1),
Szs("ad", +1),
Szs("a.d", +1),
// am and pm
#undef szst
#define szst AmPm
Szs("am", -1),
Szs("a.m", -1),
Szs("pm", +1),
Szs("p.m", +1),
// time zones
#undef szst
#define szst Zone
Szs("est", -5 * 60),
Szs("edt", -4 * 60),
Szs("cst", -6 * 60),
Szs("cdt", -5 * 60),
Szs("mst", -7 * 60),
Szs("mdt", -6 * 60),
Szs("pst", -8 * 60),
Szs("pdt", -7 * 60),
Szs("gmt", 0),
Szs("utc", 0),
// week days
#undef szst
#define szst Day
Szs("sunday", 0),
Szs("monday", 1),
Szs("tuesday", 2),
Szs("wednesday", 3),
Szs("thursday", 4),
Szs("friday", 5),
Szs("saturday", 6),
// months
#undef szst
#define szst Month
Szs("january", 0),
Szs("february", 1),
Szs("march", 2),
Szs("april", 3),
Szs("may", 4),
Szs("june", 5),
Szs("july", 6),
Szs("august", 7),
Szs("september", 8),
Szs("october", 9),
Szs("november", 10),
Szs("december", 11),
#undef szst
#undef Szs
};
const int32 kcszs = sizeof(g_rgszs) / sizeof(SZS);
///----------------------------------------------------------------------------
///----------------------------------------------------------------------------
///
/// class DateImplementation
///
///----------------------------------------------------------------------------
///----------------------------------------------------------------------------
DateImplementation::DateImplementation(double value)
{
// Assume DateImplementation is allocated in the recycler and is zero initialized
// Do not stack allocate of this struct, as it doesn't initialize all fields.
// If the stack allocated struct is copied in to recycler allocated ones, it
// many introduce false reference from the stack
Assert(!ThreadContext::IsOnStack(this));
AssertValue<byte>(this, 0, sizeof(DateImplementation));
m_modified = false;
SetTvUtc(value);
};
double
DateImplementation::GetMilliSeconds()
{
return m_tvUtc;
}
double
DateImplementation::NowFromHiResTimer(ScriptContext* scriptContext)
{
// Use current time.
return scriptContext->GetThreadContext()->GetHiResTimer()->Now();
}
double
DateImplementation::NowInMilliSeconds(ScriptContext * scriptContext)
{
return DoubleToTvUtc(DateImplementation::NowFromHiResTimer(scriptContext));
}
JavascriptString*
DateImplementation::GetString(DateStringFormat dsf,
ScriptContext* requestContext, DateTimeFlag noDateTime)
{
if (JavascriptNumber::IsNan(m_tvUtc))
{
return requestContext->GetLibrary()->GetInvalidDateString();
}
switch (dsf)
{
default:
EnsureYmdLcl(requestContext);
return GetDateDefaultString(&m_ymdLcl, &m_tzd, noDateTime, requestContext);
#ifdef ENABLE_GLOBALIZATION
case DateStringFormat::Locale:
EnsureYmdLcl(requestContext);
if( m_ymdLcl.year > 1600 && m_ymdLcl.year < 10000 )
{
// The year falls in the range which can be handled by both the Win32
// function GetDateFormat and the COM+ date type
// - the latter is for forward compatibility with JS 7.
JavascriptString *bs = GetDateLocaleString(&m_ymdLcl, &m_tzd, noDateTime, requestContext);
if (bs != nullptr)
{
return bs;
}
else
{
return GetDateDefaultString(&m_ymdLcl, &m_tzd, noDateTime, requestContext);
}
}
else
{
return GetDateDefaultString(&m_ymdLcl, &m_tzd, noDateTime, requestContext);
}
#endif
case DateStringFormat::GMT:
EnsureYmdUtc();
return GetDateGmtString(&m_ymdUtc, requestContext);
}
}
JavascriptString*
DateImplementation::GetISOString(ScriptContext* requestContext)
{
// ES5 15.9.5.43: throw RangeError if time value is not a finite number
if (!Js::NumberUtilities::IsFinite(m_tvUtc))
{
JavascriptError::ThrowRangeError(requestContext, JSERR_NeedNumber);
}
CompoundString *const bs = CompoundString::NewWithCharCapacity(30, requestContext->GetLibrary());
GetDateComponent(bs, DateData::FullYear, 0, requestContext);
bs->AppendChars(_u('-'));
// month
GetDateComponent(bs, DateData::Month, 1/*adjustment*/, requestContext);
bs->AppendChars(_u('-'));
// date
GetDateComponent(bs, DateData::Date, 0, requestContext);
bs->AppendChars(_u('T'));
// hours
GetDateComponent(bs, DateData::Hours, 0, requestContext);
bs->AppendChars(_u(':'));
// minutes
GetDateComponent(bs, DateData::Minutes, 0, requestContext);
bs->AppendChars(_u(':'));
// seconds
GetDateComponent(bs, DateData::Seconds, 0, requestContext);
// ES5 fill in milliseconds but v5.8 does not
bs->AppendChars(_u('.'));
// milliseconds
GetDateComponent(bs, DateData::Milliseconds, 0, requestContext);
bs->AppendChars(_u('Z'));
return bs;
}
void
DateImplementation::GetDateComponent(CompoundString *bs, DateData componentType, int adjust,
ScriptContext* requestContext)
{
double value = this->GetDateData(componentType, true /* fUTC */, requestContext);
if(Js::NumberUtilities::IsFinite(value))
{
const int ival = (int)value + adjust;
const int ivalAbs = ival < 0 ? -ival : ival;
switch(componentType)
{
case DateData::FullYear:
if(ival < 0 || ival > 9999)
{
// ES5 spec section 15.9.1.15.1 states that for years outside the range 0-9999, the expanded year
// representation should:
// - always include the sign
// - have 2 extra digits (6 digits total)
bs->AppendChars(ival < 0 ? _u('-') : _u('+'));
if(ivalAbs < 100000)
{
bs->AppendChars(_u('0'));
if(ivalAbs < 10000)
{
bs->AppendChars(_u('0'));
}
}
}
// Years are zero-padded to at least 4 digits in ES5
if(ivalAbs < 1000)
{
bs->AppendChars(_u('0'));
// will fall through to next case for additional padding
}
else
{
break;
}
// fall through
case DateData::Milliseconds:
if (ivalAbs < 100)
{
bs->AppendChars(_u('0'));
// will fall through to next case for additional padding
}
else
{
break;
}
// fall through
default:
if (ivalAbs < 10)
{
bs->AppendChars(_u('0'));
}
}
// _itow_s makes use of max 12 bytes for a base-10 32-bit int (_u("-2147483648\0")), although we don't need the sign
// and our numbers shouldn't be that big anyway
bs->AppendChars(
ivalAbs,
10,
[](const int value, char16 *const buffer, const CharCount charCapacity)
{
errno_t err = _itow_s(value, buffer, charCapacity, 10);
Assert(err == 0);
});
}
}
///----------------------------------------------------------------------------
///
/// Use tv as the UTC time.
///
///----------------------------------------------------------------------------
void
DateImplementation::SetTvUtc(double tv)
{
m_grfval = 0;
m_tvUtc = DoubleToTvUtc(tv);
}
double
DateImplementation::DoubleToTvUtc(double tv)
{
if (JavascriptNumber::IsNan(tv) || tv < ktvMin || tv > ktvMax)
{
return JavascriptNumber::NaN;
}
return CONFIG_FLAG(HighPrecisionDate)? tv : ConvertToInteger(tv);
}
///----------------------------------------------------------------------------
///
/// Use tv as the local time and set m_tvUtc appropriately.
///
///----------------------------------------------------------------------------
void
DateImplementation::SetTvLcl(double tv, ScriptContext* requestContext)
{
m_grfval = 0;
m_tvUtc = GetTvUtc(tv, requestContext);
}
JavascriptString*
DateImplementation::ConvertVariantDateToString(double dbl, ScriptContext* scriptContext)
{
Js::DateImplementation::TZD tzd;
DateTime::YMD ymd;
double tv = Js::DateImplementation::GetTvUtc(Js::DateImplementation::JsLocalTimeFromVarDate(dbl), scriptContext);
tv = Js::DateImplementation::GetTvLcl(tv, scriptContext, &tzd);
if (Js::JavascriptNumber::IsNan(tv))
{
return JavascriptNumber::ToStringNan(scriptContext);
}
Js::DateImplementation::GetYmdFromTv(tv, &ymd);
return DateImplementation::GetDateDefaultString(&ymd, &tzd, 0, scriptContext);
}
JavascriptString*
DateImplementation::GetDateDefaultString(DateTime::YMD *pymd, TZD *ptzd,DateTimeFlag noDateTime,ScriptContext* scriptContext)
{
return GetDateDefaultString<CompoundString>(pymd, ptzd, noDateTime, scriptContext,
[=](CharCount capacity) -> CompoundString*
{
return CompoundString::NewWithCharCapacity(capacity, scriptContext->GetLibrary());
});
}
JavascriptString*
DateImplementation::GetDateGmtString(DateTime::YMD *pymd,ScriptContext* scriptContext)
{
// toUTCString() or toGMTString() will return for example:
// "Thu, 02 Feb 2012 09:02:03 GMT" for versions IE11 or above
CompoundString *const bs = CompoundString::NewWithCharCapacity(30, scriptContext->GetLibrary());
const auto ConvertUInt16ToString_ZeroPad_2 = [](const uint16 value, char16 *const buffer, const CharCount charCapacity)
{
const charcount_t cchWritten = NumberUtilities::UInt16ToString(value, buffer, charCapacity, 2);
Assert(cchWritten != 0);
};
const auto ConvertLongToString = [](const int32 value, char16 *const buffer, const CharCount charCapacity)
{
const errno_t err = _ltow_s(value, buffer, charCapacity, 10);
Assert(err == 0);
};
bs->AppendChars(g_rgpszDay[pymd->wday]);
bs->AppendChars(_u(", "));
// sz - as %02d - output is "01" to "31"
bs->AppendChars(static_cast<WORD>(pymd->mday + 1), 2, ConvertUInt16ToString_ZeroPad_2);
bs->AppendChars(_u(' '));
bs->AppendChars(g_rgpszMonth[pymd->mon]);
bs->AppendChars(_u(' '));
// Add the year.
if ((pymd->year) > 0)
{
bs->AppendChars(pymd->year, 10, ConvertUInt32ToString_ZeroPad_4);
}
else
{
int positiveYear = -(pymd->year); // pymd->year is negative
bs->AppendChars(_u('-'));
bs->AppendChars(positiveYear, 10, ConvertUInt32ToString_ZeroPad_4);
}
// Add the time.
bs->AppendChars(_u(' '));
// sz - as %02d - HOUR
bs->AppendChars(static_cast<WORD>(pymd->time / 3600000), 2, ConvertUInt16ToString_ZeroPad_2);
bs->AppendChars(_u(':'));
// sz - as %02d - MINUTE
bs->AppendChars(static_cast<WORD>((pymd->time / 60000) % 60), 2, ConvertUInt16ToString_ZeroPad_2);
bs->AppendChars(_u(':'));
// sz - as %02d - SECOND
bs->AppendChars(static_cast<WORD>((pymd->time / 1000) % 60), 2, ConvertUInt16ToString_ZeroPad_2);
bs->AppendChars(_u(' '));
bs->AppendChars(_u("GMT"));
return bs;
}
#ifdef ENABLE_GLOBALIZATION
JavascriptString*
DateImplementation::GetDateLocaleString(DateTime::YMD *pymd, TZD *ptzd, DateTimeFlag noDateTime,ScriptContext* scriptContext)
{
SYSTEMTIME st;
int cch;
int count = 0;
const int kcchMax = 256;
WCHAR wszBuf[kcchMax];
WCHAR *pwszBuf, *pToBeFreed = NULL, *p;
JavascriptString *bs = nullptr;
// the caller of this function should ensure that the range of pymd->year is such that the following conversion works.
st.wYear = (WORD)pymd->year;
st.wMonth = (WORD)pymd->mon + 1;
st.wDayOfWeek = (WORD)pymd->wday;
st.wDay = (WORD)pymd->mday + 1;
st.wHour = (WORD)(pymd->time / 3600000);
st.wMinute = (WORD)((pymd->time / 60000) % 60);
st.wSecond = (WORD)((pymd->time / 1000) % 60);
st.wMilliseconds = (WORD)(pymd->time % 60);
cch = 0;
LCID lcid = GetUserDefaultLCID();
if( !(noDateTime & DateTimeFlag::NoDate))
{
DWORD dwFormat = DATE_LONGDATE;
if ((PRIMARYLANGID(LANGIDFROMLCID(lcid)) == LANG_ARABIC) ||
(PRIMARYLANGID(LANGIDFROMLCID(lcid)) == LANG_HEBREW))
{
dwFormat |= DATE_RTLREADING;
}
int c = GetDateFormatW( lcid, dwFormat, &st, NULL, NULL, 0 );
if( c <= 0 )
{
if (PRIMARYLANGID(LANGIDFROMLCID(lcid)) == LANG_HEBREW)
{
// Can't support some Hebrew dates - current limit is 1 Jan AD 2240
Js::JavascriptError::ThrowRangeError(scriptContext, VBSERR_CantDisplayDate);
}
AssertMsg(false, "GetDateFormat failed");
goto Error;
}
cch += c;
}
if( !(noDateTime & DateTimeFlag::NoTime))
{
int c = GetTimeFormatW( lcid, 0, &st, NULL, NULL, 0 );
if( c <= 0 )
{
AssertMsg(false, "GetTimeFormat failed");
goto Error;
}
cch += c;
}
cch++; // For the space between the date and the time.
if( cch > kcchMax )
{
pwszBuf = pToBeFreed = (WCHAR *)malloc( cch * sizeof(WCHAR) );
if(!pwszBuf)
{
Js::JavascriptError::ThrowOutOfMemoryError(scriptContext);
}
}
else
{
wszBuf[0] = '\0';
pwszBuf = wszBuf;
}
count = cch;
p = pwszBuf;
if( !(noDateTime & DateTimeFlag::NoDate))
{
DWORD dwFormat = DATE_LONGDATE;
if ((PRIMARYLANGID(LANGIDFROMLCID(lcid)) == LANG_ARABIC) ||
(PRIMARYLANGID(LANGIDFROMLCID(lcid)) == LANG_HEBREW))
{
dwFormat |= DATE_RTLREADING;
}
int c = GetDateFormatW( lcid, dwFormat, &st, NULL, p, cch );
if( c <= 0 || c > cch)
{
if (PRIMARYLANGID(LANGIDFROMLCID(lcid)) == LANG_HEBREW)
{
// Can't support some Hebrew dates - current limit is 1 Jan AD 2240
Js::JavascriptError::ThrowRangeError(scriptContext, VBSERR_CantDisplayDate);
}
AssertMsg(false, "GetDateFormat failed");
goto Error;
}
p += (c-1);
cch -= (c-1);
if( !(noDateTime & DateTimeFlag::NoTime))
{
*p++ = _u(' ');
cch--;
}
}
if( !(noDateTime & DateTimeFlag::NoTime))
{
int c = GetTimeFormatW( lcid, 0, &st, NULL, p, cch );
Assert( c > 0 );
if( c <= 0 )
{
AssertMsg(false, "GetTimeFormat failed");
goto Error;
}
cch -= (c-1);
}
bs = JavascriptString::NewCopyBuffer(pwszBuf, count-cch, scriptContext);
Error:
if( pToBeFreed )
free(pToBeFreed);
return bs;
}
#endif // ENABLE_GLOBALIZATION
double
DateImplementation::GetDateData(DateData dd, bool fUtc, ScriptContext* scriptContext)
{
DateTime::YMD *pymd;
double value = 0;
if (JavascriptNumber::IsNan(m_tvUtc))
{
return m_tvUtc;
}
if (fUtc)
{
EnsureYmdUtc();
pymd = &m_ymdUtc;
}
else
{
EnsureYmdLcl(scriptContext);
pymd = &m_ymdLcl;
}
switch (dd)
{
case DateData::Year:
Assert(scriptContext);
// WOOB bug 1099381: ES5 spec B.2.4: getYear() must return YearFromTime() - 1900.
// Note that negative value is OK for the spec.
value = pymd->year - 1900;
break;
case DateData::FullYear:
value = pymd->year;
break;
case DateData::Month:
value = pymd->mon;
break;
case DateData::Date:
value = pymd->mday + 1;
break;
case DateData::Day:
value = pymd->wday;
break;
case DateData::Hours:
value = (pymd->time / 3600000);
break;
case DateData::Minutes:
value = (pymd->time / 60000) % 60;
break;
case DateData::Seconds:
value = (pymd->time / 1000) % 60;
break;
case DateData::Milliseconds:
value = pymd->time % 1000;
break;
case DateData::TimezoneOffset:
//Assert(!fUtc);
value = (m_tvUtc - m_tvLcl) / 60000;
break;
default:
// Shouldn't come here
AssertMsg(false, "DateData type invalid");
}
return value;
}
inline bool
DateImplementation::FBig(char16 ch)
{
return (unsigned int)ch >= 128;
}
inline bool
DateImplementation::FDateDelimiter(char16 ch)
{
return (ch == '/' || ch == '-');
}
///------------------------------------------------------------------------------
// Parse a string as a date and return the number of milliseconds since
// January 1, 1970 GMT.
//
// TODO: This function is ported from IE8 jscript engine. This lengthy
// and needs a cleanup - break into smaller functions.
///------------------------------------------------------------------------------
double DateImplementation::UtcTimeFromStr(ScriptContext *scriptContext, JavascriptString *pParseString)
{
Assert(pParseString != nullptr);
double dbl;
if (scriptContext->GetLastUtcTimeFromStr(pParseString, dbl))
{
return dbl;
}
unsigned int ulength = pParseString->GetLength();
const char16 *psz = pParseString->GetSz();
if(UtcTimeFromStrCore(psz, ulength, dbl, scriptContext))
{
scriptContext->SetLastUtcTimeFromStr(pParseString, dbl);
return dbl;
}
Js::JavascriptError::ThrowOutOfMemoryError(scriptContext);
}
bool DateImplementation::TryParseDecimalDigits(
const char16 *const str,
const size_t length,
const size_t startIndex,
const size_t numDigits,
int &value)
{
Assert(str);
Assert(length);
Assert(startIndex <= length);
Assert(numDigits != 0 && numDigits <= 9); // will fit in an 'int'
if(numDigits > length - startIndex)
return false;
size_t i = 0;
// Skip leading zeroes
while(str[startIndex + i] == _u('0') && ++i < numDigits);
// Parse remaining digits
int v = 0;
for(; i < numDigits; ++i)
{
const unsigned short d = str[startIndex + i] - _u('0');
if(d > 9)
break;
v = v * 10 + d;
}
if(i < numDigits)
return false;
Assert(i == numDigits);
value = v;
// The next character must not be a digit
return !(i < length - startIndex && static_cast<unsigned short>(str[startIndex + i] - _u('0')) <= 9);
}
// Either 1 digit or 2 digits or 3 digits
// Ignore any digits after the third
bool DateImplementation::TryParseMilliseconds(
const char16 *const str,
const size_t length,
const size_t startIndex,
int &value,
size_t &foundDigits)
{
const size_t minNumDigits = 1;
Assert(str);
Assert(length);
Assert(startIndex <= length);
size_t allDigits = length - startIndex;
if(allDigits < minNumDigits)
return false;
size_t i = 0;
// Skip leading zeroes
while(str[startIndex + i] == _u('0') && ++i < allDigits);
// Parse remaining digits
int v = 0;
for(; i < allDigits ; ++i)
{
const unsigned short d = str[startIndex + i] - _u('0');
if(d > 9)
break;
if (i < 3) // not past the 3rd digit in the milliseconds, don't ignore
v = v * 10 + d;
}
if(i < minNumDigits)
return false;
foundDigits = i;
if (foundDigits == 1)
v = v * 100;
else if (foundDigits == 2)
v = v * 10;
value = v;
// The next character must not be a digit
return !(i < length - startIndex && static_cast<unsigned short>(str[startIndex + i] - _u('0')) <= 9);
}
bool DateImplementation::TryParseTwoDecimalDigits(
const char16 *const str,
const size_t length,
const size_t startIndex,
int &value,
bool canHaveTrailingDigit /* = false */)
{
Assert(str);
Assert(length);
Assert(startIndex <= length);
if(length - startIndex < 2)
return false;
unsigned short d = str[startIndex] - _u('0');
if(d > 9)
return false;
short v = d * 10;
d = str[startIndex + 1] - _u('0');
if(d > 9)
return false;
value = v + d;
// The next character must not be a digit if canHaveTrailingDigit is false
bool hasNoTrailingDigit = !(length - startIndex > 2 && static_cast<unsigned short>(str[startIndex + 2] - _u('0')) <= 9);
return canHaveTrailingDigit || hasNoTrailingDigit;
}
bool DateImplementation::TryParseIsoString(const char16 *const str, const size_t length, double &timeValue, ScriptContext *scriptContext)
{
Assert(str);
size_t i = 0;
const Js::CharClassifier *classifier = scriptContext->GetCharClassifier();
// Skip leading whitespace (for cross-browser compatibility)
// Also skip bidirectional characters, for Round tripping locale formatted date
while ((classifier->IsWhiteSpace(str[i]) || classifier->IsBiDirectionalChar(str[i])) && ++i < length);
// Minimum length must be 4 (YYYY)
if(length - i < 4)
return false;
// YYYY|(+|-)YYYYYY
int year;
switch(str[i])
{
case _u('+'):
++i;
if(!TryParseDecimalDigits(str, length, i, 6, year))
return false;
i += 6;
break;
case _u('-'):
++i;
if(!TryParseDecimalDigits(str, length, i, 6, year) || year == 0)
return false;
year = -year;
i += 6;
break;
case _u('0'):
case _u('1'):
case _u('2'):
case _u('3'):
case _u('4'):
case _u('5'):
case _u('6'):
case _u('7'):
case _u('8'):
case _u('9'):
if(!TryParseDecimalDigits(str, length, i, 4, year))
return false;
i += 4;
break;
default:
return false;
}
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
int month = 0,
day = 0,
timePortionMilliseconds = 0,
utcOffsetMilliseconds = 0;
bool isLocalTime = false;
do // while(false);
{
do // while(false);
{
// -MM
if(i >= length || str[i] != _u('-'))
break;
++i;
if(!TryParseTwoDecimalDigits(str, length, i, month))
return false;
--month;
if(month < 0 || month > 11)
return false;
i += 2;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
// -DD
if(i >= length || str[i] != _u('-'))
break;
++i;
if(!TryParseTwoDecimalDigits(str, length, i, day))
return false;
--day;
if(day < 0 || day > 30)
return false;
i += 2;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
} while(false);
// THH:mm
if(i >= length || str[i] != _u('T'))
break;
++i;
int t;
if(!TryParseTwoDecimalDigits(str, length, i, t) || t > 24)
return false;
timePortionMilliseconds += t * (60 * 60 * 1000);
i += 2;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
if(i >= length || str[i] != _u(':'))
return false;
++i;
if(!TryParseTwoDecimalDigits(str, length, i, t) || t > 59)
return false;
timePortionMilliseconds += t * (60 * 1000);
i += 2;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
do // while(false);
{
// :ss
if(i >= length || str[i] != _u(':'))
break;
++i;
if(!TryParseTwoDecimalDigits(str, length, i, t) || t > 59)
return false;
timePortionMilliseconds += t * 1000;
i += 2;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
// .sss
if(i >= length || str[i] != _u('.'))
break;
++i;
// Require one or more decimal digits. Ignore digits beyond the third
size_t foundDigits = 0;
if(!TryParseMilliseconds(str, length, i, t, foundDigits))
return false;
timePortionMilliseconds += t;
i += foundDigits;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
} while(false);
// Z|(+|-)HH:mm
if(i >= length)
{
isLocalTime = true;
break;
}
const char16 utcOffsetSign = str[i];
if(utcOffsetSign == _u('Z'))
{
++i;
break;
}
if(utcOffsetSign != _u('+') && utcOffsetSign != _u('-'))
{
isLocalTime = true;
break;
}
++i;
// In -version:6 we allow optional colons in the timezone offset
if (!TryParseTwoDecimalDigits(str, length, i, t, scriptContext->GetConfig()->IsES6DateParseFixEnabled() /* Timezone may be 4 sequential digits */) || t > 24)
return false;
utcOffsetMilliseconds += t * (60 * 60 * 1000);
i += 2;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
if(i >= length)
return false;
// The ':' is optional in ISO 8601
if (str[i] == _u(':'))
{
++i;
}
if(!TryParseTwoDecimalDigits(str, length, i, t) || t > 59)
return false;
utcOffsetMilliseconds += t * (60 * 1000);
i += 2;
// Skip bidirectional characters, for Round tripping locale formatted date
i += classifier->SkipBiDirectionalChars(str, i, length);
if(utcOffsetSign == _u('-'))
utcOffsetMilliseconds = -utcOffsetMilliseconds;
} while(false);
// Skip trailing whitespace (for cross-browser compatibility)
// Skip bidirectional characters, for Round tripping locale formatted date
while (i < length && (classifier->IsWhiteSpace(str[i]) || classifier->IsBiDirectionalChar(str[i])))
++i;
// There should only have been whitespace remaining, if any
if(i < length)
return false;
Assert(i == length);
// Compute the time value
timeValue = TvFromDate(year, month, day, timePortionMilliseconds - utcOffsetMilliseconds);
if (isLocalTime)
{
// Compatibility note:
// In ES5, it was unspecified how to handle date strings without the trailing time zone offset "Z|(+|-)HH:mm".
// In ES5.1, an absent time zone offset defaulted to "Z", which contradicted ISO8601:2004(E).
// This was corrected in an ES5.1 errata note. Moreover, the ES6 draft now follows ISO8601.
timeValue = GetTvUtc(timeValue, scriptContext);
}
return true;
}
bool DateImplementation::UtcTimeFromStrCore(
__in_ecount_z(ulength) const char16 *psz,
unsigned int ulength,
double &retVal,
ScriptContext *const scriptContext)
{
Assert(scriptContext);
if (ulength >= 0x7fffffff)
{
//Prevent unreasonable requests from causing overflows.
return false;
}
if (nullptr == psz)
{
retVal = JavascriptNumber::NaN;
return true;
}
// Try to parse the string as the ISO format first
if(TryParseIsoString(psz, ulength, retVal, scriptContext))
{
return true;
}
enum
{
ssNil,
ssMinutes,
ssSeconds,
ssMillisecond,
ssAddOffset,
ssSubOffset,
ssDate,
ssMonth,
ssYear
};
char16 *pchBase;
char16 *pch;
char16 ch;
char16 *pszSrc = nullptr;
const int32 lwNil = 0x80000000;
int32 cch;
int32 depth;
int32 lwT;
int32 lwYear = lwNil;
int32 lwMonth = lwNil;
int32 lwDate = lwNil;
int32 lwTime = lwNil;
int32 lwMillisecond = lwNil;
int32 lwZone = lwNil;
int32 lwOffset = lwNil;
int32 ss = ssNil;
const SZS *pszs;
bool fUtc;
int tAmPm = 0;
int tBcAd = 0;
size_t numOfDigits = 0;
double tv = JavascriptNumber::NaN; // Initialized for error handling.
//Create a copy to analyze
BEGIN_TEMP_ALLOCATOR(tempAllocator, scriptContext, _u("UtcTimeFromStr"));
pszSrc = AnewArray(tempAllocator, char16, ulength + 1);
size_t size = sizeof(char16) * (ulength + 1);
js_memcpy_s(pszSrc, size, psz, size);
_wcslwr_s(pszSrc,ulength+1);
bool isDateNegativeVersion5 = false;
bool isNextFieldDateNegativeVersion5 = false;
bool isZeroPaddedYear = false;
const Js::CharClassifier *classifier = scriptContext->GetCharClassifier();
#pragma prefast(suppress: __WARNING_INCORRECT_VALIDATION, "pch is guaranteed to be null terminated by __in_z on psz and js_memcpy_s copying the null byte")
for (pch = pszSrc; 0 != (ch = classifier->SkipBiDirectionalChars(pch));)
{
pch++;
if (ch <= ' ')
{
continue;
}
switch (ch)
{
case '(':
{
// skip stuff in parens
for (depth = 1; 0 != (ch = *pch); )
{
pch++;
if (ch == '(')
{
depth++;
}
else if (ch == ')' && --depth <= 0)
{
break;
}
}
continue;
}
case ',':
case ':':
case '/':
{
// ignore these
continue;
}
case '+':
{
if (lwNil != lwTime)
{
ss = ssAddOffset;
}
continue;
}
case '-':
{
if (lwNil != lwTime)
{
ss = ssSubOffset;
}
continue;
}
}
pchBase = pch - 1;
if (!FBig(ch) && isalpha(ch))
{
for ( ; !FBig(*pch) && (isalpha(*pch) || '.' == *pch); pch++)
;
cch = (int32)(pch - pchBase);
if ('.' == pchBase[cch - 1])
{
cch--;
}
//Assert(cch > 0);
// skip to the next real character
while (0 != (*pch) && (*pch <= ' ' || classifier->IsBiDirectionalChar(*pch)))
{
pch++;
}
// have an alphabetic token - look it up
if (cch == 1)
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
// military version of time zone
// z = GMT
// j isn't used
// a to m are 1 to 12
// n to y are -1 to -12
if (lwNil != lwZone)
{
goto LError;
}
if (ch <= 'm')
{
if (ch == 'j' || ch < 'a')
{
goto LError;
}
lwZone = (int32)(ch - 'a' + (ch < 'j')) * 60;
}
else if (ch <= 'y')
{
lwZone = -(int32)(ch - 'm') * 60;
}
else if (ch == 'z')
{
lwZone = 0;
}
else
{
goto LError;
}
// look for a time zone offset
ss = ('+' == *pch) ? (pch++, ssAddOffset) :
('-' == *pch) ? (pch++, ssSubOffset) : ssNil;
continue;
}
// look for a token
for (pszs = g_rgszs + kcszs; ; )
{
if (pszs-- <= g_rgszs)
goto LError;
if (cch <= pszs->cch &&
0 == memcmp(pchBase, pszs->psz, cch * sizeof(char16)))
{
break;
}
}
switch (pszs->szst)
{
case ParseStringTokenType::BcAd:
{
if (tBcAd != 0)
{
goto LError;
}
tBcAd = (int)pszs->lwVal;
break;
}
case ParseStringTokenType::AmPm:
{
if (tAmPm != 0)
{
goto LError;
}
tAmPm = (int)pszs->lwVal;
break;
}
case ParseStringTokenType::Month:
{
if (lwNil != lwMonth)
{
goto LError;
}
lwMonth = pszs->lwVal;
if ('-' == *pch)
{
// handle the case date is negative for "Thu, 23 Sep -0007 00:00:00 GMT"
isDateNegativeVersion5 = true;
pch++;
}
break;
}
case ParseStringTokenType::Zone:
{
if (lwNil != lwZone)
{
goto LError;
}
lwZone = pszs->lwVal;
// look for a time zone offset
ss = ('+' == *pch) ? (pch++, ssAddOffset) :
('-' == *pch) ? (pch++, ssSubOffset) : ssNil;
break;
}
}
continue;
}
if (FBig(ch) || !isdigit(ch))
{
goto LError;
}
for (lwT = ch - '0'; ; pch++)
{
// for time zone offset HH:mm, we already got HH so skip ':' and grab mm
if (((ss == ssAddOffset) || (ss == ssSubOffset)) && (*pch == ':'))
{
continue;
}
if (FBig(*pch) || !isdigit(*pch))
{
break;
}
// to avoid overflow
if (pch - pchBase > 6)
{
goto LError;
}
// convert string to number, e.g. 07:30 -> 730
lwT = lwT * 10 + *pch - '0';
}
numOfDigits = pch - pchBase;
// skip to the next real character
while (0 != (ch = *pch) && (ch <= ' ' || classifier->IsBiDirectionalChar(ch)))
{
pch++;
}
switch (ss)
{
case ssAddOffset:
case ssSubOffset:
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
if (lwNil != lwOffset)
goto LError;
// convert into minutes, e.g. 730 -> 7*60+30
lwOffset = lwT < 24 ? lwT * 60 :
(lwT % 100) + (lwT / 100) * 60;
if (ssSubOffset == ss)
lwOffset = -lwOffset;
lwZone = 0; // An offset is always with respect to UTC
ss = ssNil;
break;
}
case ssMinutes:
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
if (lwT >= 60)
goto LError;
lwTime += lwT * 60;
ss = (ch == ':') ? (pch++, ssSeconds) : ssNil;
break;
}
case ssSeconds:
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
if (lwT >= 60)
goto LError;
lwTime += lwT;
ss = (ch == '.') ? (pch++, ssMillisecond) : ssNil;
break;
}
case ssMillisecond:
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
if (numOfDigits <= 1)
{
// 1 digit only, treat it as hundreds
lwMillisecond = lwT * 100;
}
else if (numOfDigits <= 2)
{
// 2 digit only, treat it as tens
lwMillisecond = lwT * 10;
}
else if (numOfDigits <= 3)
{
// canonical 3 digit, per EcmaScript spec
lwMillisecond = lwT;
}
else
{
// ignore any digits beyond the third
lwMillisecond = (pchBase[0] - '0') * 100 + (pchBase[1] - '0') * 10 + (pchBase[2] - '0');
}
ss = ssNil;
break;
}
case ssDate:
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
if (lwNil != lwDate)
goto LError;
lwDate = lwT;
if ((lwNil == lwYear) && FDateDelimiter(ch))
{
// We have already parsed the year if the date is specified as YYYY/MM/DD,
// but not when it is specified as MM/DD/YYYY.
ss = ssYear;
pch++;
}
else
{
ss = ssNil;
}
break;
}
case ssMonth:
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
if (lwNil != lwMonth)
{
goto LError;
}
lwMonth = lwT - 1;
if (FDateDelimiter(ch))
{
// Mark the next token as the date so that it won't be confused with another token.
// For example, if the next character is '-' as in "2015-1-1", then it'd be used as
// the time offset without this.
ss = ssDate;
pch++;
}
break;
}
case ssYear:
{
AssertMsg(isNextFieldDateNegativeVersion5 == false, "isNextFieldDateNegativeVersion5 == false");
if (lwNil != lwYear)
goto LError;
AssertMsg(isDateNegativeVersion5 == false, "lwYear should be positive as pre-version:5 parsing");
lwYear = lwT;
ss = ssNil;
if (lwT < 1000 && numOfDigits >= 4)
{
isZeroPaddedYear = true;
}
break;
}
default:
{
// assumptions for getting a YEAR:
// - an absolute value greater or equal than 70 (thus not hour!)
// - wasn't preceded by negative sign for -version:5 year format
// - lwT has at least 4 digits (e.g. 0017 is year 17 AD)
if (lwT >= 70 || isNextFieldDateNegativeVersion5 || numOfDigits >= 4)
{
// assume it's a year - this is used particularly as version:5 year parsing
if (lwNil != lwYear)
goto LError;
// handle the case date is negative for "Tue Feb 02 -2012 01:02:03 GMT-0800"
lwYear = isDateNegativeVersion5 ? -lwT : lwT;
isNextFieldDateNegativeVersion5 = false;
if (lwT < 1000 && numOfDigits >= 4)
{
isZeroPaddedYear = true;
}
if (FDateDelimiter(ch))
{
// Mark the next token as the month so that it won't be confused with another token.
// For example, if the next character is '-' as in "2015-1-1", then it'd be used as
// the time offset without this.
ss = ssMonth;
pch++;
}
break;
}
switch (ch)
{
case ':':
{
// hour
if (lwNil != lwTime)
goto LError;
if (lwT >= 24)
goto LError;
lwTime = lwT * 3600;
ss = ssMinutes;
pch++;
break;
}
case '/':
case '-':
{
// month
if (lwNil != lwMonth)
{
// can be year
if (lwNil != lwYear)
{
// both were already parsed!
goto LError;
}
else
{
// this is a day - with the negative sign for the date (version 5+)
lwDate = lwT;
isDateNegativeVersion5 = true;
// mark the next field to be year (version 5+)
isNextFieldDateNegativeVersion5 = true;
}
}
else
{
// this is a month
lwMonth = lwT - 1;
ss = ssDate;
}
pch++;
break;
}
default:
{
// date
if (lwNil != lwDate)
goto LError;
lwDate = lwT;
break;
}
}
break;
}
}
continue;
}
if (lwNil == lwYear ||
lwNil == lwMonth || lwMonth > 11 ||
lwNil == lwDate || lwDate > 31)
{
goto LError;
}
if (tBcAd != 0)
{
if (tBcAd < 0)
{
// BC. Note that 1 BC is year 0 and 2 BC is year -1.
lwYear = -lwYear + 1;
}
}
else if (lwYear < 50 && isDateNegativeVersion5 == false && isZeroPaddedYear == false)
{
lwYear += 2000;
}
else if (lwYear < 100 && isDateNegativeVersion5 == false && isZeroPaddedYear == false)
{
lwYear += 1900;
}
if (tAmPm != 0)
{
if (lwNil == lwTime)
{
goto LError;
}
if (lwTime >= 12 * 3600L && lwTime < 13 * 3600L)
{
// In the 12:00 hour. AM means subtract 12 hours and PM means
// do nothing.
if (tAmPm < 0)
{
lwTime -= 12 * 3600L;
}
}
else
{
// Not in the 12:00 hour. AM means do nothing and PM means
// add 12 hours.
if (tAmPm > 0)
{
if (lwTime >= 12 * 3600L)
{
goto LError;
}
lwTime += 12 * 3600L;
}
}
}
else if (lwNil == lwTime)
{
lwTime = 0;
}
if (lwNil == lwMillisecond)
{
lwMillisecond = 0;
}
if (lwNil != lwZone)
{
lwTime -= lwZone * 60;
fUtc = TRUE;
}
else
{
fUtc = FALSE;
}
if (lwNil != lwOffset)
{
lwTime -= lwOffset * 60;
}
// Rebuild time.
tv = TvFromDate(lwYear, lwMonth, lwDate - 1, (double)lwTime * 1000 + lwMillisecond);
if (!fUtc)
{
tv = GetTvUtc(tv, scriptContext);
}
LError:
END_TEMP_ALLOCATOR(tempAllocator, scriptContext);
retVal = tv;
return true;
}
//------------------------------------
//Convert a utc time to a variant date.
//------------------------------------
double DateImplementation::VarDateFromJsUtcTime(double dbl, ScriptContext * scriptContext)
{
Assert(scriptContext);
// Convert to local time.
dbl = Js::DateImplementation::GetTvLcl(dbl, scriptContext);
if (!Js::NumberUtilities::IsFinite(dbl))
return Js::JavascriptNumber::NaN;
// Convert to an automation date.
dbl = dbl / 86400000 + g_kdblJanuary1st1970;
// dbl is the actual number of days since 0000h 12/30/1899.
// Convert this to a true Automation-style date.
if (dbl < 0.0)
{
// This works around a bug in OLE Automation.
// If a date is negative _and_ less than 500
// milliseconds before midnight then Automation will
// "round" it to two days earlier. To work around this
// bug, round dates just before midnight to midnight.
double dblT;
dbl = 2.0 * floor(dbl) - dbl;
dblT = dbl - floor(dbl);
if (dblT <= kdblHalfSecond && 0.0 < dblT)
dbl = ceil(dbl) + 1.0;
}
return dbl;
}
double DateImplementation::JsUtcTimeFromVarDate(double dbl, ScriptContext * scriptContext)
{
Assert(scriptContext);
return GetTvUtc(JsLocalTimeFromVarDate(dbl), scriptContext);
}
double DateImplementation::DateFncUTC(ScriptContext* scriptContext, Arguments args)
{
const int kcvarMax = 7;
double rgdbl[kcvarMax];
double tv;
double dblT;
uint ivar;
// See: https://github.com/Microsoft/ChakraCore/issues/1665
// Date.UTC should return NaN with 0 arguments.
// args.Info.Count includes an implicit first parameter, so we check for Count <= 1.
if (args.Info.Count <= 1)
{
return JavascriptNumber::NaN;
}
for (ivar = 0; (ivar < (args.Info.Count-1)) && ivar < kcvarMax; ++ivar)
{
rgdbl[ivar] = JavascriptConversion::ToNumber(args[ivar+1],scriptContext);
}
for (ivar = 0; ivar < kcvarMax; ivar++)
{
// Unspecified parameters are treated like zero, except date, which
// is treated as 1.
if (ivar >= (args.Info.Count - 1))
{
rgdbl[ivar] = (ivar == 2);
continue;
}
#pragma prefast(suppress:6001, "rgdbl index ivar < args.Info.Count - 1 are initialized")
dblT = rgdbl[ivar];
if (!Js::NumberUtilities::IsFinite(dblT))
{
return JavascriptNumber::NaN;
}
rgdbl[ivar] = ConvertToInteger(dblT);
}
// adjust the year
if (rgdbl[0] < 100 && rgdbl[0] >= 0)
{
rgdbl[0] += 1900;
}
// Get the UTC time value.
tv = TvFromDate(rgdbl[0], rgdbl[1], rgdbl[2] - 1,
rgdbl[3] * 3600000 + rgdbl[4] * 60000 + rgdbl[5] * 1000 + rgdbl[6]);
if (tv < ktvMin || tv > ktvMax)
{
return JavascriptNumber::NaN;
}
else
{
return tv;
}
}
// Maximum number of arguments used by this set operation.
static const int mpddcvar[] =
{
1, // Year
3, // FullYear
2, // Month
1, // Date
4, // Hours
3, // Minutes
2, // Seconds
1, // Milliseconds
0, // Day (shouldn't happen)
0, // TimezoneOffset (shouldn't happen)
};
double DateImplementation::SetDateData(Arguments args, DateData dd, bool fUtc, ScriptContext* scriptContext)
{
// This must accommodate the largest cvar in mpddcvar.
double rgdbl[5];
double tv = 0;
DateTime::YMD *pymd = NULL;
DateTime::YMD emptyYMD = {0};
uint count = 0;
uint cvarMax;
uint ivar;
// PREFAST: check limits
if (dd < 0 || dd >= DateData::Lim)
{
AssertMsg(false, "DateData type invalid");
Js::JavascriptError::ThrowError(scriptContext, VBSERR_InternalError);
}
// Get the parameters.
cvarMax = mpddcvar[dd];
__analysis_assume(cvarMax <= 4);
//
// arg[0] would be the date object itself
//
for (ivar = 0; (ivar < (args.Info.Count-1)) && ivar < cvarMax; ++ivar)
{
rgdbl[ivar] = JavascriptConversion::ToNumber(args[ivar+1],scriptContext);
if (!Js::NumberUtilities::IsFinite(rgdbl[ivar]))
goto LSetNan;
rgdbl[ivar] = ConvertToInteger(rgdbl[ivar]);
}
if ((count = ivar) < 1)
{
goto LSetNan;
}
if (JavascriptNumber::IsNan(m_tvUtc))
{
// If the current time is not finite, the only way we can end up
// with non-NaN is for setFullYear/setYear.
// See ES5 15.9.5.40, ES5 B.2.5.
if (!(DateData::FullYear == dd || DateData::Year == dd))
{
goto LSetNan;
}
pymd = &emptyYMD; // We need mon, mday, time to be 0.
// Fall through to DateData::Year and DataData::FullYear cases below.
}
else
{
if (fUtc)
{
EnsureYmdUtc();
pymd = &m_ymdUtc;
tv = m_tvUtc;
}
else
{
EnsureYmdLcl(scriptContext);
pymd = &m_ymdLcl;
tv = m_tvLcl;
}
}
ivar = 0;
switch (dd)
{
case DateData::Year:
if (rgdbl[0] < 100 && rgdbl[0] >= 0)
rgdbl[0] += 1900;
// fall-through
case DateData::FullYear:
LFullYear:
if (count < 3)
{
// Only {year} or {year, month} is specified. Day is not specified.
rgdbl[2] = pymd->mday + 1;
if (count < 2)
{
// Month is not specified.
rgdbl[1] = pymd->mon;
}
}
tv = TvFromDate(rgdbl[0], rgdbl[1], rgdbl[2] - 1, pymd->time);
break;
case DateData::Month:
memmove(rgdbl + 1, rgdbl, count * sizeof(double));
count++;
rgdbl[0] = pymd->year;
goto LFullYear;
case DateData::Date:
tv += (rgdbl[ivar] - pymd->mday - 1) * 86400000;
if (++ivar >= count)
{
break;
}
// fall-through
case DateData::Hours:
tv += (rgdbl[ivar] - (pymd->time / 3600000)) * 3600000;
if (++ivar >= count)
{
break;
}
// fall-through
case DateData::Minutes:
tv += (rgdbl[ivar] - (pymd->time / 60000) % 60) * 60000;
if (++ivar >= count)
{
break;
}
// fall-through
case DateData::Seconds:
tv += (rgdbl[ivar] - (pymd->time / 1000) % 60) * 1000;
if (++ivar >= count)
{
break;
}
// fall-through
case DateData::Milliseconds:
tv += rgdbl[ivar] - pymd->time % 1000;
break;
default:
AssertMsg(false, "DataData type invalid");
}
if (fUtc)
{
SetTvUtc(tv);
}
else
{
SetTvLcl(tv, scriptContext);
}
m_modified = true;
return m_tvUtc;
LSetNan:
m_grfval = 0;
m_tvUtc = JavascriptNumber::NaN;
m_modified = true;
return m_tvUtc;
}
} // namespace Js