/* -*- 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/CalendarFields.h" #include "mozilla/Assertions.h" #include "mozilla/EnumTypeTraits.h" #include "mozilla/Maybe.h" #include "mozilla/Range.h" #include "mozilla/TextUtils.h" #include #include #include "jspubtd.h" #include "NamespaceImports.h" #include "builtin/temporal/Calendar.h" #include "builtin/temporal/Era.h" #include "builtin/temporal/Temporal.h" #include "builtin/temporal/TemporalParser.h" #include "builtin/temporal/TimeZone.h" #include "gc/Barrier.h" #include "gc/Tracer.h" #include "js/Conversions.h" #include "js/ErrorReport.h" #include "js/friend/ErrorMessages.h" #include "js/GCAPI.h" #include "js/Printer.h" #include "js/RootingAPI.h" #include "js/Value.h" #include "util/Text.h" #include "vm/BytecodeUtil.h" #include "vm/JSAtomState.h" #include "vm/JSContext.h" #include "vm/JSObject.h" #include "vm/StringType.h" #include "vm/JSObject-inl.h" #include "vm/ObjectOperations-inl.h" using namespace js; using namespace js::temporal; void CalendarFields::trace(JSTracer* trc) { TraceNullableRoot(trc, &era_, "CalendarFields::era"); timeZone_.trace(trc); } void CalendarFields::setFrom(CalendarField field, const CalendarFields& source) { MOZ_ASSERT(source.has(field)); switch (field) { case CalendarField::Era: setEra(source.era()); return; case CalendarField::EraYear: setEraYear(source.eraYear()); return; case CalendarField::Year: setYear(source.year()); return; case CalendarField::Month: setMonth(source.month()); return; case CalendarField::MonthCode: setMonthCode(source.monthCode()); return; case CalendarField::Day: setDay(source.day()); return; case CalendarField::Hour: setHour(source.hour()); return; case CalendarField::Minute: setMinute(source.minute()); return; case CalendarField::Second: setSecond(source.second()); return; case CalendarField::Millisecond: setMillisecond(source.millisecond()); return; case CalendarField::Microsecond: setMicrosecond(source.microsecond()); return; case CalendarField::Nanosecond: setNanosecond(source.nanosecond()); return; case CalendarField::Offset: setOffset(source.offset()); return; case CalendarField::TimeZone: setTimeZone(source.timeZone()); return; } MOZ_CRASH("invalid temporal field"); } static PropertyName* ToPropertyName(JSContext* cx, CalendarField field) { switch (field) { case CalendarField::Era: return cx->names().era; case CalendarField::EraYear: return cx->names().eraYear; case CalendarField::Year: return cx->names().year; case CalendarField::Month: return cx->names().month; case CalendarField::MonthCode: return cx->names().monthCode; case CalendarField::Day: return cx->names().day; case CalendarField::Hour: return cx->names().hour; case CalendarField::Minute: return cx->names().minute; case CalendarField::Second: return cx->names().second; case CalendarField::Millisecond: return cx->names().millisecond; case CalendarField::Microsecond: return cx->names().microsecond; case CalendarField::Nanosecond: return cx->names().nanosecond; case CalendarField::Offset: return cx->names().offset; case CalendarField::TimeZone: return cx->names().timeZone; } MOZ_CRASH("invalid temporal field name"); } static constexpr const char* ToCString(CalendarField field) { switch (field) { case CalendarField::Era: return "era"; case CalendarField::EraYear: return "eraYear"; case CalendarField::Year: return "year"; case CalendarField::Month: return "month"; case CalendarField::MonthCode: return "monthCode"; case CalendarField::Day: return "day"; case CalendarField::Hour: return "hour"; case CalendarField::Minute: return "minute"; case CalendarField::Second: return "second"; case CalendarField::Millisecond: return "millisecond"; case CalendarField::Microsecond: return "microsecond"; case CalendarField::Nanosecond: return "nanosecond"; case CalendarField::Offset: return "offset"; case CalendarField::TimeZone: return "timeZone"; } MOZ_CRASH("invalid temporal field name"); } static constexpr bool CalendarFieldsAreSorted() { constexpr auto min = mozilla::ContiguousEnumValues::min; constexpr auto max = mozilla::ContiguousEnumValues::max; auto field = min; while (field != max) { auto next = static_cast(mozilla::UnderlyingValue(field) + 1); auto a = std::string_view{ToCString(field)}; auto b = std::string_view{ToCString(next)}; if (a.compare(b) >= 0) { return false; } field = next; } return true; } /** * CalendarExtraFields ( calendar, fields ) */ static mozilla::EnumSet CalendarExtraFields( CalendarId calendar, mozilla::EnumSet fields) { // Step 1. if (calendar == CalendarId::ISO8601) { return {}; } // Step 2. // "era" and "eraYear" are relevant for calendars supporting eras when // "year" is present. if (fields.contains(CalendarField::Year) && CalendarSupportsEra(calendar)) { return {CalendarField::Era, CalendarField::EraYear}; } return {}; } /** * ParseMonthCode ( argument ) */ template static mozilla::Maybe ParseMonthCode( mozilla::Range chars) { // Steps 1-2. (Not applicable) // Steps 3-6. if (chars.length() < 3 || chars.length() > 4) { return mozilla::Nothing(); } // Starts with capital letter 'M'. Leap months end with capital letter 'L'. bool isLeapMonth = chars.length() == 4; if (chars[0] != 'M' || (isLeapMonth && chars[3] != 'L')) { return mozilla::Nothing(); } // Month numbers are ASCII digits. if (!mozilla::IsAsciiDigit(chars[1]) || !mozilla::IsAsciiDigit(chars[2])) { return mozilla::Nothing(); } // Steps 6-7. int32_t ordinal = AsciiDigitToNumber(chars[1]) * 10 + AsciiDigitToNumber(chars[2]); // Step 8. if (ordinal == 0 && !isLeapMonth) { return mozilla::Nothing(); } // Step 9. return mozilla::Some(MonthCodeField{ordinal, isLeapMonth}); } /** * ParseMonthCode ( argument ) */ static auto ParseMonthCode(const JSLinearString* linear) { JS::AutoCheckCannotGC nogc; if (linear->hasLatin1Chars()) { return ParseMonthCode(linear->latin1Range(nogc)); } return ParseMonthCode(linear->twoByteRange(nogc)); } /** * ParseMonthCode ( argument ) */ static bool ParseMonthCode(JSContext* cx, Handle value, MonthCodeField* result) { auto reportInvalidMonthCode = [&](JSLinearString* monthCode) { if (auto code = QuoteString(cx, monthCode)) { JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, code.get()); } return false; }; // Step 1. Rooted monthCode(cx, value); if (!ToPrimitive(cx, JSTYPE_STRING, &monthCode)) { return false; } // Step 2. if (!monthCode.isString()) { ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, monthCode, nullptr, "not a string"); return false; } JSLinearString* monthCodeStr = monthCode.toString()->ensureLinear(cx); if (!monthCodeStr) { return false; } // Steps 3-9. auto parsed = ParseMonthCode(monthCodeStr); if (!parsed) { return reportInvalidMonthCode(monthCodeStr); } *result = *parsed; return true; } /** * ToOffsetString ( argument ) */ static bool ToOffsetString(JSContext* cx, Handle value, int64_t* result) { // Step 1. Rooted offset(cx, value); if (!ToPrimitive(cx, JSTYPE_STRING, &offset)) { return false; } // Step 2. if (!offset.isString()) { ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, offset, nullptr, "not a string"); return false; } Rooted offsetStr(cx, offset.toString()); // Steps 3-4. return ParseDateTimeUTCOffset(cx, offsetStr, result); } enum class Partial : bool { No, Yes }; /** * PrepareCalendarFields ( calendar, fields, calendarFieldNames, * nonCalendarFieldNames, requiredFieldNames ) */ static bool PrepareCalendarFields( JSContext* cx, Handle calendar, Handle fields, mozilla::EnumSet fieldNames, mozilla::EnumSet requiredFields, Partial partial, MutableHandle result) { MOZ_ASSERT_IF(partial == Partial::Yes, requiredFields.isEmpty()); // Steps 1-2. (Not applicable in our implementation.) // Step 3. auto extraFieldNames = CalendarExtraFields(calendar.identifier(), fieldNames); // Step 4. fieldNames += extraFieldNames; // Step 5. (Not applicable in our implementation.) // Step 6. // // Default initialize the result. result.set(CalendarFields{}); // Step 7. (Not applicable in our implementation.) // Step 8. static_assert(CalendarFieldsAreSorted(), "EnumSet iteration is sorted"); // Step 9. Rooted value(cx); for (auto fieldName : fieldNames) { auto* propertyName = ToPropertyName(cx, fieldName); const auto* cstr = ToCString(fieldName); // Step 9.a. (Not applicable in our implementation.) // Step 9.b. if (!GetProperty(cx, fields, fields, propertyName, &value)) { return false; } // Steps 9.c-d. if (!value.isUndefined()) { // Steps 9.c.i-ii. (Not applicable in our implementation.) // Steps 9.c.iii-ix. switch (fieldName) { case CalendarField::Era: { JSString* era = ToString(cx, value); if (!era) { return false; } result.setEra(era); break; } case CalendarField::EraYear: { double eraYear; if (!ToIntegerWithTruncation(cx, value, cstr, &eraYear)) { return false; } result.setEraYear(eraYear); break; } case CalendarField::Year: { double year; if (!ToIntegerWithTruncation(cx, value, cstr, &year)) { return false; } result.setYear(year); break; } case CalendarField::Month: { double month; if (!ToPositiveIntegerWithTruncation(cx, value, cstr, &month)) { return false; } result.setMonth(month); break; } case CalendarField::MonthCode: { MonthCodeField monthCode; if (!ParseMonthCode(cx, value, &monthCode)) { return false; } result.setMonthCode(monthCode); break; } case CalendarField::Day: { double day; if (!ToPositiveIntegerWithTruncation(cx, value, cstr, &day)) { return false; } result.setDay(day); break; } case CalendarField::Hour: { double hour; if (!ToIntegerWithTruncation(cx, value, cstr, &hour)) { return false; } result.setHour(hour); break; } case CalendarField::Minute: { double minute; if (!ToIntegerWithTruncation(cx, value, cstr, &minute)) { return false; } result.setMinute(minute); break; } case CalendarField::Second: { double second; if (!ToIntegerWithTruncation(cx, value, cstr, &second)) { return false; } result.setSecond(second); break; } case CalendarField::Millisecond: { double millisecond; if (!ToIntegerWithTruncation(cx, value, cstr, &millisecond)) { return false; } result.setMillisecond(millisecond); break; } case CalendarField::Microsecond: { double microsecond; if (!ToIntegerWithTruncation(cx, value, cstr, µsecond)) { return false; } result.setMicrosecond(microsecond); break; } case CalendarField::Nanosecond: { double nanosecond; if (!ToIntegerWithTruncation(cx, value, cstr, &nanosecond)) { return false; } result.setNanosecond(nanosecond); break; } case CalendarField::Offset: { int64_t offset; if (!ToOffsetString(cx, value, &offset)) { return false; } result.setOffset(OffsetField{offset}); break; } case CalendarField::TimeZone: Rooted timeZone(cx); if (!ToTemporalTimeZone(cx, value, &timeZone)) { return false; } result.setTimeZone(timeZone); break; } } else if (partial == Partial::No) { // Step 9.d.i. if (requiredFields.contains(fieldName)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_MISSING_PROPERTY, cstr); return false; } // Step 9.d.ii. result.setDefault(fieldName); } } // Step 10. if (partial == Partial::Yes && result.keys().isEmpty()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_MISSING_TEMPORAL_FIELDS); return false; } // Step 11. return true; } /** * PrepareCalendarFields ( calendar, fields, calendarFieldNames, * nonCalendarFieldNames, requiredFieldNames ) */ bool js::temporal::PrepareCalendarFields( JSContext* cx, Handle calendar, Handle fields, mozilla::EnumSet fieldNames, mozilla::EnumSet requiredFields, MutableHandle result) { return PrepareCalendarFields(cx, calendar, fields, fieldNames, requiredFields, Partial::No, result); } /** * PrepareCalendarFields ( calendar, fields, calendarFieldNames, * nonCalendarFieldNames, requiredFieldNames ) */ bool js::temporal::PreparePartialCalendarFields( JSContext* cx, Handle calendar, Handle fields, mozilla::EnumSet fieldNames, JS::MutableHandle result) { return PrepareCalendarFields(cx, calendar, fields, fieldNames, {}, Partial::Yes, result); } /** * NonISOFieldKeysToIgnore ( calendar, keys ) */ static auto NonISOFieldKeysToIgnore(CalendarId calendar, mozilla::EnumSet keys) { static constexpr auto eraOrEraYear = mozilla::EnumSet{ CalendarField::Era, CalendarField::EraYear, }; static constexpr auto eraOrAnyYear = mozilla::EnumSet{ CalendarField::Era, CalendarField::EraYear, CalendarField::Year, }; static constexpr auto monthOrMonthCode = mozilla::EnumSet{ CalendarField::Month, CalendarField::MonthCode, }; static constexpr auto dayOrAnyMonth = mozilla::EnumSet{ CalendarField::Day, CalendarField::Month, CalendarField::MonthCode, }; // A field always invalidates at least itself, so start with ignoring all // input fields. auto result = keys; // "month" and "monthCode" are mutually exclusive. if (!(keys & monthOrMonthCode).isEmpty()) { result += monthOrMonthCode; } // "era", "eraYear", and "year" are mutually exclusive when the calendar // supports eras. if (CalendarSupportsEra(calendar) && !(keys & eraOrAnyYear).isEmpty()) { result += eraOrAnyYear; } // If eras can start in the middle of the year, we have to ignore "era" and // "eraYear" if any of "day", "month", or "monthCode" is present. if (CalendarHasMidYearEras(calendar) && !(keys & dayOrAnyMonth).isEmpty()) { result += eraOrEraYear; } return result; } /** * CalendarFieldKeysToIgnore ( calendar, keys ) */ static auto CalendarFieldKeysToIgnore(CalendarId calendar, mozilla::EnumSet keys) { // Step 1. if (calendar == CalendarId::ISO8601) { // Steps 1.a and 1.b.i. auto ignoredKeys = keys; // Step 1.b.ii. if (keys.contains(CalendarField::Month)) { ignoredKeys += CalendarField::MonthCode; } // Step 1.b.iii. else if (keys.contains(CalendarField::MonthCode)) { ignoredKeys += CalendarField::Month; } // Steps 1.c-d. return ignoredKeys; } // Step 2. return NonISOFieldKeysToIgnore(calendar, keys); } /** * CalendarMergeFields ( calendar, fields, additionalFields ) */ CalendarFields js::temporal::CalendarMergeFields( const CalendarValue& calendar, const CalendarFields& fields, const CalendarFields& additionalFields) { auto calendarId = calendar.identifier(); // Steps 1. auto additionalKeys = additionalFields.keys(); // Step 2. auto overriddenKeys = CalendarFieldKeysToIgnore(calendarId, additionalKeys); MOZ_ASSERT(overriddenKeys.contains(additionalKeys)); // Step 3. auto merged = CalendarFields{}; // Step 4. auto fieldsKeys = fields.keys(); // Step 5.b. for (auto key : (fieldsKeys - overriddenKeys)) { merged.setFrom(key, fields); } // Step 5.c. for (auto key : additionalKeys) { merged.setFrom(key, additionalFields); } // Step 6. return merged; }