diff --git a/core/linux/src/internal/TimeZoneNative.kt b/core/linux/src/internal/TimeZoneNative.kt index 745023aa..38857918 100644 --- a/core/linux/src/internal/TimeZoneNative.kt +++ b/core/linux/src/internal/TimeZoneNative.kt @@ -5,6 +5,8 @@ package kotlinx.datetime.internal +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString import kotlinx.datetime.IllegalTimeZoneException import kotlinx.datetime.TimeZone @@ -16,10 +18,45 @@ internal actual fun getAvailableZoneIds(): Set = private val tzdb = runCatching { TzdbOnFilesystem() } +// This workaround is needed for Debian versions Etch (4.0) - Jessie (8.0), where the timezone data is organized differently. +// See: https://github.com/Kotlin/kotlinx-datetime/issues/430 +@OptIn(ExperimentalForeignApi::class) +private fun getTimezoneFromEtcTimezone(): String? { + val timezoneContent = Path.fromString("${systemTimezoneSearchRoot}etc/timezone").readBytes()?.toKString()?.trim() ?: return null + val zoneId = chaseSymlinks("${systemTimezoneSearchRoot}usr/share/zoneinfo/$timezoneContent") + ?.splitTimeZonePath()?.second?.toString() + ?: return null + + val zoneInfoBytes = Path.fromString("${systemTimezoneSearchRoot}usr/share/zoneinfo/$zoneId").readBytes() ?: return null + val localtimeBytes = Path.fromString("${systemTimezoneSearchRoot}etc/localtime").readBytes() ?: return null + + if (!localtimeBytes.contentEquals(zoneInfoBytes)) { + val displayTimezone = when (timezoneContent) { + zoneId -> "'$zoneId'" + else -> "'$timezoneContent' (resolved to '$zoneId')" + } + throw IllegalTimeZoneException( + "Timezone mismatch: ${systemTimezoneSearchRoot}etc/timezone specifies $displayTimezone " + + "but ${systemTimezoneSearchRoot}etc/localtime content differs from ${systemTimezoneSearchRoot}usr/share/zoneinfo/$zoneId" + ) + } + + return zoneId +} + internal actual fun currentSystemDefaultZone(): Pair { - // according to https://www.man7.org/linux/man-pages/man5/localtime.5.html, when there is no symlink, UTC is used + // According to https://www.man7.org/linux/man-pages/man5/localtime.5.html, UTC is used when /etc/localtime is missing. + // If /etc/localtime exists but isn't a symlink, we check if it's a copy of a timezone file by examining /etc/timezone + // (which is a Debian-specific approach used in older distributions). 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 -} + + zonePath.splitTimeZonePath()?.second?.toString()?.let { zoneId -> + return zoneId to null + } + + getTimezoneFromEtcTimezone()?.let { zoneId -> + return zoneId to null + } + + throw IllegalTimeZoneException("Could not determine the timezone ID that `$zonePath` corresponds to") +} \ No newline at end of file diff --git a/core/linux/test/TimeZoneNativeTest.kt b/core/linux/test/TimeZoneNativeTest.kt new file mode 100644 index 00000000..3ca78b77 --- /dev/null +++ b/core/linux/test/TimeZoneNativeTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test + +import kotlinx.datetime.IllegalTimeZoneException +import kotlinx.datetime.TimeZone +import kotlinx.datetime.internal.systemTimezoneSearchRoot +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class TimeZoneNativeTest { + + @Test + fun correctSymlinkTest() = withFakeRoot("${RESOURCES}correct-symlink/") { + val tz = TimeZone.currentSystemDefault() + assertEquals(TimeZone.of("Europe/Oslo"), tz) + } + + @Test + fun timezoneFileAgreesWithLocaltimeContentsTest() = withFakeRoot("${RESOURCES}timezone-file-agrees-with-localtime-contents/") { + val tz = TimeZone.currentSystemDefault() + assertEquals(TimeZone.of("Europe/Oslo"), tz) + } + + @Test + fun fallbackToUTCWhenNoLocaltimeTest() = withFakeRoot("${RESOURCES}fallback-to-utc-when-no-localtime/") { + val tz = TimeZone.currentSystemDefault() + assertEquals(TimeZone.UTC, tz) + } + + @Test + fun missingTimezoneWhenLocaltimeIsNotSymlinkTest() = withFakeRoot("${RESOURCES}missing-timezone-when-localtime-is-not-symlink/") { + assertFailsWith { + TimeZone.currentSystemDefault() + } + } + + @Test + fun nonExistentTimezoneInTimezoneFileTest() = withFakeRoot("${RESOURCES}non-existent-timezone-in-timezone-file/") { + assertFailsWith { + TimeZone.currentSystemDefault() + } + } + + @Test + fun timezoneFileDisagreesWithLocaltimeContentsTest() = withFakeRoot("${RESOURCES}timezone-file-disagrees-with-localtime-contents/") { + val exception = assertFailsWith { + TimeZone.currentSystemDefault() + } + + assertTrue( + exception.message?.contains("Europe/Oslo") == true, + "Exception message does not contain 'Europe/Oslo' as expected" + ) + } + + companion object { + const val RESOURCES = "./linux/test/time-zone-native-test-resources/" + + private fun withFakeRoot(fakeRoot: String, action: () -> Unit) { + val defaultRoot = systemTimezoneSearchRoot + systemTimezoneSearchRoot = fakeRoot + try { + action() + } finally { + systemTimezoneSearchRoot = defaultRoot + } + } + } +} \ No newline at end of file diff --git a/core/linux/test/time-zone-native-test-resources/correct-symlink/etc/localtime b/core/linux/test/time-zone-native-test-resources/correct-symlink/etc/localtime new file mode 120000 index 00000000..302adea9 --- /dev/null +++ b/core/linux/test/time-zone-native-test-resources/correct-symlink/etc/localtime @@ -0,0 +1 @@ +../usr/share/zoneinfo/Europe/Oslo \ No newline at end of file diff --git a/core/linux/test/time-zone-native-test-resources/correct-symlink/usr/share/zoneinfo/Europe/Oslo b/core/linux/test/time-zone-native-test-resources/correct-symlink/usr/share/zoneinfo/Europe/Oslo new file mode 100644 index 00000000..15a34c3c Binary files /dev/null and b/core/linux/test/time-zone-native-test-resources/correct-symlink/usr/share/zoneinfo/Europe/Oslo differ diff --git a/core/linux/test/time-zone-native-test-resources/fallback-to-utc-when-no-localtime/etc/.keep b/core/linux/test/time-zone-native-test-resources/fallback-to-utc-when-no-localtime/etc/.keep new file mode 100644 index 00000000..e69de29b diff --git a/core/linux/test/time-zone-native-test-resources/fallback-to-utc-when-no-localtime/usr/share/zoneinfo/Europe/.keep b/core/linux/test/time-zone-native-test-resources/fallback-to-utc-when-no-localtime/usr/share/zoneinfo/Europe/.keep new file mode 100644 index 00000000..e69de29b diff --git a/core/linux/test/time-zone-native-test-resources/missing-timezone-when-localtime-is-not-symlink/etc/localtime b/core/linux/test/time-zone-native-test-resources/missing-timezone-when-localtime-is-not-symlink/etc/localtime new file mode 100644 index 00000000..e69de29b diff --git a/core/linux/test/time-zone-native-test-resources/non-existent-timezone-in-timezone-file/etc/localtime b/core/linux/test/time-zone-native-test-resources/non-existent-timezone-in-timezone-file/etc/localtime new file mode 100644 index 00000000..e69de29b diff --git a/core/linux/test/time-zone-native-test-resources/non-existent-timezone-in-timezone-file/etc/timezone b/core/linux/test/time-zone-native-test-resources/non-existent-timezone-in-timezone-file/etc/timezone new file mode 100644 index 00000000..6bdbd727 --- /dev/null +++ b/core/linux/test/time-zone-native-test-resources/non-existent-timezone-in-timezone-file/etc/timezone @@ -0,0 +1 @@ +incorrect/timezone diff --git a/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/etc/localtime b/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/etc/localtime new file mode 100644 index 00000000..15a34c3c Binary files /dev/null and b/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/etc/localtime differ diff --git a/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/etc/timezone b/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/etc/timezone new file mode 100644 index 00000000..ccccd5d4 --- /dev/null +++ b/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/etc/timezone @@ -0,0 +1 @@ +Europe/Oslo diff --git a/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/usr/share/zoneinfo/Europe/Oslo b/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/usr/share/zoneinfo/Europe/Oslo new file mode 100644 index 00000000..15a34c3c Binary files /dev/null and b/core/linux/test/time-zone-native-test-resources/timezone-file-agrees-with-localtime-contents/usr/share/zoneinfo/Europe/Oslo differ diff --git a/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/etc/localtime b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/etc/localtime new file mode 100644 index 00000000..7f6d958f Binary files /dev/null and b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/etc/localtime differ diff --git a/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/etc/timezone b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/etc/timezone new file mode 100644 index 00000000..ccccd5d4 --- /dev/null +++ b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/etc/timezone @@ -0,0 +1 @@ +Europe/Oslo diff --git a/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/usr/share/zoneinfo/Europe/Berlin b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/usr/share/zoneinfo/Europe/Berlin new file mode 100644 index 00000000..7f6d958f Binary files /dev/null and b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/usr/share/zoneinfo/Europe/Berlin differ diff --git a/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/usr/share/zoneinfo/Europe/Oslo b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/usr/share/zoneinfo/Europe/Oslo new file mode 100644 index 00000000..15a34c3c Binary files /dev/null and b/core/linux/test/time-zone-native-test-resources/timezone-file-disagrees-with-localtime-contents/usr/share/zoneinfo/Europe/Oslo differ diff --git a/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt b/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt index 0032197a..e3968c17 100644 --- a/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt +++ b/core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt @@ -47,7 +47,9 @@ internal fun tzdbPaths(defaultTzdbPath: Path?) = sequence { currentSystemTimeZonePath?.splitTimeZonePath()?.first?.let { yield(it) } } -internal val currentSystemTimeZonePath get() = chaseSymlinks("/etc/localtime") +internal var systemTimezoneSearchRoot: String = "/" + +internal val currentSystemTimeZonePath get() = chaseSymlinks("${systemTimezoneSearchRoot}etc/localtime") /** * Given a path like `/usr/share/zoneinfo/Europe/Berlin`, produces `/usr/share/zoneinfo to Europe/Berlin`.