| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "time_zone_info_service.h" |
| |
| #include <fuchsia/intl/cpp/fidl.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <cmath> |
| #include <limits> |
| |
| #include <src/lib/icu_data/cpp/icu_data.h> |
| |
| #include "src/lib/fostr/fidl/fuchsia/intl/formatting.h" |
| #include "src/lib/intl/time_zone_info/icu_headers.h" |
| |
| namespace intl { |
| |
| namespace { |
| |
| using fuchsia::intl::TimeZones_AbsoluteToCivilTime_Response; |
| using fuchsia::intl::TimeZones_AbsoluteToCivilTime_Result; |
| using fuchsia::intl::TimeZones_CivilToAbsoluteTime_Response; |
| using fuchsia::intl::TimeZones_CivilToAbsoluteTime_Result; |
| using fuchsia::intl::TimeZones_GetTimeZoneInfo_Response; |
| using fuchsia::intl::TimeZones_GetTimeZoneInfo_Result; |
| |
| static constexpr uint64_t kMillisecondsPerSecond = 1000; |
| static constexpr uint64_t kNanosecondsPerMillisecond = 1'000'000; |
| static constexpr uint64_t kNanosecondsPerSecond = |
| kNanosecondsPerMillisecond * kMillisecondsPerSecond; |
| |
| // The earliest date, in milliseconds from the Epoch, that can fit in a `zx_time_t`. |
| static constexpr int64_t kMinEpochMilliseconds = |
| std::numeric_limits<int64_t>::min() / static_cast<int64_t>(kNanosecondsPerMillisecond); |
| // The latest date, in milliseconds from the Epoch, that can fit in a `zx_time_t`. |
| static constexpr int64_t kMaxEpochMilliseconds = |
| std::numeric_limits<int64_t>::max() / static_cast<int64_t>(kNanosecondsPerMillisecond); |
| |
| fuchsia::intl::TimeZoneId DefaultTimeZoneId() { |
| return fuchsia::intl::TimeZoneId{.id = fuchsia::intl::DEFAULT_TIME_ZONE_ID}; |
| } |
| |
| // Safely converts from ICU's 1-based day of year to Fuchsia's 0-based day of year. |
| // |
| // `icu_status` should be passed in from the previous ICU operation in order to verify that it was |
| // successful and that `icu_year_day` is expected to be valid. |
| uint16_t ICUYearDayToFuchsiaYearDay(int32_t icu_year_day, const UErrorCode icu_status) { |
| if (U_FAILURE(icu_status)) { |
| return 0; |
| } |
| FX_DCHECK(icu_year_day > 0); |
| return static_cast<uint16_t>(icu_year_day - 1); |
| } |
| |
| // Safely converts ICU `UCalendarMonths`, which is 0-based, to `fuchsia::intl::Month`, which is |
| // 1-based. |
| fuchsia::intl::Month ICUMonthToFuchsiaMonth(const int32_t icu_month, const UErrorCode icu_status) { |
| if (U_FAILURE(icu_status)) { |
| // Return an invalid enum value |
| return static_cast<fuchsia::intl::Month>(0); |
| } |
| return static_cast<fuchsia::intl::Month>(static_cast<uint8_t>(icu_month) + 1); |
| } |
| |
| // Safely converts `fuchsia::intl::Month` to ICU's `UCalendarMonths`. |
| UCalendarMonths FuchsiaMonthToICUMonth(const fuchsia::intl::Month fuchsia_month) { |
| uint8_t fuchsia_month_uint = static_cast<uint8_t>(fuchsia_month); |
| FX_DCHECK(fuchsia_month_uint > 0); |
| return static_cast<UCalendarMonths>(fuchsia_month_uint - 1); |
| } |
| |
| // Performs basic checks on required `CivilTime` fields. The rest will be checked by |
| // `icu::Calendar`. |
| bool AreRequiredFieldsValid(const fuchsia::intl::CivilTime& civil_time) { |
| return civil_time.has_year() && civil_time.has_month() && civil_time.has_day(); |
| } |
| |
| // If the client supplied redundant fields (weekday, year_day), verifies that they are consistent |
| // with the date in `calendar`. This helps prevent accidentally shuttling bad data back and forth. |
| bool AreRedundantFieldsCorrect(const fuchsia::intl::CivilTime& civil_time, |
| const icu::Calendar& calendar) { |
| UErrorCode icu_status = UErrorCode::U_ZERO_ERROR; |
| if (civil_time.has_weekday() && |
| civil_time.weekday() != static_cast<fuchsia::intl::DayOfWeek>(calendar.get( |
| UCalendarDateFields::UCAL_DAY_OF_WEEK, icu_status))) { |
| return false; |
| } |
| if (civil_time.has_year_day() && |
| civil_time.year_day() != |
| ICUYearDayToFuchsiaYearDay( |
| calendar.get(UCalendarDateFields::UCAL_DAY_OF_YEAR, icu_status), icu_status)) { |
| return false; |
| } |
| return true; |
| } |
| |
| // Fills in defaults for fields that are allowed to be omitted. |
| void PopulateDefaults(fuchsia::intl::CivilTime& civil_time) { |
| if (!civil_time.has_hour()) { |
| civil_time.set_hour(0); |
| } |
| if (!civil_time.has_minute()) { |
| civil_time.set_minute(0); |
| } |
| if (!civil_time.has_second()) { |
| civil_time.set_second(0); |
| } |
| if (!civil_time.has_nanos()) { |
| civil_time.set_nanos(0); |
| } |
| if (!civil_time.has_time_zone_id()) { |
| civil_time.set_time_zone_id(DefaultTimeZoneId()); |
| } |
| } |
| |
| // Fills in default options. |
| void PopulateDefaults(fuchsia::intl::CivilToAbsoluteTimeOptions& options) { |
| if (!options.has_repeated_time_conversion()) { |
| options.set_repeated_time_conversion(fuchsia::intl::RepeatedTimeConversion::BEFORE_TRANSITION); |
| } |
| if (!options.has_skipped_time_conversion()) { |
| options.set_skipped_time_conversion(fuchsia::intl::SkippedTimeConversion::NEXT_VALID_TIME); |
| } |
| } |
| |
| // Returns `true` if the give ICU date will fit into the range of a `zx_time_t` without under- or |
| // overflowing. |
| bool IsInZxTimeRange(UDate icu_date) { |
| int64_t absolute_time_ms = static_cast<int64_t>(icu_date); |
| return (absolute_time_ms >= kMinEpochMilliseconds && absolute_time_ms <= kMaxEpochMilliseconds); |
| } |
| |
| // Load initial ICU data if this hasn't been done already. |
| // |
| // TODO(kpozin): Eventually, this should solely be the responsibility of the client component that |
| // links `TimeZoneInfoService`, which has a better idea of what parameters ICU should be |
| // initialized with. |
| zx_status_t InitializeIcuIfNeeded() { |
| // It's okay if something else in the same process has already initialized |
| // ICU. |
| static zx_status_t status = icu_data::Initialize(); |
| switch (status) { |
| case ZX_OK: |
| case ZX_ERR_ALREADY_BOUND: |
| return ZX_OK; |
| default: |
| return status; |
| } |
| } |
| |
| TimeZones_AbsoluteToCivilTime_Result AbsoluteToCivilTimeResult( |
| fuchsia::intl::CivilTime civil_time) { |
| return TimeZones_AbsoluteToCivilTime_Result::WithResponse( |
| TimeZones_AbsoluteToCivilTime_Response(std::move(civil_time))); |
| } |
| |
| TimeZones_AbsoluteToCivilTime_Result AbsoluteToCivilTimeResult( |
| const fuchsia::intl::TimeZonesError& error) { |
| return TimeZones_AbsoluteToCivilTime_Result::WithErr(fuchsia::intl::TimeZonesError(error)); |
| } |
| |
| TimeZones_CivilToAbsoluteTime_Result CivilToAbsoluteTimeResult(zx_time_t absolute_time) { |
| return TimeZones_CivilToAbsoluteTime_Result::WithResponse( |
| TimeZones_CivilToAbsoluteTime_Response(absolute_time)); |
| } |
| |
| TimeZones_CivilToAbsoluteTime_Result CivilToAbsoluteTimeResult( |
| const fuchsia::intl::TimeZonesError& error) { |
| return TimeZones_CivilToAbsoluteTime_Result::WithErr(fuchsia::intl::TimeZonesError(error)); |
| } |
| |
| TimeZones_GetTimeZoneInfo_Result GetTimeZoneInfoResult(const fuchsia::intl::TimeZonesError& error) { |
| return TimeZones_GetTimeZoneInfo_Result::WithErr(fuchsia::intl::TimeZonesError(error)); |
| } |
| |
| TimeZones_GetTimeZoneInfo_Result GetTimeZoneInfoResult(fuchsia::intl::TimeZoneInfo time_zone_info) { |
| TimeZones_GetTimeZoneInfo_Response response(std::move(time_zone_info)); |
| return TimeZones_GetTimeZoneInfo_Result::WithResponse(std::move(response)); |
| } |
| |
| // Returns true if the civil time set on the `calendar` is invalid because it should be skipped |
| // during a forward DST transition. |
| bool IsSkippedTime(const icu::Calendar& calendar, UErrorCode& icu_status) { |
| icu_status = UErrorCode::U_ZERO_ERROR; |
| |
| // Create a clone that allows nonexistent times. |
| std::unique_ptr<icu::Calendar> lenient(calendar.clone()); |
| lenient->setLenient(true); |
| |
| // Create another clone, with a different skipped time rule. |
| std::unique_ptr<icu::Calendar> lenient_walltime_first(lenient->clone()); |
| lenient_walltime_first->setSkippedWallTimeOption(UCalendarWallTimeOption::UCAL_WALLTIME_FIRST); |
| |
| return lenient->getTime(icu_status) != lenient_walltime_first->getTime(icu_status); |
| } |
| |
| // Converts an `icu::Calendar` (with some additional values) to a `fuchsia::intl::CivilTime`. |
| // |
| // Note: Fractional seconds should be passed in as `nanoseconds`, not using `Calendar`'s |
| // milliseconds. |
| fuchsia::intl::CivilTime ICUCalendarToCivilTime(const icu::Calendar& calendar, |
| const uint64_t nanoseconds, |
| const fuchsia::intl::TimeZoneId time_zone_id, |
| UErrorCode& icu_status) { |
| FX_DCHECK(nanoseconds < kNanosecondsPerSecond); |
| |
| fuchsia::intl::CivilTime civil_time; |
| civil_time |
| .set_year(static_cast<uint16_t>(calendar.get(UCalendarDateFields::UCAL_YEAR, icu_status))) |
| .set_month(ICUMonthToFuchsiaMonth(calendar.get(UCalendarDateFields::UCAL_MONTH, icu_status), |
| icu_status)) |
| .set_day( |
| static_cast<uint8_t>(calendar.get(UCalendarDateFields::UCAL_DAY_OF_MONTH, icu_status))) |
| .set_hour( |
| static_cast<uint8_t>(calendar.get(UCalendarDateFields::UCAL_HOUR_OF_DAY, icu_status))) |
| .set_minute(static_cast<uint8_t>(calendar.get(UCalendarDateFields::UCAL_MINUTE, icu_status))) |
| .set_second(static_cast<uint8_t>(calendar.get(UCalendarDateFields::UCAL_SECOND, icu_status))) |
| .set_nanos(nanoseconds) |
| .set_weekday(static_cast<fuchsia::intl::DayOfWeek>( |
| calendar.get(UCalendarDateFields::UCAL_DAY_OF_WEEK, icu_status))) |
| .set_year_day(ICUYearDayToFuchsiaYearDay( |
| calendar.get(UCalendarDateFields::UCAL_DAY_OF_YEAR, icu_status), icu_status)) |
| .set_time_zone_id(time_zone_id); |
| |
| return civil_time; |
| } |
| |
| // Returns true if the loaded time zone's ID is "Etc/Unknown" and the client did not explicitly |
| // request it. This means that the requested time zone was not found. |
| bool IsUnexpectedlyUnknownTimeZone(const icu::TimeZone& time_zone, |
| const fuchsia::intl::TimeZoneId& requested_time_zone_id) { |
| if (requested_time_zone_id.id != UCAL_UNKNOWN_ZONE_ID) { |
| icu::UnicodeString loaded_id; |
| time_zone.getID(loaded_id); |
| if (loaded_id == UCAL_UNKNOWN_ZONE_ID) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<TimeZoneInfoService> TimeZoneInfoService::Create() { |
| return std::make_unique<TimeZoneInfoService>(); |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::intl::TimeZones> TimeZoneInfoService::GetHandler( |
| async_dispatcher_t* dispatcher) { |
| return bindings_.GetHandler(this, dispatcher); |
| } |
| |
| void TimeZoneInfoService::Start() { |
| if (InitializeIcuIfNeeded() != ZX_OK) { |
| FX_LOGS(ERROR) << "Failed to initialize ICU data"; |
| return; |
| } |
| } |
| |
| std::variant<std::unique_ptr<icu::Calendar>, fuchsia::intl::TimeZonesError> |
| TimeZoneInfoService::LoadCalendar(const fuchsia::intl::TimeZoneId& time_zone_id) { |
| // `calendar` will take ownership of time_zone. Do not delete it manually. |
| std::unique_ptr<icu::TimeZone> time_zone(icu::TimeZone::createTimeZone(time_zone_id.id.c_str())); |
| if (IsUnexpectedlyUnknownTimeZone(*time_zone, time_zone_id)) { |
| FX_LOGS(WARNING) << "Unknown time zone ID: " << time_zone_id.id; |
| return fuchsia::intl::TimeZonesError::UNKNOWN_TIME_ZONE; |
| } |
| |
| UErrorCode icu_status = UErrorCode::U_ZERO_ERROR; |
| std::unique_ptr<icu::Calendar> calendar( |
| icu::Calendar::createInstance(time_zone.release(), icu_status)); |
| auto icu_error = ConvertAndLogICUError(icu_status); |
| if (icu_error.has_value()) { |
| return icu_error.value(); |
| } |
| |
| return calendar; |
| } |
| |
| std::string ToString(const fuchsia::intl::CivilTime* civil_time) { |
| std::ostringstream buffer; |
| if (civil_time != nullptr) { |
| buffer << "\ncivil_time: " << *civil_time; |
| } |
| return buffer.str(); |
| } |
| |
| std::string ToString(const std::optional<zx_time_t> absolute_time) { |
| std::ostringstream buffer; |
| if (absolute_time.has_value()) { |
| buffer << "\nabsolute_time: " << absolute_time.value(); |
| } |
| return buffer.str(); |
| } |
| |
| std::optional<fuchsia::intl::TimeZonesError> TimeZoneInfoService::ConvertAndLogICUError( |
| const UErrorCode icu_status, const fuchsia::intl::CivilTime* civil_time, |
| const std::optional<zx_time_t> absolute_time) { |
| if (!U_FAILURE(icu_status)) { |
| return std::nullopt; |
| } |
| |
| icu::ErrorCode error; |
| error.set(icu_status); |
| |
| ((icu_status == UErrorCode::U_ILLEGAL_ARGUMENT_ERROR) ? FX_LOG_STREAM(WARNING, nullptr) |
| : FX_LOG_STREAM(ERROR, nullptr)) |
| << "ICU error: " << error.errorName() << ToString(civil_time) << ToString(absolute_time); |
| |
| switch (icu_status) { |
| case UErrorCode::U_ILLEGAL_ARGUMENT_ERROR: |
| return fuchsia::intl::TimeZonesError::INVALID_DATE; |
| default: |
| return fuchsia::intl::TimeZonesError::INTERNAL_ERROR; |
| } |
| } |
| |
| void TimeZoneInfoService::AbsoluteToCivilTime(const fuchsia::intl::TimeZoneId time_zone_id, |
| const zx_time_t absolute_time, |
| const AbsoluteToCivilTimeCallback callback) { |
| auto calendar_result = LoadCalendar(time_zone_id); |
| if (std::holds_alternative<fuchsia::intl::TimeZonesError>(calendar_result)) { |
| callback(AbsoluteToCivilTimeResult(std::get<fuchsia::intl::TimeZonesError>(calendar_result))); |
| return; |
| } |
| auto calendar = std::move(std::get<std::unique_ptr<icu::Calendar>>(calendar_result)); |
| |
| UDate epoch_millis = static_cast<double>(absolute_time / kNanosecondsPerMillisecond); |
| UErrorCode icu_status = UErrorCode::U_ZERO_ERROR; |
| |
| calendar->setTime(epoch_millis, icu_status); |
| fuchsia::intl::CivilTime civil_time(ICUCalendarToCivilTime( |
| *calendar, absolute_time % kNanosecondsPerSecond, std::move(time_zone_id), icu_status)); |
| |
| auto icu_error = ConvertAndLogICUError(icu_status); |
| auto result = icu_error.has_value() ? AbsoluteToCivilTimeResult(icu_error.value()) |
| : AbsoluteToCivilTimeResult(std::move(civil_time)); |
| callback(std::move(result)); |
| } |
| |
| void TimeZoneInfoService::CivilToAbsoluteTime(fuchsia::intl::CivilTime civil_time, |
| fuchsia::intl::CivilToAbsoluteTimeOptions options, |
| const CivilToAbsoluteTimeCallback callback) { |
| if (!AreRequiredFieldsValid(civil_time)) { |
| callback(CivilToAbsoluteTimeResult(fuchsia::intl::TimeZonesError::INVALID_DATE)); |
| return; |
| } |
| PopulateDefaults(civil_time); |
| PopulateDefaults(options); |
| |
| auto calendar_result = LoadCalendar(civil_time.time_zone_id()); |
| if (std::holds_alternative<fuchsia::intl::TimeZonesError>(calendar_result)) { |
| callback(CivilToAbsoluteTimeResult(std::get<fuchsia::intl::TimeZonesError>(calendar_result))); |
| return; |
| } |
| auto calendar = std::move(std::get<std::unique_ptr<icu::Calendar>>(calendar_result)); |
| calendar->clear(); |
| calendar->setLenient(false); |
| |
| switch (options.repeated_time_conversion()) { |
| case fuchsia::intl::RepeatedTimeConversion::BEFORE_TRANSITION: |
| calendar->setRepeatedWallTimeOption(UCalendarWallTimeOption::UCAL_WALLTIME_FIRST); |
| break; |
| default: |
| FX_LOGS(FATAL) << "Unimplemented RepeatedTimeConversion: " |
| << options.repeated_time_conversion(); |
| break; |
| } |
| switch (options.skipped_time_conversion()) { |
| case fuchsia::intl::SkippedTimeConversion::NEXT_VALID_TIME: |
| calendar->setSkippedWallTimeOption(UCalendarWallTimeOption::UCAL_WALLTIME_NEXT_VALID); |
| break; |
| case fuchsia::intl::SkippedTimeConversion::REJECT: |
| // Handled further down |
| break; |
| default: |
| FX_LOGS(FATAL) << "Unimplemented SkippedTimeConversion: " |
| << options.skipped_time_conversion(); |
| break; |
| } |
| |
| calendar->set(static_cast<int32_t>(civil_time.year()), |
| static_cast<int32_t>(FuchsiaMonthToICUMonth(civil_time.month())), |
| static_cast<int32_t>(civil_time.day()), static_cast<int32_t>(civil_time.hour()), |
| static_cast<int32_t>(civil_time.minute()), |
| static_cast<int32_t>(civil_time.second())); |
| |
| bool is_skipped_time = false; |
| UErrorCode icu_status = UErrorCode::U_ZERO_ERROR; |
| UDate time = calendar->getTime(icu_status); |
| |
| if (icu_status == UErrorCode::U_ILLEGAL_ARGUMENT_ERROR) { |
| ConvertAndLogICUError(icu_status); |
| if (options.skipped_time_conversion() != fuchsia::intl::SkippedTimeConversion::REJECT) { |
| // If the given civil time would be skipped due to a DST transition, retry. |
| UErrorCode retry_icu_status = UErrorCode::U_ZERO_ERROR; |
| is_skipped_time = IsSkippedTime(*calendar, retry_icu_status); |
| if (is_skipped_time) { |
| calendar->setLenient(true); |
| time = calendar->getTime(retry_icu_status); |
| icu_status = retry_icu_status; |
| } |
| } else { |
| FX_LOGS(INFO) << "Rejecting invalid date"; |
| } |
| } |
| |
| auto icu_error = ConvertAndLogICUError(icu_status); |
| if (icu_error.has_value()) { |
| callback(CivilToAbsoluteTimeResult(icu_error.value())); |
| return; |
| } |
| |
| if (!AreRedundantFieldsCorrect(civil_time, *calendar)) { |
| callback(CivilToAbsoluteTimeResult(fuchsia::intl::TimeZonesError::INVALID_DATE)); |
| } |
| |
| // Detect underflow and overflow. |
| // The upper bound is reduced by 1 second to leave room for `civil_time.nanos`. |
| if (!IsInZxTimeRange(time + 1.0 * kMillisecondsPerSecond)) { |
| FX_LOGS(WARNING) << "Date is out of zx_time_t range: " << civil_time; |
| callback(CivilToAbsoluteTimeResult(fuchsia::intl::TimeZonesError::INVALID_DATE)); |
| } |
| zx_time_t absolute_time_nanos = static_cast<zx_time_t>(time) * kNanosecondsPerMillisecond; |
| |
| // If the conversion substituted the next valid time (e.g. 3:00:00 AM), the fractional second |
| // must be dropped. |
| if (!is_skipped_time || |
| options.skipped_time_conversion() != fuchsia::intl::SkippedTimeConversion::NEXT_VALID_TIME) { |
| absolute_time_nanos += civil_time.nanos(); |
| } |
| |
| callback(CivilToAbsoluteTimeResult(absolute_time_nanos)); |
| } |
| |
| void TimeZoneInfoService::GetTimeZoneInfo(fuchsia::intl::TimeZoneId time_zone_id, zx_time_t at_time, |
| GetTimeZoneInfoCallback callback) { |
| std::unique_ptr<icu::TimeZone> time_zone(icu::TimeZone::createTimeZone(time_zone_id.id.c_str())); |
| |
| if (IsUnexpectedlyUnknownTimeZone(*time_zone, time_zone_id)) { |
| FX_LOGS(WARNING) << "Unknown time zone ID: " << time_zone_id.id; |
| callback(GetTimeZoneInfoResult(fuchsia::intl::TimeZonesError::UNKNOWN_TIME_ZONE)); |
| return; |
| } |
| |
| UDate at_u_date = static_cast<UDate>(at_time / kNanosecondsPerMillisecond); |
| int32_t raw_offset; |
| int32_t dst_offset; |
| icu::ErrorCode icu_status; |
| time_zone->getOffset(at_u_date, false /* not local time */, raw_offset, dst_offset, icu_status); |
| bool in_dst_at_time = time_zone->inDaylightTime(at_u_date, icu_status); |
| |
| if (icu_status.isFailure()) { |
| fuchsia::intl::TimeZonesError tz_error; |
| switch (icu_status.get()) { |
| case UErrorCode::U_ILLEGAL_ARGUMENT_ERROR: |
| FX_LOGS(WARNING) << "Invalid `at_time`:" << at_time; |
| tz_error = fuchsia::intl::TimeZonesError::INVALID_DATE; |
| break; |
| default: |
| FX_LOGS(ERROR) << "ICU error: " << icu_status.errorName(); |
| tz_error = fuchsia::intl::TimeZonesError::INTERNAL_ERROR; |
| break; |
| } |
| callback(GetTimeZoneInfoResult(tz_error)); |
| return; |
| } |
| |
| fuchsia::intl::TimeZoneInfo tz_info; |
| |
| icu::UnicodeString unicode_id; |
| time_zone->getID(unicode_id); |
| std::string id; |
| unicode_id.toUTF8String(id); |
| tz_info.set_id(fuchsia::intl::TimeZoneId{.id = id}); |
| |
| zx_duration_t total_offset = (raw_offset + dst_offset) * kNanosecondsPerMillisecond; |
| tz_info.set_total_offset_at_time(total_offset); |
| tz_info.set_in_dst_at_time(in_dst_at_time); |
| |
| callback(GetTimeZoneInfoResult(std::move(tz_info))); |
| } |
| |
| } // namespace intl |