Skip to content

Commit e25cd72

Browse files
committed
Make searching for timezone databases on Linux more robust
Using the official implementation of tzinfo as a reference.
1 parent 20030bc commit e25cd72

File tree

4 files changed

+50
-26
lines changed

4 files changed

+50
-26
lines changed

core/darwin/src/internal/TimeZoneNative.kt

+3-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import kotlinx.datetime.toKotlinInstant
1212
import kotlinx.datetime.internal.*
1313
import platform.Foundation.*
1414

15-
internal actual val systemTzdb: TimeZoneDatabase get() = tzdbOnFilesystem
15+
internal actual val systemTzdb: TimeZoneDatabase = TzdbOnFilesystem(Path.fromString(defaultTzdbPath()))
16+
17+
internal expect fun defaultTzdbPath(): String
1618

1719
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
1820
/* The framework has its own cache of the system timezone. Calls to
@@ -69,7 +71,3 @@ internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
6971
}
7072

7173
internal actual fun currentTime(): Instant = NSDate.date().toKotlinInstant()
72-
73-
private val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString(defaultTzdbPath()))
74-
75-
internal expect fun defaultTzdbPath(): String

core/linux/src/internal/TimeZoneNative.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
*/
55

66
@file:OptIn(ExperimentalForeignApi::class)
7+
78
package kotlinx.datetime.internal
89

910
import kotlinx.cinterop.*
1011
import kotlinx.datetime.Instant
1112
import platform.posix.*
1213

13-
internal actual val systemTzdb: TimeZoneDatabase get() = tzdbOnFilesystem
14+
internal actual val systemTzdb: TimeZoneDatabase = TzdbOnFilesystem()
1415

1516
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
16-
val zoneId = tzdbOnFilesystem.currentSystemDefault()?.second?.toString()
17+
val zoneId = pathToSystemDefault()?.second?.toString()
1718
?: throw IllegalStateException("Failed to get the system timezone")
1819
return zoneId to null
1920
}
@@ -35,5 +36,3 @@ internal actual fun currentTime(): Instant = memScoped {
3536
throw IllegalStateException("The readings from the system clock are not representable as an Instant")
3637
}
3738
}
38-
39-
private val tzdbOnFilesystem: TzdbOnFilesystem = TzdbOnFilesystem(Path.fromString("/usr/share/zoneinfo"))

core/nix/src/internal/TzdbOnFilesystem.kt

+34-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
package kotlinx.datetime.internal
77

8-
internal class TzdbOnFilesystem(defaultTzdbPath: Path): TimeZoneDatabase {
8+
internal class TzdbOnFilesystem(defaultTzdbPath: Path? = null): TimeZoneDatabase {
9+
10+
private val tzdbPath = tzdbPaths(defaultTzdbPath).find {
11+
it.chaseSymlinks().check()?.isDirectory == true
12+
} ?: throw IllegalStateException("Could not find the path to the timezone database")
913

1014
override fun rulesForId(id: String): TimeZoneRules =
1115
readTzFile(tzdbPath.resolve(Path.fromString(id)).readBytes()).toTimeZoneRules()
@@ -14,31 +18,44 @@ internal class TzdbOnFilesystem(defaultTzdbPath: Path): TimeZoneDatabase {
1418
tzdbPath.traverseDirectory(exclude = tzdbUnneededFiles) { add(it.toString()) }
1519
}
1620

17-
internal fun currentSystemDefault(): Pair<Path, Path>? {
18-
val info = Path(true, listOf("etc", "localtime")).readLink() ?: return null
19-
val i = info.components.indexOf("zoneinfo")
20-
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
21-
return Pair(
22-
Path(true, info.components.subList(0, i + 1)),
23-
Path(false, info.components.subList(i + 1, info.components.size))
24-
)
25-
}
26-
27-
private val tzdbPath = defaultTzdbPath.check()?.let { defaultTzdbPath }
28-
?: currentSystemDefault()?.first ?: throw IllegalStateException("Could not find the path to the timezone database")
29-
3021
}
3122

23+
/** The files that sometimes lie in the `zoneinfo` directory but aren't actually time zones. */
3224
private val tzdbUnneededFiles = setOf(
25+
// taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L88C29-L97C21
26+
"+VERSION",
27+
"leapseconds",
28+
"localtime",
3329
"posix",
3430
"posixrules",
31+
"right",
32+
"SECURITY",
33+
"src",
34+
"timeconfig",
35+
// taken from https://github.com/HowardHinnant/date/blob/ab37c362e35267d6dee02cb47760f9e9c669d3be/src/tz.cpp#L2863-L2874
3536
"Factory",
3637
"iso3166.tab",
37-
"right",
38-
"+VERSION",
3938
"zone.tab",
4039
"zone1970.tab",
4140
"tzdata.zi",
42-
"leapseconds",
4341
"leap-seconds.list"
4442
)
43+
44+
/** The directories checked for a valid timezone database. */
45+
internal fun tzdbPaths(defaultTzdbPath: Path?) = sequence {
46+
defaultTzdbPath?.let { yield(it) }
47+
// taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L70
48+
yieldAll(listOf("/usr/share/zoneinfo", "/usr/share/lib/zoneinfo", "/etc/zoneinfo").map { Path.fromString(it) })
49+
pathToSystemDefault()?.first?.let { yield(it) }
50+
}
51+
52+
// taken from https://github.com/HowardHinnant/date/blob/ab37c362e35267d6dee02cb47760f9e9c669d3be/src/tz.cpp#L3951-L3952
53+
internal fun pathToSystemDefault(): Pair<Path, Path>? {
54+
val info = Path(true, listOf("etc", "localtime")).chaseSymlinks()
55+
val i = info.components.indexOf("zoneinfo")
56+
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
57+
return Pair(
58+
Path(true, info.components.subList(0, i + 1)),
59+
Path(false, info.components.subList(i + 1, info.components.size))
60+
)
61+
}

core/nix/src/internal/filesystem.kt

+10
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ internal class Path(val isAbsolute: Boolean, val components: List<String>) {
5353
}
5454
}
5555

56+
internal fun Path.chaseSymlinks(maxDepth: Int = 100): Path {
57+
var realPath = this
58+
var depth = maxDepth
59+
while (true) {
60+
realPath = realPath.readLink() ?: break
61+
if (depth-- == 0) throw RuntimeException("Too many levels of symbolic links")
62+
}
63+
return realPath
64+
}
65+
5666
// `stat(2)` lists the other available fields
5767
internal interface PathInfo {
5868
val isDirectory: Boolean

0 commit comments

Comments
 (0)