Skip to content

Support obtaining the system timezone on old Debian-based distributions #503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 82 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
ed6e3cd
#430: Add testcontainers dependencies
DmitryNekrasov Mar 19, 2025
37e67fe
#430: Add dockerfiles
DmitryNekrasov Mar 19, 2025
0e4aadd
#430: Fix build.gradle.kts to depend on JUnit 5 to correct running of…
DmitryNekrasov Mar 19, 2025
20dc7bc
#430: Add container initialization, run simple commant inside contain…
DmitryNekrasov Mar 19, 2025
fbbd55f
#430: Add logger configuration
DmitryNekrasov Mar 19, 2025
5facbfa
#430: dockerfilePath refactoring
DmitryNekrasov Mar 19, 2025
8cfd36d
#430: Run tests inside containers
DmitryNekrasov Mar 19, 2025
683ce09
#430: Add defaultTimeZoneTest that runs inside containers
DmitryNekrasov Mar 19, 2025
e638490
#430: Split test on 2, pass cantainer as parameter
DmitryNekrasov Mar 19, 2025
3d59b39
#430: Add BeforeAll buildTestBinary
DmitryNekrasov Mar 19, 2025
0614e61
#430: Fix container stdout
DmitryNekrasov Mar 19, 2025
d294ac1
#430: Move defaultTimeZoneTest from common to linux/native
DmitryNekrasov Mar 19, 2025
fc08cb9
#430: Check INSIDE_TESTCONTAINERS env var to run test only in testcon…
DmitryNekrasov Mar 19, 2025
403ebf9
#430: Replace shouldRunTests check to Testcontainers.runIfAvailable
DmitryNekrasov Mar 20, 2025
54ad227
#430: Refactor Skipping test message
DmitryNekrasov Mar 20, 2025
1d0ff83
#430: Rename TimeZoneTest to TimeZoneConfigurationTest
DmitryNekrasov Mar 20, 2025
62e4f5b
#430: Split runTest to runTimeZoneTests and runAllTests
DmitryNekrasov Mar 20, 2025
6d84895
#430: Test fail is execResult.exitCode != 0
DmitryNekrasov Mar 20, 2025
7d620bd
#430: Removed unnecessary dependency junit-params
DmitryNekrasov Mar 20, 2025
269db7f
#430: First working iteration
DmitryNekrasov Mar 20, 2025
acfc235
#430: Add file comparison + some refactoring
DmitryNekrasov Mar 20, 2025
c52b60a
#430: Minor refactoring
DmitryNekrasov Mar 20, 2025
e36db1b
#430: Add 3 tests
DmitryNekrasov Mar 20, 2025
ddb105d
#430: Refactoring
DmitryNekrasov Mar 20, 2025
733158d
#430: Add allTimeZoneFilesMissingTest test, fails because of Expected…
DmitryNekrasov Mar 20, 2025
a156cad
#430: Add symlinkTimeZoneTest
DmitryNekrasov Mar 20, 2025
efc67b8
#430: Add invalidTimezoneFormatTest
DmitryNekrasov Mar 20, 2025
5902399
#430: Add commonTimeZoneTests
DmitryNekrasov Mar 20, 2025
325f92b
#430: Change Z to UTC in currentSystemDefaultZone()
DmitryNekrasov Mar 20, 2025
03e03c7
#430: UTC -> Z
DmitryNekrasov Mar 21, 2025
9f8aac8
#430: Add 2 new Dockerfile for Debian Jessie and Ubuntu 24.04
DmitryNekrasov Mar 21, 2025
0697ae8
#430: Fix platform for ubuntu
DmitryNekrasov Mar 21, 2025
dfdcb2a
#430: Remove some tests, run 2 tests on Debian Jessie
DmitryNekrasov Mar 21, 2025
bff21b1
#430: Add INSIDE_TESTCONTAINERS env var to Dockerfiles
DmitryNekrasov Mar 21, 2025
6b15d79
#430: Fix defaultTimeZoneTest test
DmitryNekrasov Mar 21, 2025
0ee491f
#430: Run tests on both containers
DmitryNekrasov Mar 21, 2025
548ed68
#430: Refactoring
DmitryNekrasov Mar 21, 2025
c9cf312
#430: Refactor tests
DmitryNekrasov Mar 21, 2025
0e7a3c9
#430: Refactor tests
DmitryNekrasov Mar 24, 2025
0240f49
#430: Add jessieDefaultConfigTest, jessieMissingLocaltimeTest, nobleD…
DmitryNekrasov Mar 24, 2025
c007bac
#430: Move ContainerType
DmitryNekrasov Mar 24, 2025
95dd6d9
#430: Refactor dockerfiles
DmitryNekrasov Mar 24, 2025
38cab93
#430: Refactor ContainerType
DmitryNekrasov Mar 24, 2025
9ee58d7
#430: Add nobleIncorrectSymlinkTest
DmitryNekrasov Mar 24, 2025
192f1a2
#430: Add disabledWithoutDocker = true (may help to users that don't …
DmitryNekrasov Mar 24, 2025
1dc04aa
#430: Add @TestInstance(PER_CLASS) to TimeZoneConfigurationTest
DmitryNekrasov Mar 24, 2025
df35ba0
#430: Add jessieMissingTimezoneTest
DmitryNekrasov Mar 24, 2025
2234375
#430: Add jessieIncorrectTimezoneTest
DmitryNekrasov Mar 24, 2025
7a38874
#430: Add jessieDifferentTimezonesTest
DmitryNekrasov Mar 24, 2025
7cc417f
#430: Add comment to fallsBackToUniversal
DmitryNekrasov Mar 24, 2025
d678adf
#430: Add container descriptions in ContainerType
DmitryNekrasov Mar 24, 2025
7f48fc3
#430: Remove WORKDIR from dockerfiles
DmitryNekrasov Mar 24, 2025
d6aa923
#430: Remove logging duplication
DmitryNekrasov Mar 24, 2025
d7acaca
#430: Add comment to currentSystemDefaultZone
DmitryNekrasov Mar 24, 2025
27d7306
#430: Add internal root field
DmitryNekrasov Mar 25, 2025
816f2af
#430: Add correctSymlinkTest
DmitryNekrasov Mar 26, 2025
901cc68
#430: Add TimeZoneConfigurationTest Ignore
DmitryNekrasov Mar 26, 2025
2964b49
#430: Add fallsBackToUTC test
DmitryNekrasov Mar 26, 2025
916703b
#430: Add pwd
DmitryNekrasov Mar 26, 2025
12f56d9
#430: Remove 'core' from path
DmitryNekrasov Mar 26, 2025
bf689b3
#430: Add missingTimezoneTest
DmitryNekrasov Mar 27, 2025
ed277ba
#430: Add incorrectTimezoneTest
DmitryNekrasov Mar 27, 2025
6cb1b0c
#430: Remove Oslo file from missing-localtime test
DmitryNekrasov Mar 27, 2025
e261749
#430: Add differentTimezonesTest
DmitryNekrasov Mar 27, 2025
b4f5cb3
#430: Add differentTimezonesTest
DmitryNekrasov Mar 27, 2025
da58775
#430: Remove pwd
DmitryNekrasov Mar 27, 2025
1180d03
#430: Add test related files
DmitryNekrasov Mar 27, 2025
159c4f2
#430: Remove testcontainers relaited files
DmitryNekrasov Mar 27, 2025
33e3c7f
#430: Remove testcontainers dependencies
DmitryNekrasov Mar 27, 2025
49480b8
#430: Remove logback-test.xml
DmitryNekrasov Mar 27, 2025
27128e8
#430: Introduce withFakeRoot helper method
DmitryNekrasov Mar 27, 2025
2198821
#430: Add comment about workaround
DmitryNekrasov Mar 31, 2025
7105210
#430: Rename root to systemTimezoneSearchRoot
DmitryNekrasov Mar 31, 2025
3866fbf
#430: Rename fallbackToUTCWhenNoLocaltime test
DmitryNekrasov Mar 31, 2025
f64b3bd
#430: Rename missingTimezoneWhenLocaltimeIsNotSymlinkTest test
DmitryNekrasov Mar 31, 2025
d2a9dca
#430: Remove exception message checks
DmitryNekrasov Mar 31, 2025
d429aa8
#430: Rename timezoneFileAgreesWithLocaltimeContents
DmitryNekrasov Mar 31, 2025
c446aa9
#430: Rename nonExistentTimezoneInTimezoneFile
DmitryNekrasov Mar 31, 2025
ea2b11f
#430: Rename timezoneFileDisagreesWithLocaltimeContentsTest
DmitryNekrasov Mar 31, 2025
756c89e
#430: Refactoring
DmitryNekrasov Mar 31, 2025
dd3ce5d
#430: Change exception message check
DmitryNekrasov Mar 31, 2025
c68adb0
#430: Change exception message check
DmitryNekrasov Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions core/linux/src/internal/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package kotlinx.datetime.internal

import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.toKString
import kotlinx.datetime.IllegalTimeZoneException
import kotlinx.datetime.TimeZone

Expand All @@ -16,10 +18,45 @@ internal actual fun getAvailableZoneIds(): Set<String> =

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<String, TimeZone?> {
// 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")
}
75 changes: 75 additions & 0 deletions core/linux/test/TimeZoneNativeTest.kt
Original file line number Diff line number Diff line change
@@ -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<IllegalTimeZoneException> {
TimeZone.currentSystemDefault()
}
}

@Test
fun nonExistentTimezoneInTimezoneFileTest() = withFakeRoot("${RESOURCES}non-existent-timezone-in-timezone-file/") {
assertFailsWith<IllegalTimeZoneException> {
TimeZone.currentSystemDefault()
}
}

@Test
fun timezoneFileDisagreesWithLocaltimeContentsTest() = withFakeRoot("${RESOURCES}timezone-file-disagrees-with-localtime-contents/") {
val exception = assertFailsWith<IllegalTimeZoneException> {
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
}
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
incorrect/timezone
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Europe/Oslo
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Europe/Oslo
Binary file not shown.
Binary file not shown.
4 changes: 3 additions & 1 deletion core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down