Skip to content

Commit a86e7f8

Browse files
authored
Permit missing /etc/localtime (#426)
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.
1 parent 1cb9cdb commit a86e7f8

File tree

3 files changed

+22
-10
lines changed

3 files changed

+22
-10
lines changed

core/common/src/TimeZone.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,17 @@ public expect open class TimeZone {
6363
*
6464
* If the current system time zone changes, this function can reflect this change on the next invocation.
6565
*
66+
* On Linux, this function queries the `/etc/localtime` symbolic link. If the link is missing, [UTC] is used.
67+
* If the link points to an invalid location, [IllegalTimeZoneException] is thrown.
68+
*
6669
* @sample kotlinx.datetime.test.samples.TimeZoneSamples.currentSystemDefault
6770
*/
6871
public fun currentSystemDefault(): TimeZone
6972

7073
/**
7174
* Returns the time zone with the fixed UTC+0 offset.
7275
*
73-
* The [id] of this time zone is `"UTC"`.
76+
* The [id] of this time zone is `"Z"`.
7477
*
7578
* @sample kotlinx.datetime.test.samples.TimeZoneSamples.utc
7679
*/

core/linux/src/internal/TimeZoneNative.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55

66
package kotlinx.datetime.internal
77

8+
import kotlinx.datetime.IllegalTimeZoneException
9+
810
internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
911

1012
private val tzdb = runCatching { TzdbOnFilesystem() }
1113

1214
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
13-
val zoneId = pathToSystemDefault()?.second?.toString()
14-
?: throw IllegalStateException("Failed to get the system timezone")
15+
// according to https://www.man7.org/linux/man-pages/man5/localtime.5.html, when there is no symlink, UTC is used
16+
val zonePath = currentSystemTimeZonePath ?: return "Z" to null
17+
val zoneId = zonePath.splitTimeZonePath()?.second?.toString()
18+
?: throw IllegalTimeZoneException("Could not determine the timezone ID that `$zonePath` corresponds to")
1519
return zoneId to null
1620
}

core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt

+12-7
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,21 @@ internal fun tzdbPaths(defaultTzdbPath: Path?) = sequence {
4444
defaultTzdbPath?.let { yield(it) }
4545
// taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L70
4646
yieldAll(listOf("/usr/share/zoneinfo", "/usr/share/lib/zoneinfo", "/etc/zoneinfo").map { Path.fromString(it) })
47-
pathToSystemDefault()?.first?.let { yield(it) }
47+
currentSystemTimeZonePath?.splitTimeZonePath()?.first?.let { yield(it) }
4848
}
4949

50+
internal val currentSystemTimeZonePath get() = chaseSymlinks("/etc/localtime")
51+
52+
/**
53+
* Given a path like `/usr/share/zoneinfo/Europe/Berlin`, produces `/usr/share/zoneinfo to Europe/Berlin`.
54+
* Returns null if the function can't recognize the boundary between the time zone and the tzdb.
55+
*/
5056
// taken from https://github.com/HowardHinnant/date/blob/ab37c362e35267d6dee02cb47760f9e9c669d3be/src/tz.cpp#L3951-L3952
51-
internal fun pathToSystemDefault(): Pair<Path, Path>? {
52-
val info = chaseSymlinks("/etc/localtime") ?: return null
53-
val i = info.components.indexOf("zoneinfo")
54-
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
57+
internal fun Path.splitTimeZonePath(): Pair<Path, Path>? {
58+
val i = components.indexOf("zoneinfo")
59+
if (!isAbsolute || i == -1 || i == components.size - 1) return null
5560
return Pair(
56-
Path(true, info.components.subList(0, i + 1)),
57-
Path(false, info.components.subList(i + 1, info.components.size))
61+
Path(true, components.subList(0, i + 1)),
62+
Path(false, components.subList(i + 1, components.size))
5863
)
5964
}

0 commit comments

Comments
 (0)