/* -*- 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/. */ #include "builtin/temporal/TemporalParser.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/Maybe.h" #include "mozilla/Range.h" #include "mozilla/Result.h" #include "mozilla/Span.h" #include "mozilla/TextUtils.h" #include #include #include #include #include #include #include #include #include #include "jsnum.h" #include "NamespaceImports.h" #include "builtin/temporal/Duration.h" #include "builtin/temporal/PlainDate.h" #include "builtin/temporal/PlainTime.h" #include "builtin/temporal/TemporalTypes.h" #include "builtin/temporal/TemporalUnit.h" #include "gc/Barrier.h" #include "gc/Tracer.h" #include "js/ErrorReport.h" #include "js/friend/ErrorMessages.h" #include "js/GCAPI.h" #include "js/RootingAPI.h" #include "js/TypeDecls.h" #include "util/Text.h" #include "vm/JSAtomState.h" #include "vm/JSContext.h" #include "vm/StringType.h" using namespace js; using namespace js::temporal; // TODO: Better error message for empty strings? // TODO: Add string input to error message? // TODO: Better error messages, for example display current character? // https://bugzilla.mozilla.org/show_bug.cgi?id=1839676 struct StringName final { // Start position and length of this name. size_t start = 0; size_t length = 0; bool present() const { return length > 0; } }; static JSLinearString* ToString(JSContext* cx, JSString* string, const StringName& name) { MOZ_ASSERT(name.present()); return NewDependentString(cx, string, name.start, name.length); } template bool EqualCharIgnoreCaseAscii(CharT c1, char c2) { if constexpr (sizeof(CharT) > sizeof(char)) { if (!mozilla::IsAscii(c1)) { return false; } } static constexpr auto toLower = 0x20; static_assert('a' - 'A' == toLower); // Convert both characters to lower case before the comparison. char c = c1; if (mozilla::IsAsciiUppercaseAlpha(c1)) { c = c + toLower; } char d = c2; if (mozilla::IsAsciiUppercaseAlpha(c2)) { d = d + toLower; } return c == d; } using CalendarName = StringName; using AnnotationKey = StringName; using AnnotationValue = StringName; using TimeZoneName = StringName; struct Annotation final { AnnotationKey key; AnnotationValue value; bool critical = false; }; struct TimeSpec final { PlainTime time; }; struct TimeZoneUTCOffset final { // ±1 for time zones with an offset, otherwise 0. int32_t sign = 0; // An integer in the range [0, 23]. int32_t hour = 0; // An integer in the range [0, 59]. int32_t minute = 0; }; struct DateTimeUTCOffset final { // ±1 for time zones with an offset, otherwise 0. int32_t sign = 0; // An integer in the range [0, 23]. int32_t hour = 0; // An integer in the range [0, 59]. int32_t minute = 0; // An integer in the range [0, 59]. int32_t second = 0; // An integer in the range [0, 999'999]. int32_t fractionalPart = 0; // Time zone with sub-minute precision. bool subMinutePrecision = false; // Convert to a TimeZoneUTCOffset. TimeZoneUTCOffset toTimeZoneUTCOffset() const { MOZ_ASSERT(!subMinutePrecision, "unexpected sub-minute precision"); return {sign, hour, minute}; } }; /** * ParseDateTimeUTCOffset ( offsetString ) */ static int64_t ParseDateTimeUTCOffset(const DateTimeUTCOffset& offset) { constexpr int64_t nanoPerSec = 1'000'000'000; MOZ_ASSERT(offset.sign == -1 || offset.sign == +1); MOZ_ASSERT(0 <= offset.hour && offset.hour < 24); MOZ_ASSERT(0 <= offset.minute && offset.minute < 60); MOZ_ASSERT(0 <= offset.second && offset.second < 60); MOZ_ASSERT(0 <= offset.fractionalPart && offset.fractionalPart < nanoPerSec); // sign × (((hours × 60 + minutes) × 60 + seconds) × 10^9 + nanoseconds). int64_t seconds = (offset.hour * 60 + offset.minute) * 60 + offset.second; int64_t nanos = (seconds * nanoPerSec) + offset.fractionalPart; int64_t result = offset.sign * nanos; MOZ_ASSERT(std::abs(result) < ToNanoseconds(TemporalUnit::Day), "time zone offset is less than 24:00 hours"); return result; } static int32_t ParseTimeZoneOffset(const TimeZoneUTCOffset& offset) { MOZ_ASSERT(offset.sign == -1 || offset.sign == +1); MOZ_ASSERT(0 <= offset.hour && offset.hour < 24); MOZ_ASSERT(0 <= offset.minute && offset.minute < 60); // sign × (hour × 60 + minute). int32_t result = offset.sign * (offset.hour * 60 + offset.minute); MOZ_ASSERT(std::abs(result) < UnitsPerDay(TemporalUnit::Minute), "time zone offset is less than 24:00 hours"); return result; } /** * Struct to hold time zone annotations. */ struct TimeZoneAnnotation final { // Time zone offset. TimeZoneUTCOffset offset; // Time zone name. TimeZoneName name; /** * Returns true iff the time zone has an offset part, e.g. "+01:00". */ bool hasOffset() const { return offset.sign != 0; } /** * Returns true iff the time zone has an IANA name, e.g. "Asia/Tokyo". */ bool hasName() const { return name.present(); } }; /** * Struct to hold any time zone parts of a parsed string. */ struct TimeZoneString final { // Date-time UTC offset. DateTimeUTCOffset offset; // Time zone annotation; TimeZoneAnnotation annotation; // UTC time zone. bool utc = false; static auto from(DateTimeUTCOffset offset) { TimeZoneString timeZone{}; timeZone.offset = offset; return timeZone; } static auto from(TimeZoneUTCOffset offset) { TimeZoneString timeZone{}; timeZone.annotation.offset = offset; return timeZone; } static auto from(TimeZoneName name) { TimeZoneString timeZone{}; timeZone.annotation.name = name; return timeZone; } static auto UTC() { TimeZoneString timeZone{}; timeZone.utc = true; return timeZone; } /** * Returns true iff the time zone has an offset part, e.g. "+01:00". */ bool hasOffset() const { return offset.sign != 0; } /** * Returns true iff the time zone has an annotation. */ bool hasAnnotation() const { return annotation.hasName() || annotation.hasOffset(); } /** * Returns true iff the time zone uses the "Z" abbrevation to denote UTC time. */ bool isUTC() const { return utc; } }; /** * Struct to hold the parsed date, time, time zone, and calendar components. */ struct ZonedDateTimeString final { PlainDate date; PlainTime time; TimeZoneString timeZone; CalendarName calendar; }; template static bool IsISO8601Calendar(mozilla::Span calendar) { static constexpr std::string_view iso8601 = "iso8601"; if (calendar.size() != iso8601.length()) { return false; } for (size_t i = 0; i < iso8601.length(); i++) { if (!EqualCharIgnoreCaseAscii(calendar[i], iso8601[i])) { return false; } } return true; } static constexpr int32_t AbsentYear = INT32_MAX; /** * ParseISODateTime ( isoString ) */ static bool ParseISODateTime(JSContext* cx, const ZonedDateTimeString& parsed, PlainDateTime* result) { // Steps 1-6, 8, 10-13 (Not applicable here). PlainDateTime dateTime = {parsed.date, parsed.time}; // NOTE: ToIntegerOrInfinity("") is 0. if (dateTime.date.year == AbsentYear) { dateTime.date.year = 0; } // Step 7. if (dateTime.date.month == 0) { dateTime.date.month = 1; } // Step 9. if (dateTime.date.day == 0) { dateTime.date.day = 1; } // Step 14. if (dateTime.time.second == 60) { dateTime.time.second = 59; } // ParseISODateTime, steps 15-16 (Not applicable in our implementation). // Call ThrowIfInvalidISODate to report an error if |days| exceeds the number // of days in the month. All other values are already in-bounds. MOZ_ASSERT(std::abs(dateTime.date.year) <= 999'999); MOZ_ASSERT(1 <= dateTime.date.month && dateTime.date.month <= 12); MOZ_ASSERT(1 <= dateTime.date.day && dateTime.date.day <= 31); // ParseISODateTime, step 17. if (!ThrowIfInvalidISODate(cx, dateTime.date)) { return false; } // ParseISODateTime, step 18. MOZ_ASSERT(IsValidTime(dateTime.time)); // Steps 19-25. (Handled in caller.) *result = dateTime; return true; } static bool ParseTimeZoneAnnotation(JSContext* cx, const TimeZoneAnnotation& annotation, JSLinearString* linear, MutableHandle result) { MOZ_ASSERT(annotation.hasOffset() || annotation.hasName()); if (annotation.hasOffset()) { int32_t offset = ParseTimeZoneOffset(annotation.offset); result.set(ParsedTimeZone::fromOffset(offset)); return true; } auto* str = ToString(cx, linear, annotation.name); if (!str) { return false; } result.set(ParsedTimeZone::fromName(str)); return true; } /** * Struct for the parsed duration components. */ struct TemporalDurationString final { // A non-negative integer or +Infinity. double years = 0; // A non-negative integer or +Infinity. double months = 0; // A non-negative integer or +Infinity. double weeks = 0; // A non-negative integer or +Infinity. double days = 0; // A non-negative integer or +Infinity. double hours = 0; // A non-negative integer or +Infinity. double minutes = 0; // A non-negative integer or +Infinity. double seconds = 0; // An integer in the range [0, 999'999]. int32_t hoursFraction = 0; // An integer in the range [0, 999'999]. int32_t minutesFraction = 0; // An integer in the range [0, 999'999]. int32_t secondsFraction = 0; // ±1 when an offset is present, otherwise 0. int32_t sign = 0; }; class ParserError final { JSErrNum error_ = JSMSG_NOT_AN_ERROR; public: constexpr MOZ_IMPLICIT ParserError(JSErrNum error) : error_(error) {} constexpr JSErrNum error() const { return error_; } constexpr operator JSErrNum() const { return error(); } }; namespace mozilla::detail { // Zero is used for tagging, so it mustn't be an error. static_assert(static_cast(0) == JSMSG_NOT_AN_ERROR); // Ensure efficient packing of the error type. template <> struct UnusedZero<::ParserError> { private: using Error = ::ParserError; using ErrorKind = JSErrNum; public: using StorageType = std::underlying_type_t; static constexpr bool value = true; static constexpr StorageType nullValue = 0; static constexpr Error Inspect(const StorageType& aValue) { return Error(static_cast(aValue)); } static constexpr Error Unwrap(StorageType aValue) { return Error(static_cast(aValue)); } static constexpr StorageType Store(Error aValue) { return static_cast(aValue.error()); } }; } // namespace mozilla::detail static_assert(mozilla::Result::Strategy != mozilla::detail::PackingStrategy::Variant); template class StringReader final { mozilla::Span string_; // Current position in the string. size_t index_ = 0; public: explicit StringReader(mozilla::Span string) : string_(string) {} /** * Returns the input string. */ mozilla::Span string() const { return string_; } /** * Returns a substring of the input string. */ mozilla::Span substring(const StringName& name) const { MOZ_ASSERT(name.present()); return string_.Subspan(name.start, name.length); } /** * Returns the current parse position. */ size_t index() const { return index_; } /** * Returns the length of the input string- */ size_t length() const { return string_.size(); } /** * Returns true iff the whole string has been parsed. */ bool atEnd() const { return index() == length(); } /** * Reset the parser to a previous parse position. */ void reset(size_t index = 0) { MOZ_ASSERT(index <= length()); index_ = index; } /** * Returns true if at least `amount` characters can be read from the current * parse position. */ bool hasMore(size_t amount) const { return index() + amount <= length(); } /** * Advances the parse position by `amount` characters. */ void advance(size_t amount) { MOZ_ASSERT(hasMore(amount)); index_ += amount; } /** * Returns the character at the current parse position. */ CharT current() const { return string()[index()]; } /** * Returns the character at the next parse position. */ CharT next() const { return string()[index() + 1]; } /** * Returns the character at position `index`. */ CharT at(size_t index) const { return string()[index]; } }; template class TemporalParser final { StringReader reader_; /** * Read an unlimited amount of decimal digits, returning `Nothing` if no * digits were read. */ mozilla::Maybe digits(JSContext* cx); /** * Read exactly `length` digits, returning `Nothing` on failure. */ mozilla::Maybe digits(size_t length) { MOZ_ASSERT(length > 0, "can't read zero digits"); MOZ_ASSERT(length <= std::numeric_limits::digits10, "can't read more than digits10 digits without overflow"); if (!reader_.hasMore(length)) { return mozilla::Nothing(); } int32_t num = 0; size_t index = reader_.index(); for (size_t i = 0; i < length; i++) { auto ch = reader_.at(index + i); if (!mozilla::IsAsciiDigit(ch)) { return mozilla::Nothing(); } num = num * 10 + AsciiDigitToNumber(ch); } reader_.advance(length); return mozilla::Some(num); } // TimeFractionalPart : // Digit{1, 9} // // Fraction : // DecimalSeparator TimeFractionalPart mozilla::Maybe fraction() { if (!reader_.hasMore(2)) { return mozilla::Nothing(); } if (!hasDecimalSeparator() || !mozilla::IsAsciiDigit(reader_.next())) { return mozilla::Nothing(); } // Consume the decimal separator. MOZ_ALWAYS_TRUE(decimalSeparator()); // Maximal nine fractional digits are supported. constexpr size_t maxFractions = 9; // Read up to |maxFractions| digits. int32_t num = 0; size_t index = reader_.index(); size_t i = 0; for (; i < std::min(reader_.length() - index, maxFractions); i++) { CharT ch = reader_.at(index + i); if (!mozilla::IsAsciiDigit(ch)) { break; } num = num * 10 + AsciiDigitToNumber(ch); } // Skip past the read digits. reader_.advance(i); // Normalize the fraction to |maxFractions| digits. for (; i < maxFractions; i++) { num *= 10; } return mozilla::Some(num); } /** * Returns true iff the current character is `ch`. */ bool hasCharacter(CharT ch) const { return reader_.hasMore(1) && reader_.current() == ch; } /** * Consumes the current character if it's equal to `ch` and then returns * `true`. Otherwise returns `false`. */ bool character(CharT ch) { if (!hasCharacter(ch)) { return false; } reader_.advance(1); return true; } /** * Consumes the next characters if they're equal to `str` and then returns * `true`. Otherwise returns `false`. */ template bool string(const char (&str)[N]) { static_assert(N > 2, "use character() for one element strings"); if (!reader_.hasMore(N - 1)) { return false; } size_t index = reader_.index(); for (size_t i = 0; i < N - 1; i++) { if (reader_.at(index + i) != str[i]) { return false; } } reader_.advance(N - 1); return true; } /** * Returns true if the next two characters are ASCII alphabetic characters. */ bool hasTwoAsciiAlpha() { if (!reader_.hasMore(2)) { return false; } size_t index = reader_.index(); return mozilla::IsAsciiAlpha(reader_.at(index)) && mozilla::IsAsciiAlpha(reader_.at(index + 1)); } /** * Returns true iff the current character is one of `chars`. */ bool hasOneOf(std::initializer_list chars) const { if (!reader_.hasMore(1)) { return false; } auto ch = reader_.current(); return std::find(chars.begin(), chars.end(), ch) != chars.end(); } /** * Consumes the current character if it's in `chars` and then returns `true`. * Otherwise returns `false`. */ bool oneOf(std::initializer_list chars) { if (!hasOneOf(chars)) { return false; } reader_.advance(1); return true; } /** * Consumes the current character if it matches the predicate and then returns * `true`. Otherwise returns `false`. */ template bool matches(Predicate&& predicate) { if (!reader_.hasMore(1)) { return false; } CharT ch = reader_.current(); if (!predicate(ch)) { return false; } reader_.advance(1); return true; } // Sign : // ASCIISign // U+2212 // // ASCIISign : one of // + - bool hasSign() const { return hasOneOf({'+', '-', 0x2212}); } /** * Consumes the current character, which must be a sign character, and returns * its numeric value. */ int32_t sign() { MOZ_ASSERT(hasSign()); int32_t plus = hasCharacter('+'); reader_.advance(1); return plus ? 1 : -1; } // DecimalSeparator : one of // . , bool hasDecimalSeparator() const { return hasOneOf({'.', ','}); } bool decimalSeparator() { return oneOf({'.', ','}); } // DaysDesignator : one of // D d bool daysDesignator() { return oneOf({'D', 'd'}); } // HoursDesignator : one of // H h bool hoursDesignator() { return oneOf({'H', 'h'}); } // MinutesDesignator : one of // M m bool minutesDesignator() { return oneOf({'M', 'm'}); } // MonthsDesignator : one of // M m bool monthsDesignator() { return oneOf({'M', 'm'}); } // DurationDesignator : one of // P p bool durationDesignator() { return oneOf({'P', 'p'}); } // SecondsDesignator : one of // S s bool secondsDesignator() { return oneOf({'S', 's'}); } // DateTimeSeparator : // // T // t bool dateTimeSeparator() { return oneOf({' ', 'T', 't'}); } // TimeDesignator : one of // T t bool hasTimeDesignator() const { return hasOneOf({'T', 't'}); } bool timeDesignator() { return oneOf({'T', 't'}); } // WeeksDesignator : one of // W w bool weeksDesignator() { return oneOf({'W', 'w'}); } // YearsDesignator : one of // Y y bool yearsDesignator() { return oneOf({'Y', 'y'}); } // UTCDesignator : one of // Z z bool utcDesignator() { return oneOf({'Z', 'z'}); } // TZLeadingChar : // Alpha // . // _ bool tzLeadingChar() { return matches([](auto ch) { return mozilla::IsAsciiAlpha(ch) || ch == '.' || ch == '_'; }); } // TZChar : // TZLeadingChar // DecimalDigit // - // + bool tzChar() { return matches([](auto ch) { return mozilla::IsAsciiAlphanumeric(ch) || ch == '.' || ch == '_' || ch == '-' || ch == '+'; }); } // AnnotationCriticalFlag : // ! bool annotationCriticalFlag() { return character('!'); } // AKeyLeadingChar : // LowercaseAlpha // _ bool aKeyLeadingChar() { return matches([](auto ch) { return mozilla::IsAsciiLowercaseAlpha(ch) || ch == '_'; }); } // AKeyChar : // AKeyLeadingChar // DecimalDigit // - bool aKeyChar() { return matches([](auto ch) { return mozilla::IsAsciiLowercaseAlpha(ch) || mozilla::IsAsciiDigit(ch) || ch == '-' || ch == '_'; }); } // AnnotationValueComponent : // Alpha AnnotationValueComponent? // DecimalDigit AnnotationValueComponent? bool annotationValueComponent() { size_t index = reader_.index(); size_t i = 0; for (; index + i < reader_.length(); i++) { auto ch = reader_.at(index + i); if (!mozilla::IsAsciiAlphanumeric(ch)) { break; } } if (i == 0) { return false; } reader_.advance(i); return true; } template static constexpr bool inBounds(const T& x, const T& min, const T& max) { return min <= x && x <= max; } mozilla::Result dateTime(); mozilla::Result date(); mozilla::Result dateSpecYearMonth(); mozilla::Result dateSpecMonthDay(); mozilla::Result validMonthDay(); mozilla::Result timeSpec(); // Return true when |Annotation| can start at the current position. bool hasAnnotationStart() const { return hasCharacter('['); } // Return true when |TimeZoneAnnotation| can start at the current position. bool hasTimeZoneAnnotationStart() const { if (!hasCharacter('[')) { return false; } // Ensure no '=' is found before the closing ']', otherwise the opening '[' // may actually start an |Annotation| instead of a |TimeZoneAnnotation|. for (size_t i = reader_.index() + 1; i < reader_.length(); i++) { CharT ch = reader_.at(i); if (ch == '=') { return false; } if (ch == ']') { break; } } return true; } // Return true when |DateTimeUTCOffset| can start at the current position. bool hasDateTimeUTCOffsetStart() { return hasOneOf({'Z', 'z', '+', '-', 0x2212}); } mozilla::Result dateTimeUTCOffset(); mozilla::Result utcOffsetSubMinutePrecision(); mozilla::Result timeZoneUTCOffsetName(); mozilla::Result timeZoneIdentifier(); mozilla::Result timeZoneAnnotation(); mozilla::Result timeZoneIANAName(); mozilla::Result annotationKey(); mozilla::Result annotationValue(); mozilla::Result annotation(); mozilla::Result annotations(); mozilla::Result annotatedTime(); mozilla::Result annotatedDateTime(); mozilla::Result annotatedDateTimeTimeRequired(); mozilla::Result annotatedYearMonth(); mozilla::Result annotatedMonthDay(); public: explicit TemporalParser(mozilla::Span str) : reader_(str) {} mozilla::Result parseTemporalInstantString(); mozilla::Result parseTemporalTimeZoneString(); mozilla::Result parseTimeZoneIdentifier(); mozilla::Result parseTimeZoneOffsetString(); mozilla::Result parseDateTimeUTCOffset(); mozilla::Result parseTemporalDurationString(JSContext* cx); mozilla::Result parseTemporalCalendarString(); mozilla::Result parseTemporalTimeString(); mozilla::Result parseTemporalMonthDayString(); mozilla::Result parseTemporalYearMonthString(); mozilla::Result parseTemporalDateTimeString(); mozilla::Result parseTemporalZonedDateTimeString(); }; template mozilla::Result TemporalParser::dateTime() { // DateTime : // Date // Date DateTimeSeparator TimeSpec DateTimeUTCOffset? ZonedDateTimeString result = {}; auto dt = date(); if (dt.isErr()) { return dt.propagateErr(); } result.date = dt.unwrap(); if (dateTimeSeparator()) { auto time = timeSpec(); if (time.isErr()) { return time.propagateErr(); } result.time = time.unwrap(); if (hasDateTimeUTCOffsetStart()) { auto tz = dateTimeUTCOffset(); if (tz.isErr()) { return tz.propagateErr(); } result.timeZone = tz.unwrap(); } } return result; } template mozilla::Result TemporalParser::date() { // Date : // DateYear - DateMonth - DateDay // DateYear DateMonth DateDay PlainDate result = {}; // DateYear : // DecimalDigit{4} // Sign DecimalDigit{6} if (auto year = digits(4)) { result.year = year.value(); } else if (hasSign()) { int32_t yearSign = sign(); if (auto year = digits(6)) { result.year = yearSign * year.value(); if (yearSign < 0 && result.year == 0) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_NEGATIVE_ZERO_YEAR); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_EXTENDED_YEAR); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_YEAR); } // Optional: - character('-'); // DateMonth : // 0 NonzeroDigit // 10 // 11 // 12 if (auto month = digits(2)) { result.month = month.value(); if (!inBounds(result.month, 1, 12)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); } // Optional: - character('-'); // DateDay : // 0 NonzeroDigit // 1 DecimalDigit // 2 DecimalDigit // 30 // 31 if (auto day = digits(2)) { result.day = day.value(); if (!inBounds(result.day, 1, 31)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DAY); } return result; } template mozilla::Result TemporalParser::timeSpec() { // TimeSpec : // TimeHour // TimeHour : TimeMinute // TimeHour TimeMinute // TimeHour : TimeMinute : TimeSecond TimeFraction? // TimeHour TimeMinute TimeSecond TimeFraction? PlainTime result = {}; // TimeHour : // Hour[+Padded] // // Hour[Padded] : // [~Padded] DecimalDigit // [~Padded] 0 DecimalDigit // 1 DecimalDigit // 20 // 21 // 22 // 23 if (auto hour = digits(2)) { result.hour = hour.value(); if (!inBounds(result.hour, 0, 23)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_HOUR); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); } // Optional: : bool needsMinutes = character(':'); // TimeMinute : // MinuteSecond // // MinuteSecond : // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit // 3 DecimalDigit // 4 DecimalDigit // 5 DecimalDigit if (auto minute = digits(2)) { result.minute = minute.value(); if (!inBounds(result.minute, 0, 59)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MINUTE); } // Optional: : bool needsSeconds = needsMinutes && character(':'); // TimeSecond : // MinuteSecond // 60 if (auto second = digits(2)) { result.second = second.value(); if (!inBounds(result.second, 0, 60)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_LEAPSECOND); } // TimeFraction : // Fraction if (auto f = fraction()) { int32_t fractionalPart = f.value(); result.millisecond = fractionalPart / 1'000'000; result.microsecond = (fractionalPart % 1'000'000) / 1'000; result.nanosecond = fractionalPart % 1'000; } } else if (needsSeconds) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_SECOND); } } else if (needsMinutes) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MINUTE); } return result; } template mozilla::Result TemporalParser::dateTimeUTCOffset() { // DateTimeUTCOffset : // UTCDesignator // UTCOffsetSubMinutePrecision if (utcDesignator()) { return TimeZoneString::UTC(); } if (hasSign()) { auto offset = utcOffsetSubMinutePrecision(); if (offset.isErr()) { return offset.propagateErr(); } return TimeZoneString::from(offset.unwrap()); } return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE); } template mozilla::Result TemporalParser::timeZoneUTCOffsetName() { // TimeZoneUTCOffsetName : // UTCOffsetMinutePrecision // // UTCOffsetMinutePrecision : // Sign Hour[+Padded] // Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond // Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond TimeZoneUTCOffset result = {}; if (!hasSign()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE_SIGN); } result.sign = sign(); // Hour[Padded] : // [~Padded] DecimalDigit // [+Padded] 0 DecimalDigit // 1 DecimalDigit // 20 // 21 // 22 // 23 if (auto hour = digits(2)) { result.hour = hour.value(); if (!inBounds(result.hour, 0, 23)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_HOUR); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); } // TimeSeparator[Extended] : // [+Extended] : // [~Extended] [empty] bool needsMinutes = character(':'); // MinuteSecond : // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit // 3 DecimalDigit // 4 DecimalDigit // 5 DecimalDigit if (auto minute = digits(2)) { result.minute = minute.value(); if (!inBounds(result.minute, 0, 59)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MINUTE); } if (hasCharacter(':')) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_SUBMINUTE_TIMEZONE); } } else if (needsMinutes) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MINUTE); } return result; } template mozilla::Result TemporalParser::utcOffsetSubMinutePrecision() { // clang-format off // // UTCOffsetSubMinutePrecision : // UTCOffsetMinutePrecision // UTCOffsetWithSubMinuteComponents[+Extended] // UTCOffsetWithSubMinuteComponents[~Extended] // // UTCOffsetMinutePrecision : // Sign Hour[+Padded] // Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond // Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond // // UTCOffsetWithSubMinuteComponents[Extended] : // Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction? // // clang-format on DateTimeUTCOffset result = {}; if (!hasSign()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE_SIGN); } result.sign = sign(); // Hour[Padded] : // [~Padded] DecimalDigit // [+Padded] 0 DecimalDigit // 1 DecimalDigit // 20 // 21 // 22 // 23 if (auto hour = digits(2)) { result.hour = hour.value(); if (!inBounds(result.hour, 0, 23)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_HOUR); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); } // TimeSeparator[Extended] : // [+Extended] : // [~Extended] [empty] bool needsMinutes = character(':'); // MinuteSecond : // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit // 3 DecimalDigit // 4 DecimalDigit // 5 DecimalDigit if (auto minute = digits(2)) { result.minute = minute.value(); if (!inBounds(result.minute, 0, 59)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MINUTE); } // TimeSeparator[Extended] : // [+Extended] : // [~Extended] [empty] bool needsSeconds = needsMinutes && character(':'); // MinuteSecond : // 0 DecimalDigit // 1 DecimalDigit // 2 DecimalDigit // 3 DecimalDigit // 4 DecimalDigit // 5 DecimalDigit if (auto second = digits(2)) { result.second = second.value(); if (!inBounds(result.second, 0, 59)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_SECOND); } if (auto fractionalPart = fraction()) { result.fractionalPart = fractionalPart.value(); } result.subMinutePrecision = true; } else if (needsSeconds) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_SECOND); } } else if (needsMinutes) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MINUTE); } return result; } template mozilla::Result TemporalParser::timeZoneIdentifier() { // TimeZoneIdentifier : // TimeZoneUTCOffsetName // TimeZoneIANAName TimeZoneAnnotation result = {}; if (hasSign()) { auto offset = timeZoneUTCOffsetName(); if (offset.isErr()) { return offset.propagateErr(); } result.offset = offset.unwrap(); } else { auto name = timeZoneIANAName(); if (name.isErr()) { return name.propagateErr(); } result.name = name.unwrap(); } return result; } template mozilla::Result TemporalParser::timeZoneAnnotation() { // TimeZoneAnnotation : // [ AnnotationCriticalFlag? TimeZoneIdentifier ] if (!character('[')) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_BEFORE_TIMEZONE); } // Skip over the optional critical flag. annotationCriticalFlag(); auto result = timeZoneIdentifier(); if (result.isErr()) { return result.propagateErr(); } if (!character(']')) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_AFTER_TIMEZONE); } return result; } template mozilla::Result TemporalParser::timeZoneIANAName() { // TimeZoneIANAName : // TimeZoneIANANameComponent // TimeZoneIANAName / TimeZoneIANANameComponent // // TimeZoneIANANameComponent : // TZLeadingChar // TimeZoneIANANameComponent TZChar size_t start = reader_.index(); do { if (!tzLeadingChar()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE_NAME); } // Optionally followed by a sequence of |TZChar|. while (tzChar()) { } } while (character('/')); return TimeZoneName{start, reader_.index() - start}; } template mozilla::Maybe TemporalParser::digits(JSContext* cx) { auto span = reader_.string().Subspan(reader_.index()); // GetPrefixInteger can't fail when integer separator handling is disabled. const CharT* endp = nullptr; double num; MOZ_ALWAYS_TRUE(GetPrefixInteger(span.data(), span.data() + span.size(), 10, IntegerSeparatorHandling::None, &endp, &num)); size_t len = endp - span.data(); if (len == 0) { return mozilla::Nothing(); } reader_.advance(len); return mozilla::Some(num); } template mozilla::Result TemporalParser::parseTemporalInstantString() { // Initialize all fields to zero. ZonedDateTimeString result = {}; // clang-format off // // TemporalInstantString : // Date DateTimeSeparator TimeSpec DateTimeUTCOffset TimeZoneAnnotation? Annotations? // // clang-format on auto dt = date(); if (dt.isErr()) { return dt.propagateErr(); } result.date = dt.unwrap(); if (!dateTimeSeparator()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DATE_TIME_SEPARATOR); } auto time = timeSpec(); if (time.isErr()) { return time.propagateErr(); } result.time = time.unwrap(); auto tz = dateTimeUTCOffset(); if (tz.isErr()) { return tz.propagateErr(); } result.timeZone = tz.unwrap(); if (hasTimeZoneAnnotationStart()) { auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); } if (hasAnnotationStart()) { if (auto cal = annotations(); cal.isErr()) { return cal.propagateErr(); } } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return result; } /** * ParseTemporalInstantString ( isoString ) */ template static auto ParseTemporalInstantString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalInstantString(); } /** * ParseTemporalInstantString ( isoString ) */ static auto ParseTemporalInstantString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalInstantString(str->latin1Range(nogc)); } return ParseTemporalInstantString(str->twoByteRange(nogc)); } /** * ParseTemporalInstantString ( isoString ) */ bool js::temporal::ParseTemporalInstantString(JSContext* cx, Handle str, PlainDateTime* result, int64_t* offset) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Step 1. auto parseResult = ::ParseTemporalInstantString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); // Step 2. if (!ParseISODateTime(cx, parsed, result)) { return false; } // Steps 3-4. if (parsed.timeZone.hasOffset()) { *offset = ParseDateTimeUTCOffset(parsed.timeZone.offset); } else { MOZ_ASSERT(parsed.timeZone.isUTC()); *offset = 0; } return true; } template mozilla::Result TemporalParser::parseTemporalTimeZoneString() { // TimeZoneIdentifier : // TimeZoneUTCOffsetName // TimeZoneIANAName if (hasSign()) { if (auto offset = timeZoneUTCOffsetName(); offset.isOk() && reader_.atEnd()) { ZonedDateTimeString result = {}; result.timeZone = TimeZoneString::from(offset.unwrap()); return result; } } else { if (auto name = timeZoneIANAName(); name.isOk() && reader_.atEnd()) { ZonedDateTimeString result = {}; result.timeZone = TimeZoneString::from(name.unwrap()); return result; } } // Try all five parse goals from ParseISODateTime in order. // // TemporalDateTimeString // TemporalInstantString // TemporalTimeString // TemporalMonthDayString // TemporalYearMonthString // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalDateTimeString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalInstantString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalTimeString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalMonthDayString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalYearMonthString(); dt.isOk()) { return dt.unwrap(); } else { return dt.propagateErr(); } } /** * ParseTemporalTimeZoneString ( timeZoneString ) */ template static auto ParseTemporalTimeZoneString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalTimeZoneString(); } /** * ParseTemporalTimeZoneString ( timeZoneString ) */ static auto ParseTemporalTimeZoneString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalTimeZoneString(str->latin1Range(nogc)); } return ParseTemporalTimeZoneString(str->twoByteRange(nogc)); } /** * ParseTemporalTimeZoneString ( timeZoneString ) */ bool js::temporal::ParseTemporalTimeZoneString( JSContext* cx, Handle str, MutableHandle result) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-4. auto parseResult = ::ParseTemporalTimeZoneString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); const auto& timeZone = parsed.timeZone; // Step 3. PlainDateTime unused; if (!ParseISODateTime(cx, parsed, &unused)) { return false; } if (timeZone.hasAnnotation()) { // Case 1: 19700101T00:00Z[+02:00] // Case 2: 19700101T00:00+00:00[+02:00] // Case 3: 19700101T00:00[+02:00] // Case 4: 19700101T00:00Z[Europe/Berlin] // Case 5: 19700101T00:00+00:00[Europe/Berlin] // Case 6: 19700101T00:00[Europe/Berlin] if (!ParseTimeZoneAnnotation(cx, timeZone.annotation, linear, result)) { return false; } } else if (timeZone.isUTC()) { result.set(ParsedTimeZone::fromName(cx->names().UTC)); } else if (timeZone.hasOffset()) { // ToTemporalTimeZoneSlotValue, step 7. // // Error reporting for sub-minute precision moved here. if (timeZone.offset.subMinutePrecision) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PARSER_INVALID_SUBMINUTE_TIMEZONE); return false; } int32_t offset = ParseTimeZoneOffset(timeZone.offset.toTimeZoneUTCOffset()); result.set(ParsedTimeZone::fromOffset(offset)); } else { // Step 5. JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE); return false; } // Step 6. return true; } template mozilla::Result TemporalParser::parseTimeZoneIdentifier() { auto result = timeZoneIdentifier(); if (result.isErr()) { return result.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return result; } /** * ParseTimeZoneIdentifier ( identifier ) */ template static auto ParseTimeZoneIdentifier(mozilla::Span str) { TemporalParser parser(str); return parser.parseTimeZoneIdentifier(); } /** * ParseTimeZoneIdentifier ( identifier ) */ static auto ParseTimeZoneIdentifier(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTimeZoneIdentifier(str->latin1Range(nogc)); } return ParseTimeZoneIdentifier(str->twoByteRange(nogc)); } /** * ParseTimeZoneIdentifier ( identifier ) */ bool js::temporal::ParseTimeZoneIdentifier( JSContext* cx, Handle str, MutableHandle result) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-2. auto parseResult = ::ParseTimeZoneIdentifier(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } auto timeZone = parseResult.unwrap(); // Steps 3-4. return ParseTimeZoneAnnotation(cx, timeZone, linear, result); } template mozilla::Result TemporalParser::parseTimeZoneOffsetString() { auto offset = timeZoneUTCOffsetName(); if (offset.isErr()) { return offset.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return offset.unwrap(); } /** * ParseTimeZoneOffsetString ( isoString ) */ template static auto ParseTimeZoneOffsetString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTimeZoneOffsetString(); } /** * ParseTimeZoneOffsetString ( isoString ) */ static auto ParseTimeZoneOffsetString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTimeZoneOffsetString(str->latin1Range(nogc)); } return ParseTimeZoneOffsetString(str->twoByteRange(nogc)); } /** * ParseTimeZoneOffsetString ( isoString ) */ bool js::temporal::ParseTimeZoneOffsetString(JSContext* cx, Handle str, int32_t* result) { // Step 1. (Not applicable in our implementation.) Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Step 2. auto parseResult = ::ParseTimeZoneOffsetString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } // Steps 3-13. *result = ParseTimeZoneOffset(parseResult.unwrap()); return true; } template mozilla::Result TemporalParser::parseDateTimeUTCOffset() { auto offset = utcOffsetSubMinutePrecision(); if (offset.isErr()) { return offset.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return offset.unwrap(); } /** * ParseDateTimeUTCOffset ( offsetString ) */ template static auto ParseDateTimeUTCOffset(mozilla::Span str) { TemporalParser parser(str); return parser.parseDateTimeUTCOffset(); } /** * ParseDateTimeUTCOffset ( offsetString ) */ static auto ParseDateTimeUTCOffset(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseDateTimeUTCOffset(str->latin1Range(nogc)); } return ParseDateTimeUTCOffset(str->twoByteRange(nogc)); } /** * ParseDateTimeUTCOffset ( offsetString ) */ bool js::temporal::ParseDateTimeUTCOffset(JSContext* cx, Handle str, int64_t* result) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-2. auto parseResult = ::ParseDateTimeUTCOffset(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } // Steps 3-21. *result = ParseDateTimeUTCOffset(parseResult.unwrap()); return true; } template mozilla::Result TemporalParser::parseTemporalDurationString(JSContext* cx) { // Initialize all fields to zero. TemporalDurationString result = {}; // TemporalDurationString : // Duration // // Duration : // Sign? DurationDesignator DurationDate // Sign? DurationDesignator DurationTime if (hasSign()) { result.sign = sign(); } if (!durationDesignator()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DESIGNATOR); } // DurationDate : // DurationYearsPart DurationTime? // DurationMonthsPart DurationTime? // DurationWeeksPart DurationTime? // DurationDaysPart DurationTime? do { double num; if (hasTimeDesignator()) { break; } if (auto d = digits(cx); !d) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); } else { num = *d; } // DurationYearsPart : // DurationYears YearsDesignator DurationMonthsPart // DurationYears YearsDesignator DurationWeeksPart // DurationYears YearsDesignator DurationDaysPart? // // DurationYears : // DecimalDigits[~Sep] if (yearsDesignator()) { result.years = num; if (reader_.atEnd()) { return result; } if (hasTimeDesignator()) { break; } if (auto d = digits(cx); !d) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); } else { num = *d; } } // DurationMonthsPart : // DurationMonths MonthsDesignator DurationWeeksPart // DurationMonths MonthsDesignator DurationDaysPart? // // DurationMonths : // DecimalDigits[~Sep] if (monthsDesignator()) { result.months = num; if (reader_.atEnd()) { return result; } if (hasTimeDesignator()) { break; } if (auto d = digits(cx); !d) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); } else { num = *d; } } // DurationWeeksPart : // DurationWeeks WeeksDesignator DurationDaysPart? // // DurationWeeks : // DecimalDigits[~Sep] if (weeksDesignator()) { result.weeks = num; if (reader_.atEnd()) { return result; } if (hasTimeDesignator()) { break; } if (auto d = digits(cx); !d) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); } else { num = *d; } } // DurationDaysPart : // DurationDays DaysDesignator // // DurationDays : // DecimalDigits[~Sep] if (daysDesignator()) { result.days = num; if (reader_.atEnd()) { return result; } if (hasTimeDesignator()) { break; } } return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } while (false); // DurationTime : // DurationTimeDesignator DurationHoursPart // DurationTimeDesignator DurationMinutesPart // DurationTimeDesignator DurationSecondsPart if (!timeDesignator()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIME_DESIGNATOR); } double num; mozilla::Maybe frac; auto digitsAndFraction = [&]() { auto d = digits(cx); if (!d) { return false; } num = *d; frac = fraction(); return true; }; if (!digitsAndFraction()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); } // clang-format off // // DurationHoursPart : // DurationWholeHours DurationHoursFraction HoursDesignator // DurationWholeHours HoursDesignator DurationMinutesPart // DurationWholeHours HoursDesignator DurationSecondsPart? // // DurationWholeHours : // DecimalDigits[~Sep] // // DurationHoursFraction : // TimeFraction // // TimeFraction : // Fraction // // clang-format on bool hasHoursFraction = false; if (hoursDesignator()) { hasHoursFraction = bool(frac); result.hours = num; result.hoursFraction = frac.valueOr(0); if (reader_.atEnd()) { return result; } if (!digitsAndFraction()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); } } // clang-format off // // DurationMinutesPart : // DurationWholeMinutes DurationMinutesFraction MinutesDesignator // DurationWholeMinutes MinutesDesignator DurationSecondsPart? // // DurationWholeMinutes : // DecimalDigits[~Sep] // // DurationMinutesFraction : // TimeFraction // // TimeFraction : // Fraction // // clang-format on bool hasMinutesFraction = false; if (minutesDesignator()) { if (hasHoursFraction) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DURATION_MINUTES); } hasMinutesFraction = bool(frac); result.minutes = num; result.minutesFraction = frac.valueOr(0); if (reader_.atEnd()) { return result; } if (!digitsAndFraction()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); } } // DurationSecondsPart : // DurationWholeSeconds DurationSecondsFraction? SecondsDesignator // // DurationWholeSeconds : // DecimalDigits[~Sep] // // DurationSecondsFraction : // TimeFraction // // TimeFraction : // Fraction if (secondsDesignator()) { if (hasHoursFraction || hasMinutesFraction) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DURATION_SECONDS); } result.seconds = num; result.secondsFraction = frac.valueOr(0); if (reader_.atEnd()) { return result; } } return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } /** * ParseTemporalDurationString ( isoString ) */ template static auto ParseTemporalDurationString(JSContext* cx, mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalDurationString(cx); } /** * ParseTemporalDurationString ( isoString ) */ static auto ParseTemporalDurationString(JSContext* cx, Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalDurationString(cx, str->latin1Range(nogc)); } return ParseTemporalDurationString(cx, str->twoByteRange(nogc)); } /** * ParseTemporalDurationString ( isoString ) */ bool js::temporal::ParseTemporalDurationString(JSContext* cx, Handle str, Duration* result) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-3. auto parseResult = ::ParseTemporalDurationString(cx, linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } TemporalDurationString parsed = parseResult.unwrap(); // Steps 4-8. double years = parsed.years; double months = parsed.months; double weeks = parsed.weeks; double days = parsed.days; double hours = parsed.hours; // Steps 9-17. double minutes, seconds, milliseconds, microseconds, nanoseconds; if (parsed.hoursFraction) { MOZ_ASSERT(parsed.hoursFraction > 0); MOZ_ASSERT(parsed.hoursFraction < 1'000'000'000); // Step 9.a. MOZ_ASSERT(parsed.minutes == 0); MOZ_ASSERT(parsed.minutesFraction == 0); MOZ_ASSERT(parsed.seconds == 0); MOZ_ASSERT(parsed.secondsFraction == 0); // Steps 9.b-d. int64_t h = int64_t(parsed.hoursFraction) * 60; minutes = h / 1'000'000'000; // Steps 13 and 15-17. int64_t min = (h % 1'000'000'000) * 60; seconds = min / 1'000'000'000; milliseconds = (min % 1'000'000'000) / 1'000'000; microseconds = (min % 1'000'000) / 1'000; nanoseconds = (min % 1'000); } // Step 11. else if (parsed.minutesFraction) { MOZ_ASSERT(parsed.minutesFraction > 0); MOZ_ASSERT(parsed.minutesFraction < 1'000'000'000); // Step 11.a. MOZ_ASSERT(parsed.seconds == 0); MOZ_ASSERT(parsed.secondsFraction == 0); // Step 10. minutes = parsed.minutes; // Steps 11.b-d and 15-17. int64_t min = int64_t(parsed.minutesFraction) * 60; seconds = min / 1'000'000'000; milliseconds = (min % 1'000'000'000) / 1'000'000; microseconds = (min % 1'000'000) / 1'000; nanoseconds = (min % 1'000); } // Step 14. else if (parsed.secondsFraction) { MOZ_ASSERT(parsed.secondsFraction > 0); MOZ_ASSERT(parsed.secondsFraction < 1'000'000'000); // Step 10. minutes = parsed.minutes; // Step 12. seconds = parsed.seconds; // Steps 14, 16-17 milliseconds = (parsed.secondsFraction / 1'000'000); microseconds = ((parsed.secondsFraction % 1'000'000) / 1'000); nanoseconds = (parsed.secondsFraction % 1'000); } else { // Step 10. minutes = parsed.minutes; // Step 12. seconds = parsed.seconds; // Steps 15-17 milliseconds = 0; microseconds = 0; nanoseconds = 0; } // Steps 18-19. int32_t factor = parsed.sign ? parsed.sign : 1; MOZ_ASSERT(factor == -1 || factor == 1); // Step 20. *result = { (years * factor) + (+0.0), (months * factor) + (+0.0), (weeks * factor) + (+0.0), (days * factor) + (+0.0), (hours * factor) + (+0.0), (minutes * factor) + (+0.0), (seconds * factor) + (+0.0), (milliseconds * factor) + (+0.0), (microseconds * factor) + (+0.0), (nanoseconds * factor) + (+0.0), }; if (!ThrowIfInvalidDuration(cx, *result)) { return false; } return true; } template mozilla::Result TemporalParser::annotationKey() { // AnnotationKey : // AKeyLeadingChar // AnnotationKey AKeyChar size_t start = reader_.index(); if (!aKeyLeadingChar()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_ANNOTATION_KEY); } // Optionally followed by a sequence of |AKeyChar|. while (aKeyChar()) { } return AnnotationKey{start, reader_.index() - start}; } template mozilla::Result TemporalParser::annotationValue() { // AnnotationValue : // AnnotationValueComponent // AnnotationValueComponent - AnnotationValue size_t start = reader_.index(); do { if (!annotationValueComponent()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_ANNOTATION_VALUE); } } while (character('-')); return AnnotationValue{start, reader_.index() - start}; } template mozilla::Result TemporalParser::annotation() { // Annotation : // [ AnnotationCriticalFlag? AnnotationKey = AnnotationValue ] if (!character('[')) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_BEFORE_ANNOTATION); } bool critical = annotationCriticalFlag(); auto key = annotationKey(); if (key.isErr()) { return key.propagateErr(); } if (!character('=')) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_ASSIGNMENT_IN_ANNOTATION); } auto value = annotationValue(); if (value.isErr()) { return value.propagateErr(); } if (!character(']')) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_AFTER_ANNOTATION); } return Annotation{key.unwrap(), value.unwrap(), critical}; } template mozilla::Result TemporalParser::annotations() { // Annotations: // Annotation Annotations? MOZ_ASSERT(hasAnnotationStart()); CalendarName calendar; bool calendarWasCritical = false; while (hasAnnotationStart()) { auto anno = annotation(); if (anno.isErr()) { return anno.propagateErr(); } auto [key, value, critical] = anno.unwrap(); // FIXME: spec issue - ignore case for "[u-ca=" to match BCP47? // https://github.com/tc39/proposal-temporal/issues/2524 static constexpr std::string_view ca = "u-ca"; auto keySpan = reader_.substring(key); if (keySpan.size() == ca.length() && std::equal(ca.begin(), ca.end(), keySpan.data())) { if (!calendar.present()) { calendar = value; calendarWasCritical = critical; } else if (critical || calendarWasCritical) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_CRITICAL_ANNOTATION); } } else if (critical) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_CRITICAL_ANNOTATION); } } return calendar; } template mozilla::Result TemporalParser::annotatedTime() { // clang-format off // // AnnotatedTime : // TimeDesignator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? // TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation? Annotations? // // clang-format on if (timeDesignator()) { ZonedDateTimeString result = {}; auto time = timeSpec(); if (time.isErr()) { return time.propagateErr(); } result.time = time.unwrap(); if (hasDateTimeUTCOffsetStart()) { auto tz = dateTimeUTCOffset(); if (tz.isErr()) { return tz.propagateErr(); } result.timeZone = tz.unwrap(); } if (hasTimeZoneAnnotationStart()) { auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); } if (hasAnnotationStart()) { auto cal = annotations(); if (cal.isErr()) { return cal.propagateErr(); } result.calendar = cal.unwrap(); } return result; } // clang-format off // // TimeSpecWithOptionalOffsetNotAmbiguous : // TimeSpec DateTimeUTCOffset? but not one of ValidMonthDay or DateSpecYearMonth // // clang-format on size_t start = reader_.index(); ZonedDateTimeString result = {}; auto time = timeSpec(); if (time.isErr()) { return time.propagateErr(); } result.time = time.unwrap(); if (hasDateTimeUTCOffsetStart()) { auto tz = dateTimeUTCOffset(); if (tz.isErr()) { return tz.propagateErr(); } result.timeZone = tz.unwrap(); } size_t end = reader_.index(); // Reset and check if the input can also be parsed as ValidMonthDay. reader_.reset(start); if (validMonthDay().isOk()) { if (reader_.index() == end) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_AMBIGUOUS_TIME_MONTH_DAY); } } // Reset and check if the input can also be parsed as DateSpecYearMonth. reader_.reset(start); if (dateSpecYearMonth().isOk()) { if (reader_.index() == end) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_AMBIGUOUS_TIME_YEAR_MONTH); } } // Input can neither be parsed as ValidMonthDay nor DateSpecYearMonth. reader_.reset(end); if (hasTimeZoneAnnotationStart()) { auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); } if (hasAnnotationStart()) { auto cal = annotations(); if (cal.isErr()) { return cal.propagateErr(); } result.calendar = cal.unwrap(); } return result; } template mozilla::Result TemporalParser::annotatedDateTime() { // AnnotatedDateTime[Zoned] : // [~Zoned] DateTime TimeZoneAnnotation? Annotations? // [+Zoned] DateTime TimeZoneAnnotation Annotations? auto dt = dateTime(); if (dt.isErr()) { return dt.propagateErr(); } auto result = dt.unwrap(); if (hasTimeZoneAnnotationStart()) { auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); } if (hasAnnotationStart()) { auto cal = annotations(); if (cal.isErr()) { return cal.propagateErr(); } result.calendar = cal.unwrap(); } return result; } template mozilla::Result TemporalParser::annotatedDateTimeTimeRequired() { // clang-format off // // AnnotatedDateTimeTimeRequired : // Date DateTimeSeparator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? // // clang-format on ZonedDateTimeString result = {}; auto dt = date(); if (dt.isErr()) { return dt.propagateErr(); } result.date = dt.unwrap(); if (!dateTimeSeparator()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DATE_TIME_SEPARATOR); } auto time = timeSpec(); if (time.isErr()) { return time.propagateErr(); } result.time = time.unwrap(); if (hasDateTimeUTCOffsetStart()) { auto tz = dateTimeUTCOffset(); if (tz.isErr()) { return tz.propagateErr(); } result.timeZone = tz.unwrap(); } if (hasTimeZoneAnnotationStart()) { auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); } if (hasAnnotationStart()) { auto cal = annotations(); if (cal.isErr()) { return cal.propagateErr(); } result.calendar = cal.unwrap(); } return result; } template mozilla::Result TemporalParser::annotatedYearMonth() { // AnnotatedYearMonth : // DateSpecYearMonth TimeZoneAnnotation? Annotations? ZonedDateTimeString result = {}; auto yearMonth = dateSpecYearMonth(); if (yearMonth.isErr()) { return yearMonth.propagateErr(); } result.date = yearMonth.unwrap(); if (hasTimeZoneAnnotationStart()) { auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); } if (hasAnnotationStart()) { auto cal = annotations(); if (cal.isErr()) { return cal.propagateErr(); } result.calendar = cal.unwrap(); } return result; } template mozilla::Result TemporalParser::annotatedMonthDay() { // AnnotatedMonthDay : // DateSpecMonthDay TimeZoneAnnotation? Annotations? ZonedDateTimeString result = {}; auto monthDay = dateSpecMonthDay(); if (monthDay.isErr()) { return monthDay.propagateErr(); } result.date = monthDay.unwrap(); if (hasTimeZoneAnnotationStart()) { auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); } if (hasAnnotationStart()) { auto cal = annotations(); if (cal.isErr()) { return cal.propagateErr(); } result.calendar = cal.unwrap(); } return result; } template mozilla::Result TemporalParser::dateSpecYearMonth() { // DateSpecYearMonth : // DateYear -? DateMonth PlainDate result = {}; // DateYear : // DecimalDigit{4} // Sign DecimalDigit{6} if (auto year = digits(4)) { result.year = year.value(); } else if (hasSign()) { int32_t yearSign = sign(); if (auto year = digits(6)) { result.year = yearSign * year.value(); if (yearSign < 0 && result.year == 0) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_NEGATIVE_ZERO_YEAR); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_EXTENDED_YEAR); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_YEAR); } character('-'); // DateMonth : // 0 NonzeroDigit // 10 // 11 // 12 if (auto month = digits(2)) { result.month = month.value(); if (!inBounds(result.month, 1, 12)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); } // Absent days default to 1, cf. ParseISODateTime. result.day = 1; return result; } template mozilla::Result TemporalParser::dateSpecMonthDay() { // DateSpecMonthDay : // -- DateMonth -? DateDay // DateMonth -? DateDay PlainDate result = {}; string("--"); result.year = AbsentYear; // DateMonth : // 0 NonzeroDigit // 10 // 11 // 12 if (auto month = digits(2)) { result.month = month.value(); if (!inBounds(result.month, 1, 12)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); } character('-'); // DateDay : // 0 NonzeroDigit // 1 DecimalDigit // 2 DecimalDigit // 30 // 31 if (auto day = digits(2)) { result.day = day.value(); if (!inBounds(result.day, 1, 31)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DAY); } return result; } template mozilla::Result TemporalParser::validMonthDay() { // ValidMonthDay : // DateMonth -? 0 NonZeroDigit // DateMonth -? 1 DecimalDigit // DateMonth -? 2 DecimalDigit // DateMonth -? 30 but not one of 0230 or 02-30 // DateMonthWithThirtyOneDays -? 31 // // DateMonthWithThirtyOneDays : one of // 01 03 05 07 08 10 12 PlainDate result = {}; // DateMonth : // 0 NonzeroDigit // 10 // 11 // 12 if (auto month = digits(2)) { result.month = month.value(); if (!inBounds(result.month, 1, 12)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); } character('-'); if (auto day = digits(2)) { result.day = day.value(); if (!inBounds(result.day, 1, 31)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); } } else { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DAY); } if (result.month == 2 && result.day > 29) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); } if (result.day > 30) { MOZ_ASSERT(result.day == 31); static constexpr int32_t monthsWithThirtyOneDays[] = { 1, 3, 5, 7, 8, 10, 12, }; if (std::find(std::begin(monthsWithThirtyOneDays), std::end(monthsWithThirtyOneDays), result.month) == std::end(monthsWithThirtyOneDays)) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); } } return result; } template mozilla::Result TemporalParser::parseTemporalCalendarString() { // Handle the common case of a standalone calendar name first. // // All valid calendar names start with two alphabetic characters and none of // the ParseISODateTime parse goals can start with two alphabetic characters. // TemporalTimeString can start with 'T', so we can't only check the first // character. if (hasTwoAsciiAlpha()) { auto cal = annotationValue(); if (cal.isErr()) { return cal.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } ZonedDateTimeString result = {}; result.calendar = cal.unwrap(); return result; } // Try all five parse goals from ParseISODateTime in order. // // TemporalDateTimeString // TemporalInstantString // TemporalTimeString // TemporalZonedDateTimeString // TemporalMonthDayString // TemporalYearMonthString if (auto dt = parseTemporalDateTimeString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalInstantString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalTimeString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalMonthDayString(); dt.isOk()) { return dt.unwrap(); } // Restart parsing from the start of the string. reader_.reset(); if (auto dt = parseTemporalYearMonthString(); dt.isOk()) { return dt.unwrap(); } else { return dt.propagateErr(); } } /** * ParseTemporalCalendarString ( isoString ) */ template static auto ParseTemporalCalendarString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalCalendarString(); } /** * ParseTemporalCalendarString ( isoString ) */ static auto ParseTemporalCalendarString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalCalendarString(str->latin1Range(nogc)); } return ParseTemporalCalendarString(str->twoByteRange(nogc)); } /** * ParseTemporalCalendarString ( isoString ) */ JSLinearString* js::temporal::ParseTemporalCalendarString( JSContext* cx, Handle str) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return nullptr; } // Steps 1-3. auto parseResult = ::ParseTemporalCalendarString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return nullptr; } ZonedDateTimeString parsed = parseResult.unwrap(); PlainDateTime unused; if (!ParseISODateTime(cx, parsed, &unused)) { return nullptr; } // Step 2.b. if (!parsed.calendar.present()) { return cx->names().iso8601; } // Steps 2.c and 3.c return ToString(cx, linear, parsed.calendar); } template mozilla::Result TemporalParser::parseTemporalTimeString() { // TemporalTimeString : // AnnotatedTime // AnnotatedDateTimeTimeRequired if (auto time = annotatedTime(); time.isOk() && reader_.atEnd()) { return time.unwrap(); } // Reset and try the next option. reader_.reset(); auto dt = annotatedDateTimeTimeRequired(); if (dt.isErr()) { return dt.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return dt.unwrap(); } /** * ParseTemporalTimeString ( isoString ) */ template static auto ParseTemporalTimeString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalTimeString(); } /** * ParseTemporalTimeString ( isoString ) */ static auto ParseTemporalTimeString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalTimeString(str->latin1Range(nogc)); } return ParseTemporalTimeString(str->twoByteRange(nogc)); } /** * ParseTemporalTimeString ( isoString ) */ bool js::temporal::ParseTemporalTimeString(JSContext* cx, Handle str, PlainTime* result) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-2. auto parseResult = ::ParseTemporalTimeString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); // Step 3. if (parsed.timeZone.isUTC()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); return false; } // Step 4. PlainDateTime dateTime; if (!ParseISODateTime(cx, parsed, &dateTime)) { return false; } *result = dateTime.time; // Step 5. return true; } template mozilla::Result TemporalParser::parseTemporalMonthDayString() { // TemporalMonthDayString : // AnnotatedMonthDay // AnnotatedDateTime[~Zoned] if (auto monthDay = annotatedMonthDay(); monthDay.isOk() && reader_.atEnd()) { auto result = monthDay.unwrap(); // ParseISODateTime, step 3. if (result.calendar.present() && !IsISO8601Calendar(reader_.substring(result.calendar))) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_MONTH_DAY_CALENDAR_NOT_ISO8601); } return result; } // Reset and try the next option. reader_.reset(); auto dt = annotatedDateTime(); if (dt.isErr()) { return dt.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return dt.unwrap(); } /** * ParseTemporalMonthDayString ( isoString ) */ template static auto ParseTemporalMonthDayString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalMonthDayString(); } /** * ParseTemporalMonthDayString ( isoString ) */ static auto ParseTemporalMonthDayString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalMonthDayString(str->latin1Range(nogc)); } return ParseTemporalMonthDayString(str->twoByteRange(nogc)); } /** * ParseTemporalMonthDayString ( isoString ) */ bool js::temporal::ParseTemporalMonthDayString( JSContext* cx, Handle str, PlainDate* result, bool* hasYear, MutableHandle calendar) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-2 . auto parseResult = ::ParseTemporalMonthDayString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); // Step 3. if (parsed.timeZone.isUTC()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); return false; } // Step 4. PlainDateTime dateTime; if (!ParseISODateTime(cx, parsed, &dateTime)) { return false; } *result = dateTime.date; // Steps 5-6. *hasYear = parsed.date.year != AbsentYear; if (parsed.calendar.present()) { calendar.set(ToString(cx, linear, parsed.calendar)); if (!calendar) { return false; } } // Step 7. return true; } template mozilla::Result TemporalParser::parseTemporalYearMonthString() { // TemporalYearMonthString : // AnnotatedYearMonth // AnnotatedDateTime[~Zoned] if (auto yearMonth = annotatedYearMonth(); yearMonth.isOk() && reader_.atEnd()) { auto result = yearMonth.unwrap(); // ParseISODateTime, step 3. if (result.calendar.present() && !IsISO8601Calendar(reader_.substring(result.calendar))) { return mozilla::Err( JSMSG_TEMPORAL_PARSER_YEAR_MONTH_CALENDAR_NOT_ISO8601); } return result; } // Reset and try the next option. reader_.reset(); auto dt = annotatedDateTime(); if (dt.isErr()) { return dt.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return dt.unwrap(); } /** * ParseTemporalYearMonthString ( isoString ) */ template static auto ParseTemporalYearMonthString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalYearMonthString(); } /** * ParseTemporalYearMonthString ( isoString ) */ static auto ParseTemporalYearMonthString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalYearMonthString(str->latin1Range(nogc)); } return ParseTemporalYearMonthString(str->twoByteRange(nogc)); } /** * ParseTemporalYearMonthString ( isoString ) */ bool js::temporal::ParseTemporalYearMonthString( JSContext* cx, Handle str, PlainDate* result, MutableHandle calendar) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-2. auto parseResult = ::ParseTemporalYearMonthString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); // Step 3. if (parsed.timeZone.isUTC()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); return false; } // Step 4. PlainDateTime dateTime; if (!ParseISODateTime(cx, parsed, &dateTime)) { return false; } *result = dateTime.date; if (parsed.calendar.present()) { calendar.set(ToString(cx, linear, parsed.calendar)); if (!calendar) { return false; } } // Step 5. return true; } template mozilla::Result TemporalParser::parseTemporalDateTimeString() { // TemporalDateTimeString[Zoned] : // AnnotatedDateTime[?Zoned] auto dateTime = annotatedDateTime(); if (dateTime.isErr()) { return dateTime.propagateErr(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return dateTime.unwrap(); } /** * ParseTemporalDateTimeString ( isoString ) */ template static auto ParseTemporalDateTimeString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalDateTimeString(); } /** * ParseTemporalDateTimeString ( isoString ) */ static auto ParseTemporalDateTimeString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalDateTimeString(str->latin1Range(nogc)); } return ParseTemporalDateTimeString(str->twoByteRange(nogc)); } /** * ParseTemporalDateTimeString ( isoString ) */ bool js::temporal::ParseTemporalDateTimeString( JSContext* cx, Handle str, PlainDateTime* result, MutableHandle calendar) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-2. auto parseResult = ::ParseTemporalDateTimeString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); // Step 3. if (parsed.timeZone.isUTC()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); return false; } // Step 4. if (!ParseISODateTime(cx, parsed, result)) { return false; } if (parsed.calendar.present()) { calendar.set(ToString(cx, linear, parsed.calendar)); if (!calendar) { return false; } } return true; } /** * ParseTemporalDateString ( isoString ) */ bool js::temporal::ParseTemporalDateString(JSContext* cx, Handle str, PlainDate* result, MutableHandle calendar) { // Step 1. PlainDateTime dateTime; if (!ParseTemporalDateTimeString(cx, str, &dateTime, calendar)) { return false; } // Step 2. *result = dateTime.date; return true; } template mozilla::Result TemporalParser::parseTemporalZonedDateTimeString() { // Parse goal: TemporalDateTimeString[+Zoned] // // TemporalDateTimeString[Zoned] : // AnnotatedDateTime[?Zoned] // // AnnotatedDateTime[Zoned] : // [~Zoned] DateTime TimeZoneAnnotation? Annotations? // [+Zoned] DateTime TimeZoneAnnotation Annotations? auto dt = dateTime(); if (dt.isErr()) { return dt.propagateErr(); } auto result = dt.unwrap(); auto annotation = timeZoneAnnotation(); if (annotation.isErr()) { return annotation.propagateErr(); } result.timeZone.annotation = annotation.unwrap(); if (hasAnnotationStart()) { auto cal = annotations(); if (cal.isErr()) { return cal.propagateErr(); } result.calendar = cal.unwrap(); } if (!reader_.atEnd()) { return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); } return result; } /** * ParseTemporalZonedDateTimeString ( isoString ) */ template static auto ParseTemporalZonedDateTimeString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalZonedDateTimeString(); } /** * ParseTemporalZonedDateTimeString ( isoString ) */ static auto ParseTemporalZonedDateTimeString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalZonedDateTimeString(str->latin1Range(nogc)); } return ParseTemporalZonedDateTimeString(str->twoByteRange(nogc)); } /** * ParseTemporalZonedDateTimeString ( isoString ) */ bool js::temporal::ParseTemporalZonedDateTimeString( JSContext* cx, Handle str, PlainDateTime* dateTime, bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, MutableHandle timeZoneName, MutableHandle calendar) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Step 1. auto parseResult = ::ParseTemporalZonedDateTimeString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); // Step 2. (ParseISODateTime, steps 1-18.) if (!ParseISODateTime(cx, parsed, dateTime)) { return false; } // Step 2. (ParseISODateTime, steps 19-21.) { MOZ_ASSERT(parsed.timeZone.hasAnnotation()); // Case 1: 19700101T00:00Z[+02:00] // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "+02:00" } // // Case 2: 19700101T00:00+02:00[+02:00] // { [[Z]]: false, [[OffsetString]]: "+02:00", [[Name]]: "+02:00" } // // Case 3: 19700101[+02:00] // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "+02:00" } // // Case 4: 19700101T00:00Z[Europe/Berlin] // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } // // Case 5: 19700101T00:00+01:00[Europe/Berlin] // { [[Z]]: false, [[OffsetString]]: "+01:00", [[Name]]: "Europe/Berlin" } // // Case 6: 19700101[Europe/Berlin] // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } const auto& annotation = parsed.timeZone.annotation; if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneName)) { return false; } if (parsed.timeZone.isUTC()) { *isUTC = true; *hasOffset = false; *timeZoneOffset = 0; } else if (parsed.timeZone.hasOffset()) { *isUTC = false; *hasOffset = true; *timeZoneOffset = ParseDateTimeUTCOffset(parsed.timeZone.offset); } else { *isUTC = false; *hasOffset = false; *timeZoneOffset = 0; } } // Step 2. (ParseISODateTime, steps 23-24.) if (parsed.calendar.present()) { calendar.set(ToString(cx, linear, parsed.calendar)); if (!calendar) { return false; } } // Step 2. (ParseISODateTime, step 25.) return true; } /** * ParseTemporalRelativeToString ( isoString ) */ template static auto ParseTemporalRelativeToString(mozilla::Span str) { TemporalParser parser(str); return parser.parseTemporalDateTimeString(); } /** * ParseTemporalRelativeToString ( isoString ) */ static auto ParseTemporalRelativeToString(Handle str) { JS::AutoCheckCannotGC nogc; if (str->hasLatin1Chars()) { return ParseTemporalRelativeToString(str->latin1Range(nogc)); } return ParseTemporalRelativeToString(str->twoByteRange(nogc)); } /** * ParseTemporalRelativeToString ( isoString ) */ bool js::temporal::ParseTemporalRelativeToString( JSContext* cx, Handle str, PlainDateTime* dateTime, bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, MutableHandle timeZoneName, MutableHandle calendar) { Rooted linear(cx, str->ensureLinear(cx)); if (!linear) { return false; } // Steps 1-2. auto parseResult = ::ParseTemporalRelativeToString(linear); if (parseResult.isErr()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, parseResult.unwrapErr()); return false; } ZonedDateTimeString parsed = parseResult.unwrap(); // Step 3. if (parsed.timeZone.isUTC() && !parsed.timeZone.hasAnnotation()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR_WITHOUT_NAME); return false; } // Step 4. (ParseISODateTime, steps 1-18.) if (!ParseISODateTime(cx, parsed, dateTime)) { return false; } // Step 4. (ParseISODateTime, steps 19-22.) if (parsed.timeZone.hasAnnotation()) { // Case 1: 19700101Z[+02:00] // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "+02:00" } // // Case 2: 19700101+00:00[+02:00] // { [[Z]]: false, [[OffsetString]]: "+00:00", [[Name]]: "+02:00" } // // Case 3: 19700101[+02:00] // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "+02:00" } // // Case 4: 19700101Z[Europe/Berlin] // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } // // Case 5: 19700101+00:00[Europe/Berlin] // { [[Z]]: false, [[OffsetString]]: "+00:00", [[Name]]: "Europe/Berlin" } // // Case 6: 19700101[Europe/Berlin] // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } const auto& annotation = parsed.timeZone.annotation; if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneName)) { return false; } if (parsed.timeZone.isUTC()) { *isUTC = true; *hasOffset = false; *timeZoneOffset = 0; } else if (parsed.timeZone.hasOffset()) { *isUTC = false; *hasOffset = true; *timeZoneOffset = ParseDateTimeUTCOffset(parsed.timeZone.offset); } else { *isUTC = false; *hasOffset = false; *timeZoneOffset = 0; } } else { // ToRelativeTemporalObject ignores any other time zone information when no // bracketed time zone annotation is present. *isUTC = false; *hasOffset = false; *timeZoneOffset = 0; timeZoneName.set(ParsedTimeZone{}); } // Step 4. (ParseISODateTime, steps 23-24.) if (parsed.calendar.present()) { calendar.set(ToString(cx, linear, parsed.calendar)); if (!calendar) { return false; } } // Step 4. (Return) return true; } void js::temporal::ParsedTimeZone::trace(JSTracer* trc) { TraceNullableRoot(trc, &name, "ParsedTimeZone::name"); }