From 98ab7dfab785f05ddb3289fe7fa286b66f8b01fb Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 19 Aug 2024 13:23:24 +0200 Subject: [PATCH] Permit missing /etc/localtime We received a report that in some container images, /etc/localtime is left unspecified. According to the Linux documentation (https://www.man7.org/linux/man-pages/man5/localtime.5.html), this means that the UTC time zone must be used. We still throw an exception if we see a time zone we don't recognize. --- core/common/src/TimeZone.kt | 5 ++++- core/linux/src/internal/TimeZoneNative.kt | 8 ++++++-- .../src/internal/TzdbOnFilesystem.kt | 19 ++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index 6a6943b34..84d0373e5 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -63,6 +63,9 @@ public expect open class TimeZone { * * If the current system time zone changes, this function can reflect this change on the next invocation. * + * On Linux, this function queries the `/etc/localtime` symbolic link. If the link is missing, [UTC] is used. + * If the link points to an invalid location, [IllegalTimeZoneException] is thrown. + * * @sample kotlinx.datetime.test.samples.TimeZoneSamples.currentSystemDefault */ public fun currentSystemDefault(): TimeZone @@ -70,7 +73,7 @@ public expect open class TimeZone { /** * Returns the time zone with the fixed UTC+0 offset. * - * The [id] of this time zone is `"UTC"`. + * The [id] of this time zone is `"Z"`. * * @sample kotlinx.datetime.test.samples.TimeZoneSamples.utc */ diff --git a/core/linux/src/internal/TimeZoneNative.kt b/core/linux/src/internal/TimeZoneNative.kt index 8efb2944c..47a1accb9 100644 --- a/core/linux/src/internal/TimeZoneNative.kt +++ b/core/linux/src/internal/TimeZoneNative.kt @@ -5,12 +5,16 @@ package kotlinx.datetime.internal +import kotlinx.datetime.IllegalTimeZoneException + internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow() private val tzdb = runCatching { TzdbOnFilesystem() } internal actual fun currentSystemDefaultZone(): Pair { - val zoneId = pathToSystemDefault()?.second?.toString() - ?: throw IllegalStateException("Failed to get the system timezone") + // according to https://www.man7.org/linux/man-pages/man5/localtime.5.html, when there is no symlink, UTC is used + val zonePath = currentSystemTimeZonePath ?: return "Z" to null + val zoneId = zonePath.splitTimeZonePath()?.second?.toString() + ?: throw IllegalTimeZoneException("Could not determine the timezone ID that `$zonePath` corresponds to") return zoneId to null } diff --git a/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt b/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt index ee2efa84a..0032197a1 100644 --- a/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt +++ b/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt @@ -44,16 +44,21 @@ internal fun tzdbPaths(defaultTzdbPath: Path?) = sequence { defaultTzdbPath?.let { yield(it) } // taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L70 yieldAll(listOf("/usr/share/zoneinfo", "/usr/share/lib/zoneinfo", "/etc/zoneinfo").map { Path.fromString(it) }) - pathToSystemDefault()?.first?.let { yield(it) } + currentSystemTimeZonePath?.splitTimeZonePath()?.first?.let { yield(it) } } +internal val currentSystemTimeZonePath get() = chaseSymlinks("/etc/localtime") + +/** + * Given a path like `/usr/share/zoneinfo/Europe/Berlin`, produces `/usr/share/zoneinfo to Europe/Berlin`. + * Returns null if the function can't recognize the boundary between the time zone and the tzdb. + */ // taken from https://github.com/HowardHinnant/date/blob/ab37c362e35267d6dee02cb47760f9e9c669d3be/src/tz.cpp#L3951-L3952 -internal fun pathToSystemDefault(): Pair? { - val info = chaseSymlinks("/etc/localtime") ?: return null - val i = info.components.indexOf("zoneinfo") - if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null +internal fun Path.splitTimeZonePath(): Pair? { + val i = components.indexOf("zoneinfo") + if (!isAbsolute || i == -1 || i == components.size - 1) return null return Pair( - Path(true, info.components.subList(0, i + 1)), - Path(false, info.components.subList(i + 1, info.components.size)) + Path(true, components.subList(0, i + 1)), + Path(false, components.subList(i + 1, components.size)) ) }