double parseES5DateFromNullTerminatedCharacters(const char* dateString) { // This parses a date of the form defined in ECMA-262-5, section 15.9.1.15 // (similar to RFC 3339 / ISO 8601: YYYY-MM-DDTHH:mm:ss[.sss]Z). // In most cases it is intentionally strict (e.g. correct field widths, no stray whitespace). static const long daysPerMonth[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // The year must be present, but the other fields may be omitted - see ES5.1 15.9.1.15. int year = 0; long month = 1; long day = 1; long hours = 0; long minutes = 0; double seconds = 0; long timeZoneSeconds = 0; // Parse the date YYYY[-MM[-DD]] char* currentPosition = parseES5DatePortion(dateString, year, month, day); if (!currentPosition) return std::numeric_limits<double>::quiet_NaN(); // Look for a time portion. if (*currentPosition == 'T') { // Parse the time HH:mm[:ss[.sss]][Z|(+|-)00:00] currentPosition = parseES5TimePortion(currentPosition + 1, hours, minutes, seconds, timeZoneSeconds); if (!currentPosition) return std::numeric_limits<double>::quiet_NaN(); } // Check that we have parsed all characters in the string. if (*currentPosition) return std::numeric_limits<double>::quiet_NaN(); // A few of these checks could be done inline above, but since many of them are interrelated // we would be sacrificing readability to "optimize" the (presumably less common) failure path. if (month < 1 || month > 12) return std::numeric_limits<double>::quiet_NaN(); if (day < 1 || day > daysPerMonth[month - 1]) return std::numeric_limits<double>::quiet_NaN(); if (month == 2 && day > 28 && !isLeapYear(year)) return std::numeric_limits<double>::quiet_NaN(); if (hours < 0 || hours > 24) return std::numeric_limits<double>::quiet_NaN(); if (hours == 24 && (minutes || seconds)) return std::numeric_limits<double>::quiet_NaN(); if (minutes < 0 || minutes > 59) return std::numeric_limits<double>::quiet_NaN(); if (seconds < 0 || seconds >= 61) return std::numeric_limits<double>::quiet_NaN(); if (seconds > 60) { // Discard leap seconds by clamping to the end of a minute. seconds = 60; } double dateSeconds = ymdhmsToSeconds(year, month, day, hours, minutes, seconds) - timeZoneSeconds; return dateSeconds * msPerSecond; }
// Odd case where 'exec' is allowed to be 0, to accomodate a caller in WebCore. static double parseDateFromNullTerminatedCharacters(const char* dateString, bool& haveTZ, int& offset) { haveTZ = false; offset = 0; // This parses a date in the form: // Tuesday, 09-Nov-99 23:12:40 GMT // or // Sat, 01-Jan-2000 08:00:00 GMT // or // Sat, 01 Jan 2000 08:00:00 GMT // or // 01 Jan 99 22:00 +0100 (exceptions in rfc822/rfc2822) // ### non RFC formats, added for Javascript: // [Wednesday] January 09 1999 23:12:40 GMT // [Wednesday] January 09 23:12:40 GMT 1999 // // We ignore the weekday. // Skip leading space skipSpacesAndComments(dateString); long month = -1; const char *wordStart = dateString; // Check contents of first words if not number while (*dateString && !isASCIIDigit(*dateString)) { if (isASCIISpace(*dateString) || *dateString == '(') { if (dateString - wordStart >= 3) month = findMonth(wordStart); skipSpacesAndComments(dateString); wordStart = dateString; } else dateString++; } // Missing delimiter between month and day (like "January29")? if (month == -1 && wordStart != dateString) month = findMonth(wordStart); skipSpacesAndComments(dateString); if (!*dateString) return NaN; // ' 09-Nov-99 23:12:40 GMT' char* newPosStr; long day; if (!parseLong(dateString, &newPosStr, 10, &day)) return NaN; dateString = newPosStr; if (!*dateString) return NaN; if (day < 0) return NaN; long year = 0; if (day > 31) { // ### where is the boundary and what happens below? if (*dateString != '/') return NaN; // looks like a YYYY/MM/DD date if (!*++dateString) return NaN; year = day; if (!parseLong(dateString, &newPosStr, 10, &month)) return NaN; month -= 1; dateString = newPosStr; if (*dateString++ != '/' || !*dateString) return NaN; if (!parseLong(dateString, &newPosStr, 10, &day)) return NaN; dateString = newPosStr; } else if (*dateString == '/' && month == -1) { dateString++; // This looks like a MM/DD/YYYY date, not an RFC date. month = day - 1; // 0-based if (!parseLong(dateString, &newPosStr, 10, &day)) return NaN; if (day < 1 || day > 31) return NaN; dateString = newPosStr; if (*dateString == '/') dateString++; if (!*dateString) return NaN; } else { if (*dateString == '-') dateString++; skipSpacesAndComments(dateString); if (*dateString == ',') dateString++; if (month == -1) { // not found yet month = findMonth(dateString); if (month == -1) return NaN; while (*dateString && *dateString != '-' && *dateString != ',' && !isASCIISpace(*dateString)) dateString++; if (!*dateString) return NaN; // '-99 23:12:40 GMT' if (*dateString != '-' && *dateString != '/' && *dateString != ',' && !isASCIISpace(*dateString)) return NaN; dateString++; } } if (month < 0 || month > 11) return NaN; // '99 23:12:40 GMT' if (year <= 0 && *dateString) { if (!parseLong(dateString, &newPosStr, 10, &year)) return NaN; } // Don't fail if the time is missing. long hour = 0; long minute = 0; long second = 0; if (!*newPosStr) dateString = newPosStr; else { // ' 23:12:40 GMT' if (!(isASCIISpace(*newPosStr) || *newPosStr == ',')) { if (*newPosStr != ':') return NaN; // There was no year; the number was the hour. year = -1; } else { // in the normal case (we parsed the year), advance to the next number dateString = ++newPosStr; skipSpacesAndComments(dateString); } parseLong(dateString, &newPosStr, 10, &hour); // Do not check for errno here since we want to continue // even if errno was set becasue we are still looking // for the timezone! // Read a number? If not, this might be a timezone name. if (newPosStr != dateString) { dateString = newPosStr; if (hour < 0 || hour > 23) return NaN; if (!*dateString) return NaN; // ':12:40 GMT' if (*dateString++ != ':') return NaN; if (!parseLong(dateString, &newPosStr, 10, &minute)) return NaN; dateString = newPosStr; if (minute < 0 || minute > 59) return NaN; // ':40 GMT' if (*dateString && *dateString != ':' && !isASCIISpace(*dateString)) return NaN; // seconds are optional in rfc822 + rfc2822 if (*dateString ==':') { dateString++; if (!parseLong(dateString, &newPosStr, 10, &second)) return NaN; dateString = newPosStr; if (second < 0 || second > 59) return NaN; } skipSpacesAndComments(dateString); if (strncasecmp(dateString, "AM", 2) == 0) { if (hour > 12) return NaN; if (hour == 12) hour = 0; dateString += 2; skipSpacesAndComments(dateString); } else if (strncasecmp(dateString, "PM", 2) == 0) { if (hour > 12) return NaN; if (hour != 12) hour += 12; dateString += 2; skipSpacesAndComments(dateString); } } } // Don't fail if the time zone is missing. // Some websites omit the time zone (4275206). if (*dateString) { if (strncasecmp(dateString, "GMT", 3) == 0 || strncasecmp(dateString, "UTC", 3) == 0) { dateString += 3; haveTZ = true; } if (*dateString == '+' || *dateString == '-') { long o; if (!parseLong(dateString, &newPosStr, 10, &o)) return NaN; dateString = newPosStr; if (o < -9959 || o > 9959) return NaN; int sgn = (o < 0) ? -1 : 1; o = labs(o); if (*dateString != ':') { offset = ((o / 100) * 60 + (o % 100)) * sgn; } else { // GMT+05:00 long o2; if (!parseLong(dateString, &newPosStr, 10, &o2)) return NaN; dateString = newPosStr; offset = (o * 60 + o2) * sgn; } haveTZ = true; } else { for (int i = 0; i < int(sizeof(known_zones) / sizeof(KnownZone)); i++) { if (0 == strncasecmp(dateString, known_zones[i].tzName, strlen(known_zones[i].tzName))) { offset = known_zones[i].tzOffset; dateString += strlen(known_zones[i].tzName); haveTZ = true; break; } } } } skipSpacesAndComments(dateString); if (*dateString && year == -1) { if (!parseLong(dateString, &newPosStr, 10, &year)) return NaN; dateString = newPosStr; } skipSpacesAndComments(dateString); // Trailing garbage if (*dateString) return NaN; // Y2K: Handle 2 digit years. if (year >= 0 && year < 100) { if (year < 50) year += 2000; else year += 1900; } return ymdhmsToSeconds(year, month + 1, day, hour, minute, second) * msPerSecond; }
// Odd case where 'exec' is allowed to be 0, to accomodate a caller in WebCore. double parseDateFromNullTerminatedCharacters(const char* dateString, bool& haveTZ, int& offset) { haveTZ = false; offset = 0; // This parses a date in the form: // Tuesday, 09-Nov-99 23:12:40 GMT // or // Sat, 01-Jan-2000 08:00:00 GMT // or // Sat, 01 Jan 2000 08:00:00 GMT // or // 01 Jan 99 22:00 +0100 (exceptions in rfc822/rfc2822) // ### non RFC formats, added for Javascript: // [Wednesday] January 09 1999 23:12:40 GMT // [Wednesday] January 09 23:12:40 GMT 1999 // // We ignore the weekday. // Skip leading space skipSpacesAndComments(dateString); long month = -1; const char *wordStart = dateString; // Check contents of first words if not number while (*dateString && !isASCIIDigit(*dateString)) { if (isASCIISpace(*dateString) || *dateString == '(') { if (dateString - wordStart >= 3) month = findMonth(wordStart); skipSpacesAndComments(dateString); wordStart = dateString; } else dateString++; } // Missing delimiter between month and day (like "January29")? if (month == -1 && wordStart != dateString) month = findMonth(wordStart); skipSpacesAndComments(dateString); if (!*dateString) return std::numeric_limits<double>::quiet_NaN(); // ' 09-Nov-99 23:12:40 GMT' char* newPosStr; long day; if (!parseLong(dateString, &newPosStr, 10, &day)) return std::numeric_limits<double>::quiet_NaN(); dateString = newPosStr; if (day < 0) return std::numeric_limits<double>::quiet_NaN(); std::optional<int> year; if (day > 31) { // ### where is the boundary and what happens below? if (*dateString != '/') return std::numeric_limits<double>::quiet_NaN(); // looks like a YYYY/MM/DD date if (!*++dateString) return std::numeric_limits<double>::quiet_NaN(); if (day <= std::numeric_limits<int>::min() || day >= std::numeric_limits<int>::max()) return std::numeric_limits<double>::quiet_NaN(); year = static_cast<int>(day); if (!parseLong(dateString, &newPosStr, 10, &month)) return std::numeric_limits<double>::quiet_NaN(); month -= 1; dateString = newPosStr; if (*dateString++ != '/' || !*dateString) return std::numeric_limits<double>::quiet_NaN(); if (!parseLong(dateString, &newPosStr, 10, &day)) return std::numeric_limits<double>::quiet_NaN(); dateString = newPosStr; } else if (*dateString == '/' && month == -1) { dateString++; // This looks like a MM/DD/YYYY date, not an RFC date. month = day - 1; // 0-based if (!parseLong(dateString, &newPosStr, 10, &day)) return std::numeric_limits<double>::quiet_NaN(); if (day < 1 || day > 31) return std::numeric_limits<double>::quiet_NaN(); dateString = newPosStr; if (*dateString == '/') dateString++; if (!*dateString) return std::numeric_limits<double>::quiet_NaN(); } else { if (*dateString == '-') dateString++; skipSpacesAndComments(dateString); if (*dateString == ',') dateString++; if (month == -1) { // not found yet month = findMonth(dateString); if (month == -1) return std::numeric_limits<double>::quiet_NaN(); while (*dateString && *dateString != '-' && *dateString != ',' && !isASCIISpace(*dateString)) dateString++; if (!*dateString) return std::numeric_limits<double>::quiet_NaN(); // '-99 23:12:40 GMT' if (*dateString != '-' && *dateString != '/' && *dateString != ',' && !isASCIISpace(*dateString)) return std::numeric_limits<double>::quiet_NaN(); dateString++; } } if (month < 0 || month > 11) return std::numeric_limits<double>::quiet_NaN(); // '99 23:12:40 GMT' if (*dateString && !year) { int result = 0; if (!parseInt(dateString, &newPosStr, 10, &result)) return std::numeric_limits<double>::quiet_NaN(); year = result; } // Don't fail if the time is missing. long hour = 0; long minute = 0; long second = 0; if (!*newPosStr) dateString = newPosStr; else { // ' 23:12:40 GMT' if (!(isASCIISpace(*newPosStr) || *newPosStr == ',')) { if (*newPosStr != ':') return std::numeric_limits<double>::quiet_NaN(); // There was no year; the number was the hour. year = std::nullopt; } else { // in the normal case (we parsed the year), advance to the next number dateString = ++newPosStr; skipSpacesAndComments(dateString); } parseLong(dateString, &newPosStr, 10, &hour); // Do not check for errno here since we want to continue // even if errno was set becasue we are still looking // for the timezone! // Read a number? If not, this might be a timezone name. if (newPosStr != dateString) { dateString = newPosStr; if (hour < 0 || hour > 23) return std::numeric_limits<double>::quiet_NaN(); if (!*dateString) return std::numeric_limits<double>::quiet_NaN(); // ':12:40 GMT' if (*dateString++ != ':') return std::numeric_limits<double>::quiet_NaN(); if (!parseLong(dateString, &newPosStr, 10, &minute)) return std::numeric_limits<double>::quiet_NaN(); dateString = newPosStr; if (minute < 0 || minute > 59) return std::numeric_limits<double>::quiet_NaN(); // ':40 GMT' if (*dateString && *dateString != ':' && !isASCIISpace(*dateString)) return std::numeric_limits<double>::quiet_NaN(); // seconds are optional in rfc822 + rfc2822 if (*dateString ==':') { dateString++; if (!parseLong(dateString, &newPosStr, 10, &second)) return std::numeric_limits<double>::quiet_NaN(); dateString = newPosStr; if (second < 0 || second > 59) return std::numeric_limits<double>::quiet_NaN(); } skipSpacesAndComments(dateString); if (startsWithLettersIgnoringASCIICase(dateString, "am")) { if (hour > 12) return std::numeric_limits<double>::quiet_NaN(); if (hour == 12) hour = 0; dateString += 2; skipSpacesAndComments(dateString); } else if (startsWithLettersIgnoringASCIICase(dateString, "pm")) { if (hour > 12) return std::numeric_limits<double>::quiet_NaN(); if (hour != 12) hour += 12; dateString += 2; skipSpacesAndComments(dateString); } } } // The year may be after the time but before the time zone. if (isASCIIDigit(*dateString) && !year) { int result = 0; if (!parseInt(dateString, &newPosStr, 10, &result)) return std::numeric_limits<double>::quiet_NaN(); year = result; dateString = newPosStr; skipSpacesAndComments(dateString); } // Don't fail if the time zone is missing. // Some websites omit the time zone (4275206). if (*dateString) { if (startsWithLettersIgnoringASCIICase(dateString, "gmt") || startsWithLettersIgnoringASCIICase(dateString, "utc")) { dateString += 3; haveTZ = true; } if (*dateString == '+' || *dateString == '-') { int o; if (!parseInt(dateString, &newPosStr, 10, &o)) return std::numeric_limits<double>::quiet_NaN(); dateString = newPosStr; if (o < -9959 || o > 9959) return std::numeric_limits<double>::quiet_NaN(); int sgn = (o < 0) ? -1 : 1; o = abs(o); if (*dateString != ':') { if (o >= 24) offset = ((o / 100) * 60 + (o % 100)) * sgn; else offset = o * 60 * sgn; } else { // GMT+05:00 ++dateString; // skip the ':' int o2; if (!parseInt(dateString, &newPosStr, 10, &o2)) return std::numeric_limits<double>::quiet_NaN(); dateString = newPosStr; offset = (o * 60 + o2) * sgn; } haveTZ = true; } else { for (auto& knownZone : knownZones) { // Since the passed-in length is used for both strings, the following checks that // dateString has the time zone name as a prefix, not that it is equal. auto length = strlen(knownZone.tzName); if (equalLettersIgnoringASCIICase(dateString, knownZone.tzName, length)) { offset = knownZone.tzOffset; dateString += length; haveTZ = true; break; } } } } skipSpacesAndComments(dateString); if (*dateString && !year) { int result = 0; if (!parseInt(dateString, &newPosStr, 10, &result)) return std::numeric_limits<double>::quiet_NaN(); year = result; dateString = newPosStr; skipSpacesAndComments(dateString); } // Trailing garbage if (*dateString) return std::numeric_limits<double>::quiet_NaN(); // Y2K: Handle 2 digit years. if (year) { int yearValue = year.value(); if (yearValue >= 0 && yearValue < 100) { if (yearValue < 50) yearValue += 2000; else yearValue += 1900; } year = yearValue; } else { // We select 2000 as default value. This is because of the following reasons. // 1. Year 2000 was used for the initial value of the variable `year`. While it won't be posed to users in WebKit, // V8 used this 2000 as its default value. (As of April 2017, V8 is using the year 2001 and Spider Monkey is // not doing this kind of fallback.) // 2. It is a leap year. When using `new Date("Feb 29")`, we assume that people want to save month and day. // Leap year can save user inputs if they is valid. If we use the current year instead, the current year // may not be a leap year. In that case, `new Date("Feb 29").getMonth()` becomes 2 (March). year = 2000; } ASSERT(year); return ymdhmsToSeconds(year.value(), month + 1, day, hour, minute, second) * msPerSecond; }