blob: bfaf112ef7e8f36a756d3645c1d18e939d9312dd [file] [log] [blame]
// 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