From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- js/src/builtin/temporal/TemporalParser.cpp | 3464 ++++++++++++++++++++++++++++ 1 file changed, 3464 insertions(+) create mode 100644 js/src/builtin/temporal/TemporalParser.cpp (limited to 'js/src/builtin/temporal/TemporalParser.cpp') diff --git a/js/src/builtin/temporal/TemporalParser.cpp b/js/src/builtin/temporal/TemporalParser.cpp new file mode 100644 index 0000000000..c117e63b31 --- /dev/null +++ b/js/src/builtin/temporal/TemporalParser.cpp @@ -0,0 +1,3464 @@ +/* -*- 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"); +} -- cgit v1.2.3