Skip to content

Commit ce30554

Browse files
authored
No longer use stat in timezone database implementations (#385)
<https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api> states that Apple will require the apps to declare all usages of `stat`, as it can be used to check the file timestamps.
1 parent 9de96c0 commit ce30554

File tree

5 files changed

+39
-60
lines changed

5 files changed

+39
-60
lines changed

core/androidNative/src/internal/TzdbBionic.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ internal fun TzdbBionic(): TimeZoneDatabase = TzdbBionic(buildMap<String, TzdbBi
2626
Path.fromString("/system/usr/share/zoneinfo/tzdata"), // immutable fallback tzdb
2727
Path.fromString("/apex/com.android.tzdata/etc/tz/tzdata"), // an up-to-date tzdb, may not exist
2828
)) {
29-
if (path.check() == null) continue // the file does not exist
3029
// be careful to only read each file a single time and keep many references to the same ByteArray in memory.
31-
val content = path.readBytes()
30+
val content = path.readBytes() ?: continue // this file does not exist
3231
val header = BionicTzdbHeader.parse(content)
3332
val indexSize = header.data_offset - header.index_offset
3433
check(indexSize % 52 == 0) { "Invalid index size: $indexSize (must be a multiple of 52)" }

core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt

+15-6
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,28 @@ package kotlinx.datetime.internal
77

88
internal class TzdbOnFilesystem(defaultTzdbPath: Path? = null): TimeZoneDatabase {
99

10-
private val tzdbPath = tzdbPaths(defaultTzdbPath).find {
11-
it.chaseSymlinks().check()?.isDirectory == true
10+
internal val tzdbPath = tzdbPaths(defaultTzdbPath).find { path ->
11+
tabPaths.any { path.containsFile(it) }
1212
} ?: throw IllegalStateException("Could not find the path to the timezone database")
1313

14-
override fun rulesForId(id: String): TimeZoneRules =
15-
readTzFile(tzdbPath.resolve(Path.fromString(id)).readBytes()).toTimeZoneRules()
14+
override fun rulesForId(id: String): TimeZoneRules {
15+
val idAsPath = Path.fromString(id)
16+
require(!idAsPath.isAbsolute) { "Timezone ID '$idAsPath' must not begin with a '/'" }
17+
require(idAsPath.components.none { it == ".." }) { "'$idAsPath' must not contain '..' as a component" }
18+
val file = Path(tzdbPath.isAbsolute, tzdbPath.components + idAsPath.components)
19+
val contents = file.readBytes() ?: throw RuntimeException("File '$file' not found")
20+
return readTzFile(contents).toTimeZoneRules()
21+
}
1622

1723
override fun availableTimeZoneIds(): Set<String> = buildSet {
18-
tzdbPath.traverseDirectory(exclude = tzdbUnneededFiles) { add(it.toString()) }
24+
tzdbPath.tryTraverseDirectory(exclude = tzdbUnneededFiles) { add(it.toString()) }
1925
}
2026

2127
}
2228

29+
// taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L354
30+
private val tabPaths = listOf("zone1970.tab", "zone.tab", "tab/zone_sun.tab")
31+
2332
/** The files that sometimes lie in the `zoneinfo` directory but aren't actually time zones. */
2433
private val tzdbUnneededFiles = setOf(
2534
// taken from https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/data_sources/zoneinfo_data_source.rb#L88C29-L97C21
@@ -51,7 +60,7 @@ internal fun tzdbPaths(defaultTzdbPath: Path?) = sequence {
5160

5261
// taken from https://github.com/HowardHinnant/date/blob/ab37c362e35267d6dee02cb47760f9e9c669d3be/src/tz.cpp#L3951-L3952
5362
internal fun pathToSystemDefault(): Pair<Path, Path>? {
54-
val info = Path(true, listOf("etc", "localtime")).chaseSymlinks()
63+
val info = chaseSymlinks("/etc/localtime") ?: return null
5564
val i = info.components.indexOf("zoneinfo")
5665
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
5766
return Pair(

core/tzdbOnFilesystem/src/internal/filesystem.kt

+18-16
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,37 @@ package kotlinx.datetime.internal
99
import kotlinx.cinterop.*
1010
import platform.posix.*
1111

12-
internal fun Path.chaseSymlinks(maxDepth: Int = 100): Path {
13-
var realPath = this
14-
var depth = maxDepth
15-
while (true) {
16-
realPath = realPath.readLink() ?: break
17-
if (depth-- == 0) throw RuntimeException("Too many levels of symbolic links")
18-
}
19-
return realPath
12+
internal fun chaseSymlinks(name: String): Path? = memScoped {
13+
val buffer = allocArray<ByteVar>(PATH_MAX)
14+
realpath(name, buffer)?.let { Path.fromString(it.toKString()) }
2015
}
2116

22-
internal fun Path.traverseDirectory(exclude: Set<String> = emptySet(), stripLeadingComponents: Int = this.components.size, actionOnFile: (Path) -> Unit) {
23-
val handler = opendir(this.toString()) ?: return
17+
internal fun Path.containsFile(file: String): Boolean = access("$this/$file", F_OK) == 0
18+
19+
internal fun Path.tryTraverseDirectory(
20+
exclude: Set<String> = emptySet(),
21+
stripLeadingComponents: Int = this.components.size,
22+
maxDepth: Int = 100,
23+
actionOnFile: (Path) -> Unit
24+
): Boolean {
25+
if (maxDepth <= 0) throw IllegalStateException("Max depth reached: $this")
26+
val handler = opendir(this.toString()) ?: return false
2427
try {
2528
while (true) {
2629
val entry = readdir(handler) ?: break
2730
val name = entry.pointed.d_name.toKString()
2831
if (name == "." || name == "..") continue
2932
if (name in exclude) continue
3033
val path = Path(isAbsolute, components + name)
31-
val info = path.check() ?: continue // skip broken symlinks
32-
if (info.isDirectory) {
33-
if (!info.isSymlink) {
34-
path.traverseDirectory(exclude, stripLeadingComponents, actionOnFile)
35-
}
36-
} else {
34+
val isDirectory = path.tryTraverseDirectory(
35+
exclude, stripLeadingComponents, maxDepth = maxDepth - 1, actionOnFile
36+
)
37+
if (!isDirectory) {
3738
actionOnFile(Path(false, path.components.drop(stripLeadingComponents)))
3839
}
3940
}
4041
} finally {
4142
closedir(handler)
4243
}
44+
return true
4345
}

core/tzdbOnFilesystem/test/TimeZoneRulesCompleteTest.kt

+3-5
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ class TimeZoneRulesCompleteTest {
1616
@OptIn(ExperimentalEncodingApi::class)
1717
@Test
1818
fun iterateOverAllTimezones() {
19-
val root = Path.fromString("/usr/share/zoneinfo")
20-
val tzdb = TzdbOnFilesystem(root)
19+
val tzdb = TzdbOnFilesystem()
2120
for (id in tzdb.availableTimeZoneIds()) {
22-
val file = root.resolve(Path.fromString(id))
2321
val rules = tzdb.rulesForId(id)
24-
runUnixCommand("env LOCALE=C zdump -V $file").windowed(size = 2, step = 2).forEach { (line1, line2) ->
22+
runUnixCommand("env LOCALE=C zdump -V ${tzdb.tzdbPath}/$id").windowed(size = 2, step = 2).forEach { (line1, line2) ->
2523
val beforeTransition = parseZdumpLine(line1)
2624
val afterTransition = parseZdumpLine(line2)
2725
try {
@@ -51,7 +49,7 @@ class TimeZoneRulesCompleteTest {
5149
} catch (e: Throwable) {
5250
println(beforeTransition)
5351
println(afterTransition)
54-
println(Base64.encode(file.readBytes()))
52+
println(Base64.encode(Path.fromString("${tzdb.tzdbPath}/$id").readBytes()!!))
5553
throw e
5654
}
5755
}

core/tzfile/src/internal/filesystem.kt

+2-31
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,6 @@ import kotlinx.cinterop.*
1010
import platform.posix.*
1111

1212
internal class Path(val isAbsolute: Boolean, val components: List<String>) {
13-
fun check(): PathInfo? = memScoped {
14-
val stat = alloc<stat>()
15-
val err = stat(this@Path.toString(), stat.ptr)
16-
if (err != 0) return null
17-
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
20-
}
21-
}
22-
23-
fun readLink(): Path? = memScoped {
24-
val buffer = allocArray<ByteVar>(PATH_MAX)
25-
val err = readlink(this@Path.toString(), buffer, PATH_MAX.convert<size_t>())
26-
if (err == (-1).convert<ssize_t>()) return null
27-
buffer[err] = 0
28-
fromString(buffer.toKString())
29-
}
30-
31-
fun resolve(other: Path): Path = when {
32-
other.isAbsolute -> other
33-
else -> Path(isAbsolute, components + other.components)
34-
}
35-
3613
override fun toString(): String = buildString {
3714
if (isAbsolute) append("/")
3815
if (components.isNotEmpty()) {
@@ -53,14 +30,8 @@ internal class Path(val isAbsolute: Boolean, val components: List<String>) {
5330
}
5431
}
5532

56-
// `stat(2)` lists the other available fields
57-
internal interface PathInfo {
58-
val isDirectory: Boolean
59-
val isSymlink: Boolean
60-
}
61-
62-
internal fun Path.readBytes(): ByteArray {
63-
val handler = fopen(this.toString(), "rb") ?: throw RuntimeException("Cannot open file $this")
33+
internal fun Path.readBytes(): ByteArray? {
34+
val handler = fopen(this.toString(), "rb") ?: return null
6435
try {
6536
var err = fseek(handler, 0, SEEK_END)
6637
if (err == -1) throw RuntimeException("Cannot jump to the end of $this: $errnoString")

0 commit comments

Comments
 (0)