Revision control

++ // Copyright (C) 2013 John Layt <jlayt@kde.org>
++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qtimezone.h"
++ #include "qtimezoneprivate_p.h"
++ #include "qtimezonelocale_p.h"
++
++ #include <unicode/ucal.h>
++
++ #include <qdebug.h>
++ #include <qlist.h>
++
++ #include <algorithm>
++
++ QT_BEGIN_NAMESPACE
++
++ /*
++ Private
++
++ ICU implementation
++ */
++
++ // ICU utilities
++
++ #ifdef Q_OS_HARMONY
++ #include <BasicServicesKit/time_service.h>
++ #endif
++
++ // Qt wrapper around ucal_getDefaultTimeZone()
++ static QByteArray ucalDefaultTimeZoneId()
++ {
++ #ifdef Q_OS_HARMONY
++ char tzBuffer[32] = { 0 };
++ TimeService_ErrCode err = OH_TimeService_GetTimeZone(tzBuffer, sizeof(tzBuffer));
++
++ if (err == TIMESERVICE_ERR_OK)
++ return QByteArray(tzBuffer);
++ #endif
++ int32_t size = 30;
++ QString result(size, Qt::Uninitialized);
++ UErrorCode status = U_ZERO_ERROR;
++
++ // size = ucal_getDefaultTimeZone(result, resultLength, status)
++ size = ucal_getDefaultTimeZone(reinterpret_cast<UChar *>(result.data()), size, &status);
++
++ // If overflow, then resize and retry
++ if (status == U_BUFFER_OVERFLOW_ERROR) {
++ result.resize(size);
++ status = U_ZERO_ERROR;
++ size = ucal_getDefaultTimeZone(reinterpret_cast<UChar *>(result.data()), size, &status);
++ }
++
++ // If successful on first or second go, resize and return
++ if (U_SUCCESS(status)) {
++ result.resize(size);
++ return std::move(result).toUtf8();
++ }
++
++ return QByteArray();
++ }
++
++ // Qt wrapper around ucal_get() for offsets
++ static bool ucalOffsetsAtTime(UCalendar *m_ucal, qint64 atMSecsSinceEpoch,
++ int *utcOffset, int *dstOffset)
++ {
++ *utcOffset = 0;
++ *dstOffset = 0;
++
++ // Clone the ucal so we don't change the shared object
++ UErrorCode status = U_ZERO_ERROR;
++ UCalendar *ucal = ucal_clone(m_ucal, &status);
++ if (!U_SUCCESS(status))
++ return false;
++
++ // Set the date to find the offset for
++ status = U_ZERO_ERROR;
++ ucal_setMillis(ucal, atMSecsSinceEpoch, &status);
++
++ int32_t utc = 0;
++ if (U_SUCCESS(status)) {
++ status = U_ZERO_ERROR;
++ // Returns msecs
++ utc = ucal_get(ucal, UCAL_ZONE_OFFSET, &status) / 1000;
++ }
++
++ int32_t dst = 0;
++ if (U_SUCCESS(status)) {
++ status = U_ZERO_ERROR;
++ // Returns msecs
++ dst = ucal_get(ucal, UCAL_DST_OFFSET, &status) / 1000;
++ }
++
++ ucal_close(ucal);
++ if (U_SUCCESS(status)) {
++ *utcOffset = utc;
++ *dstOffset = dst;
++ return true;
++ }
++ return false;
++ }
++
++ #if U_ICU_VERSION_MAJOR_NUM >= 50
++ // Qt wrapper around qt_ucal_getTimeZoneTransitionDate & ucal_get
++ static QTimeZonePrivate::Data ucalTimeZoneTransition(UCalendar *m_ucal,
++ UTimeZoneTransitionType type,
++ qint64 atMSecsSinceEpoch)
++ {
++ QTimeZonePrivate::Data tran;
++
++ // Clone the ucal so we don't change the shared object
++ UErrorCode status = U_ZERO_ERROR;
++ UCalendar *ucal = ucal_clone(m_ucal, &status);
++ if (!U_SUCCESS(status))
++ return tran;
++
++ // Set the date to find the transition for
++ status = U_ZERO_ERROR;
++ const UDate when = UDate(atMSecsSinceEpoch);
++ ucal_setMillis(ucal, when, &status);
++
++ // Find the transition time
++ UDate tranMSecs = 0;
++ status = U_ZERO_ERROR;
++ bool ok = ucal_getTimeZoneTransitionDate(ucal, type, &tranMSecs, &status);
++
++ // Catch a known violation (in ICU 67) of the specified behavior:
++ if (U_SUCCESS(status) && ok && type == UCAL_TZ_TRANSITION_NEXT) {
++ // At the end of time, that can "succeed" with tranMSecs ==
++ // atMSecsSinceEpoch, which should be treated as a failure.
++ // (At the start of time, previous correctly fails.)
++ ok = tranMSecs > when;
++ }
++
++ // Set the transition time to find the offsets for
++ if (U_SUCCESS(status) && ok) {
++ status = U_ZERO_ERROR;
++ ucal_setMillis(ucal, tranMSecs, &status);
++ }
++
++ int32_t utc = 0;
++ if (U_SUCCESS(status) && ok) {
++ status = U_ZERO_ERROR;
++ utc = ucal_get(ucal, UCAL_ZONE_OFFSET, &status) / 1000;
++ }
++
++ int32_t dst = 0;
++ if (U_SUCCESS(status) && ok) {
++ status = U_ZERO_ERROR;
++ dst = ucal_get(ucal, UCAL_DST_OFFSET, &status) / 1000;
++ }
++
++ ucal_close(ucal);
++ if (!U_SUCCESS(status) || !ok)
++ return tran;
++ tran.atMSecsSinceEpoch = qint64(tranMSecs);
++ tran.offsetFromUtc = utc + dst;
++ tran.standardTimeOffset = utc;
++ tran.daylightTimeOffset = dst;
++ // TODO No ICU API, use short name as abbreviation.
++ QTimeZone::TimeType timeType = dst == 0 ? QTimeZone::StandardTime : QTimeZone::DaylightTime;
++ using namespace QtTimeZoneLocale;
++ tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, timeType,
++ QTimeZone::ShortName, QLocale().name().toUtf8());
++ return tran;
++ }
++ #endif // U_ICU_VERSION_SHORT
++
++ // Convert a uenum (listing zone names) to a QList<QByteArray>
++ static QList<QByteArray> uenumToIdList(UEnumeration *uenum)
++ {
++ QList<QByteArray> list;
++ int32_t size = 0;
++ UErrorCode status = U_ZERO_ERROR;
++ // TODO Perhaps use uenum_unext instead?
++ QByteArray result = uenum_next(uenum, &size, &status);
++ while (U_SUCCESS(status) && !result.isEmpty()) {
++ list << result;
++ // Include the CLDR-canonical name, as well
++ const QByteArrayView zone = QTimeZonePrivate::aliasToIana(result);
++ if (!zone.isEmpty()) {
++ list << zone.toByteArray();
++ Q_ASSERT(QTimeZonePrivate::aliasToIana(zone).isEmpty());
++ }
++ status = U_ZERO_ERROR;
++ result = uenum_next(uenum, &size, &status);
++ }
++
++ std::sort(list.begin(), list.end());
++ list.erase(std::unique(list.begin(), list.end()), list.end());
++ return list;
++ }
++
++ // Qt wrapper around ucal_getDSTSavings()
++ static int ucalDaylightOffset(const QByteArray &id)
++ {
++ UErrorCode status = U_ZERO_ERROR;
++ const QString utf16 = QString::fromLatin1(id);
++ const int32_t dstMSecs = ucal_getDSTSavings(
++ reinterpret_cast<const UChar *>(utf16.data()), &status);
++ return U_SUCCESS(status) ? dstMSecs / 1000 : 0;
++ }
++
++ // Create the system default time zone
++ QIcuTimeZonePrivate::QIcuTimeZonePrivate()
++ : m_ucal(nullptr)
++ {
++ // TODO No ICU C API to obtain system tz, assume default hasn't been changed
++ init(ucalDefaultTimeZoneId());
++ }
++
++ // Create a named time zone
++ QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QByteArray &ianaId)
++ : m_ucal(nullptr)
++ {
++ // ICU misleadingly maps invalid IDs to GMT.
++ if (isTimeZoneIdAvailable(ianaId))
++ init(ianaId);
++ }
++
++ QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QIcuTimeZonePrivate &other)
++ : QTimeZonePrivate(other), m_ucal(nullptr)
++ {
++ // Clone the ucal so we don't close the shared object
++ UErrorCode status = U_ZERO_ERROR;
++ m_ucal = ucal_clone(other.m_ucal, &status);
++ if (!U_SUCCESS(status)) {
++ m_id.clear();
++ m_ucal = nullptr;
++ }
++ }
++
++ QIcuTimeZonePrivate::~QIcuTimeZonePrivate()
++ {
++ ucal_close(m_ucal);
++ }
++
++ QIcuTimeZonePrivate *QIcuTimeZonePrivate::clone() const
++ {
++ return new QIcuTimeZonePrivate(*this);
++ }
++
++ void QIcuTimeZonePrivate::init(const QByteArray &ianaId)
++ {
++ m_id = ianaId;
++
++ const QString id = QString::fromUtf8(m_id);
++ UErrorCode status = U_ZERO_ERROR;
++ //TODO Use UCAL_GREGORIAN for now to match QLocale, change to UCAL_DEFAULT once full ICU support
++ m_ucal = ucal_open(reinterpret_cast<const UChar *>(id.data()), id.size(),
++ QLocale().name().toUtf8(), UCAL_GREGORIAN, &status);
++
++ if (!U_SUCCESS(status)) {
++ m_id.clear();
++ m_ucal = nullptr;
++ }
++ }
++
++ QString QIcuTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
++ QTimeZone::NameType nameType,
++ const QLocale &locale) const
++ {
++ // Base class has handled OffsetName if we came via the other overload.
++ if (nameType == QTimeZone::OffsetName) {
++ int offset = standardTimeOffset(QDateTime::currentMSecsSinceEpoch());
++ // We can't use transitions reliably to find out right DST offset.
++ // Instead use DST offset API to try to get it, when needed:
++ if (timeType == QTimeZone::DaylightTime)
++ offset += ucalDaylightOffset(m_id);
++ // This is only valid for times since the most recent standard offset
++ // change; for earlier times, caller must use the other overload.
++
++ // Use our own formating for offset names (ICU C API doesn't support it
++ // and we may as well be self-consistent anyway).
++ return isoOffsetFormat(offset);
++ }
++ // Technically this may be suspect, if locale isn't QLocale(), since that's
++ // what we used when constructing m_ucal; does ICU cope with inconsistency ?
++ using namespace QtTimeZoneLocale;
++ return ucalTimeZoneDisplayName(m_ucal, timeType, nameType, locale.name().toUtf8());
++ }
++
++ int QIcuTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
++ {
++ int stdOffset = 0;
++ int dstOffset = 0;
++ ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
++ return stdOffset + dstOffset;
++ }
++
++ int QIcuTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
++ {
++ int stdOffset = 0;
++ int dstOffset = 0;
++ ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
++ return stdOffset;
++ }
++
++ int QIcuTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
++ {
++ int stdOffset = 0;
++ int dstOffset = 0;
++ ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
++ return dstOffset;
++ }
++
++ bool QIcuTimeZonePrivate::hasDaylightTime() const
++ {
++ if (ucalDaylightOffset(m_id) != 0)
++ return true;
++ #if U_ICU_VERSION_MAJOR_NUM >= 50
++ for (qint64 when = minMSecs(); when != invalidMSecs(); ) {
++ auto data = nextTransition(when);
++ if (data.daylightTimeOffset && data.daylightTimeOffset != invalidSeconds())
++ return true;
++ when = data.atMSecsSinceEpoch;
++ }
++ #endif
++ return false;
++ }
++
++ bool QIcuTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
++ {
++ // Clone the ucal so we don't change the shared object
++ UErrorCode status = U_ZERO_ERROR;
++ UCalendar *ucal = ucal_clone(m_ucal, &status);
++ if (!U_SUCCESS(status))
++ return false;
++
++ // Set the date to find the offset for
++ status = U_ZERO_ERROR;
++ ucal_setMillis(ucal, atMSecsSinceEpoch, &status);
++
++ bool result = false;
++ if (U_SUCCESS(status)) {
++ status = U_ZERO_ERROR;
++ result = ucal_inDaylightTime(ucal, &status);
++ }
++
++ ucal_close(ucal);
++ return result;
++ }
++
++ QTimeZonePrivate::Data QIcuTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
++ {
++ // Available in ICU C++ api, and draft C api in v50
++ Data data;
++ #if U_ICU_VERSION_MAJOR_NUM >= 50
++ data = ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE,
++ forMSecsSinceEpoch);
++ if (data.atMSecsSinceEpoch == invalidMSecs()) // before first transition
++ #endif
++ {
++ ucalOffsetsAtTime(m_ucal, forMSecsSinceEpoch, &data.standardTimeOffset,
++ &data.daylightTimeOffset);
++ data.offsetFromUtc = data.standardTimeOffset + data.daylightTimeOffset;
++ // TODO No ICU API for abbreviation, use short name for it:
++ using namespace QtTimeZoneLocale;
++ QTimeZone::TimeType timeType
++ = data.daylightTimeOffset ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
++ data.abbreviation = ucalTimeZoneDisplayName(m_ucal, timeType, QTimeZone::ShortName,
++ QLocale().name().toUtf8());
++ }
++ data.atMSecsSinceEpoch = forMSecsSinceEpoch;
++ return data;
++ }
++
++ bool QIcuTimeZonePrivate::hasTransitions() const
++ {
++ // Available in ICU C++ api, and draft C api in v50
++ #if U_ICU_VERSION_MAJOR_NUM >= 50
++ return true;
++ #else
++ return false;
++ #endif
++ }
++
++ QTimeZonePrivate::Data QIcuTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
++ {
++ // Available in ICU C++ api, and draft C api in v50
++ #if U_ICU_VERSION_MAJOR_NUM >= 50
++ return ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_NEXT, afterMSecsSinceEpoch);
++ #else
++ Q_UNUSED(afterMSecsSinceEpoch);
++ return {};
++ #endif
++ }
++
++ QTimeZonePrivate::Data QIcuTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
++ {
++ // Available in ICU C++ api, and draft C api in v50
++ #if U_ICU_VERSION_MAJOR_NUM >= 50
++ return ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_PREVIOUS, beforeMSecsSinceEpoch);
++ #else
++ Q_UNUSED(beforeMSecsSinceEpoch);
++ return {};
++ #endif
++ }
++
++ QByteArray QIcuTimeZonePrivate::systemTimeZoneId() const
++ {
++ // No ICU C API to obtain system tz
++ // TODO Assume default hasn't been changed and is the latests system
++ return ucalDefaultTimeZoneId();
++ }
++
++ bool QIcuTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
++ {
++ return QtTimeZoneLocale::ucalKnownTimeZoneId(QString::fromUtf8(ianaId));
++ }
++
++ QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds() const
++ {
++ UErrorCode status = U_ZERO_ERROR;
++ UEnumeration *uenum = ucal_openTimeZones(&status);
++ // Does not document order of entries.
++ QList<QByteArray> result;
++ if (U_SUCCESS(status))
++ result = uenumToIdList(uenum); // Ensures sorted, unique.
++ uenum_close(uenum);
++ return result;
++ }
++
++ QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
++ {
++ const QLatin1StringView regionCode = QLocalePrivate::territoryToCode(territory);
++ const QByteArray regionCodeUtf8 = QString(regionCode).toUtf8();
++ UErrorCode status = U_ZERO_ERROR;
++ UEnumeration *uenum = ucal_openCountryTimeZones(regionCodeUtf8.data(), &status);
++ QList<QByteArray> result;
++ if (U_SUCCESS(status))
++ result = uenumToIdList(uenum);
++ uenum_close(uenum);
++ // We could merge in what matchingTimeZoneIds(territory) gives us, but
++ // hopefully that's redundant, as ICU packages CLDR.
++ return result;
++ }
++
++ QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
++ {
++ // TODO Available directly in C++ api but not C api, from 4.8 onwards new filter method works
++ #if U_ICU_VERSION_MAJOR_NUM >= 49 || (U_ICU_VERSION_MAJOR_NUM == 4 && U_ICU_VERSION_MINOR_NUM == 8)
++ UErrorCode status = U_ZERO_ERROR;
++ UEnumeration *uenum = ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_ANY, nullptr,
++ &offsetFromUtc, &status);
++ QList<QByteArray> result;
++ if (U_SUCCESS(status))
++ result = uenumToIdList(uenum);
++ uenum_close(uenum);
++ // We could merge in what matchingTimeZoneIds(offsetFromUtc) gives us, but
++ // hopefully that's redundant, as ICU packages CLDR.
++ return result;
++ #else
++ return QTimeZonePrivate::availableTimeZoneIds(offsetFromUtc);
++ #endif
++ }
++
++ QT_END_NAMESPACE