Skip to content

Commit 87dbb18

Browse files
committed
More robustly search for zoneinfo directories
1 parent 2cc6b6a commit 87dbb18

File tree

6 files changed

+53
-35
lines changed

6 files changed

+53
-35
lines changed

core/darwinDevices/src/internal/TimeZoneNative.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66
package kotlinx.datetime.internal
77

8-
internal actual fun getTzdbPath(): Path = Path.fromString("/var/db/timezone/zoneinfo")
8+
internal actual fun defaultTzdbPath(): String? = "/var/db/timezone/zoneinfo"

core/darwinSimulator/src/internal/TimeZoneNative.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66
package kotlinx.datetime.internal
77

8-
internal actual fun getTzdbPath(): Path = Path.fromString("/usr/share/zoneinfo.default")
8+
internal actual fun defaultTzdbPath(): String? = "/usr/share/zoneinfo.default"

core/linux/src/internal/TimeZoneNative.kt

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,4 @@ internal actual fun currentSystemDefaultZone(): RegionTimeZone {
1111
return RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId)
1212
}
1313

14-
internal actual fun getTzdbPath(): Path {
15-
val defaultPath = Path.fromString("/usr/share/zoneinfo")
16-
return defaultPath.check()?.let { defaultPath }
17-
?: pathToSystemDefault()?.first ?: throw IllegalStateException("Could not find the path to the timezone database")
18-
}
19-
20-
private fun pathToSystemDefault(): Pair<Path, Path>? {
21-
val info = Path(true, listOf("etc", "localtime")).readLink() ?: return null
22-
val i = info.components.indexOf("zoneinfo")
23-
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
24-
return Pair(
25-
Path(true, info.components.subList(0, i + 1)),
26-
Path(false, info.components.subList(i + 1, info.components.size))
27-
)
28-
}
14+
internal actual fun defaultTzdbPath(): String? = null

core/nix/src/internal/TimeZoneNative.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.

core/nix/src/internal/TzdbOnFilesystem.kt

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
package kotlinx.datetime.internal
77

8-
import kotlinx.datetime.*
9-
108
internal class TzdbOnFilesystem(val tzdbPath: Path): TimezoneDatabase {
119

1210
override fun rulesForId(id: String): TimeZoneRules =
@@ -18,16 +16,49 @@ internal class TzdbOnFilesystem(val tzdbPath: Path): TimezoneDatabase {
1816

1917
}
2018

19+
/** The files that sometimes lie in the `zoneinfo` directory but aren't actually time zones. */
2120
private val tzdbUnneededFiles = setOf(
21+
// taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L88C29-L97C21
22+
"+VERSION",
23+
"leapseconds",
24+
"localtime",
2225
"posix",
2326
"posixrules",
27+
"right",
28+
"SECURITY",
29+
"src",
30+
"timeconfig",
31+
// taken from https://github.com/HowardHinnant/date/blob/ab37c362e35267d6dee02cb47760f9e9c669d3be/src/tz.cpp#L2863-L2874
2432
"Factory",
2533
"iso3166.tab",
26-
"right",
27-
"+VERSION",
2834
"zone.tab",
2935
"zone1970.tab",
3036
"tzdata.zi",
31-
"leapseconds",
3237
"leap-seconds.list"
3338
)
39+
40+
/** If the platform has a preference for a specific timezone database path, this field contains it. */
41+
internal expect fun defaultTzdbPath(): String?
42+
43+
/** The directories checked for a valid timezone database. */
44+
private val tzdbPaths = sequence {
45+
defaultTzdbPath()?.let { yield(Path.fromString(it)) }
46+
// taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L70
47+
yieldAll(listOf("/usr/share/zoneinfo", "/usr/share/lib/zoneinfo", "/etc/zoneinfo").map { Path.fromString(it) })
48+
pathToSystemDefault()?.first?.let { yield(it) }
49+
}
50+
51+
// taken from https://github.com/HowardHinnant/date/blob/ab37c362e35267d6dee02cb47760f9e9c669d3be/src/tz.cpp#L3951-L3952
52+
internal fun pathToSystemDefault(): Pair<Path, Path>? {
53+
val info = Path(true, listOf("etc", "localtime")).chaseSymlinks().first
54+
val i = info.components.indexOf("zoneinfo")
55+
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
56+
return Pair(
57+
Path(true, info.components.subList(0, i + 1)),
58+
Path(false, info.components.subList(i + 1, info.components.size))
59+
)
60+
}
61+
62+
internal actual val systemTzdb: TimezoneDatabase = tzdbPaths.find {
63+
it.chaseSymlinks().second?.isDirectory == true
64+
}?.let { TzdbOnFilesystem(it) } ?: throw IllegalStateException("Could not find the path to the timezone database")

core/nix/src/internal/filesystem.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
55

6-
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
6+
@file:OptIn(ExperimentalForeignApi::class)
77
package kotlinx.datetime.internal
88

99
import kotlinx.cinterop.*
@@ -15,8 +15,8 @@ internal class Path(val isAbsolute: Boolean, val components: List<String>) {
1515
val err = stat(this@Path.toString(), stat.ptr)
1616
if (err != 0) return null
1717
object : PathInfo {
18-
override val isDirectory: Boolean = stat.st_mode.toInt() and S_IFMT == S_IFDIR // `inode(7)`, S_ISDIR
19-
override val isSymlink: Boolean = stat.st_mode.toInt() and S_IFMT == S_IFLNK // `inode(7)`, S_ISLNK
18+
override val isDirectory: Boolean get() = stat.st_mode.toInt() and S_IFMT == S_IFDIR // `inode(7)`, S_ISDIR
19+
override val isSymlink: Boolean get() = stat.st_mode.toInt() and S_IFMT == S_IFLNK // `inode(7)`, S_ISLNK
2020
}
2121
}
2222

@@ -53,6 +53,17 @@ internal class Path(val isAbsolute: Boolean, val components: List<String>) {
5353
}
5454
}
5555

56+
internal fun Path.chaseSymlinks(): Pair<Path, PathInfo?> {
57+
var realPath = this
58+
var stat: PathInfo? = realPath.check()
59+
while (stat?.isSymlink == true) {
60+
stat = null
61+
realPath = realPath.readLink() ?: break
62+
stat = realPath.check() ?: break
63+
}
64+
return realPath to stat
65+
}
66+
5667
// `stat(2)` lists the other available fields
5768
internal interface PathInfo {
5869
val isDirectory: Boolean

0 commit comments

Comments
 (0)