Skip to content

Commit 290a666

Browse files
authored
Implement kotlinx-datetime for the Android native targets (#344)
To test, run an Android emulator, and then, in the command line, ./gradlew androidNativeArm64TestBinaries && adb push core/build/bin/androidNativeArm64/debugTest/test.kexe /data/local/tmp/ && adb shell /data/local/tmp/test.kexe Change `Arm64` to another platfom (`X86`, `X64`, or `Arm32`) as needed.
1 parent ab0a40d commit 290a666

File tree

13 files changed

+224
-76
lines changed

13 files changed

+224
-76
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2019-2023 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
@file:OptIn(ExperimentalForeignApi::class)
7+
package kotlinx.datetime.internal
8+
9+
import kotlinx.cinterop.*
10+
import platform.posix.*
11+
12+
internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
13+
14+
private val tzdb = runCatching { TzdbBionic() }
15+
16+
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> = memScoped {
17+
val name = readSystemProperty("persist.sys.timezone")
18+
?: throw IllegalStateException("The system property 'persist.sys.timezone' should contain the system timezone")
19+
return name to null
20+
}
21+
22+
private fun readSystemProperty(name: String): String? = memScoped {
23+
// see https://android.googlesource.com/platform/bionic/+/froyo/libc/include/sys/system_properties.h
24+
val result = allocArray<ByteVar>(92)
25+
val error = __system_property_get(name, result)
26+
if (error == 0) null else result.toKString()
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
/*
6+
* Based on the bionic project.
7+
* Copyright (C) 2017 The Android Open Source Project
8+
*/
9+
10+
package kotlinx.datetime.internal
11+
12+
private class TzdbBionic(private val rules: Map<String, Entry>) : TimeZoneDatabase {
13+
override fun rulesForId(id: String): TimeZoneRules =
14+
rules[id]?.readRules() ?: throw IllegalStateException("Unknown time zone $id")
15+
16+
override fun availableTimeZoneIds(): Set<String> = rules.keys
17+
18+
class Entry(val file: ByteArray, val offset: Int, val length: Int) {
19+
fun readRules(): TimeZoneRules = readTzFile(file.copyOfRange(offset, offset + length)).toTimeZoneRules()
20+
}
21+
}
22+
23+
// see https://android.googlesource.com/platform/bionic/+/master/libc/tzcode/bionic.cpp for the format
24+
internal fun TzdbBionic(): TimeZoneDatabase = TzdbBionic(buildMap<String, TzdbBionic.Entry> {
25+
for (path in listOf(
26+
Path.fromString("/system/usr/share/zoneinfo/tzdata"), // immutable fallback tzdb
27+
Path.fromString("/apex/com.android.tzdata/etc/tz/tzdata"), // an up-to-date tzdb, may not exist
28+
)) {
29+
if (path.check() == null) continue // the file does not exist
30+
// 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()
32+
val header = BionicTzdbHeader.parse(content)
33+
val indexSize = header.data_offset - header.index_offset
34+
check(indexSize % 52 == 0) { "Invalid index size: $indexSize (must be a multiple of 52)" }
35+
val reader = BinaryDataReader(content, header.index_offset)
36+
repeat(indexSize / 52) {
37+
val name = reader.readNullTerminatedUtf8String(40)
38+
val start = reader.readInt()
39+
val length = reader.readInt()
40+
reader.readInt() // unused
41+
// intentionally overwrite the older entries
42+
put(name, TzdbBionic.Entry(content, header.data_offset + start, length))
43+
}
44+
}
45+
})
46+
47+
// bionic_tzdata_header_t
48+
private class BionicTzdbHeader(
49+
val version: String,
50+
val index_offset: Int,
51+
val data_offset: Int,
52+
val final_offset: Int,
53+
) {
54+
override fun toString(): String =
55+
"BionicTzdbHeader(version='$version', index_offset=$index_offset, " +
56+
"data_offset=$data_offset, final_offset=$final_offset)"
57+
58+
companion object {
59+
fun parse(content: ByteArray): BionicTzdbHeader =
60+
with(BinaryDataReader(content)) {
61+
BionicTzdbHeader(
62+
version = readNullTerminatedUtf8String(12),
63+
index_offset = readInt(),
64+
data_offset = readInt(),
65+
final_offset = readInt(),
66+
)
67+
}.apply {
68+
check(version.startsWith("tzdata") && version.length < 12) { "Unknown tzdata version: $version" }
69+
check(index_offset <= data_offset) { "Invalid data and index offsets: $data_offset and $index_offset" }
70+
}
71+
}
72+
}

core/build.gradle.kts

+41-39
Original file line numberDiff line numberDiff line change
@@ -37,47 +37,48 @@ kotlin {
3737
explicitApi()
3838

3939
infra {
40-
common("nix") {
40+
common("tzfile") {
4141
// Tiers are in accordance with <https://kotlinlang.org/docs/native-target-support.html>
42-
common("linux") {
43-
// Tier 1
44-
target("linuxX64")
45-
// Tier 2
46-
target("linuxArm64")
47-
// Tier 4 (deprecated, but still in demand)
48-
target("linuxArm32Hfp")
49-
}
50-
// the following targets are not supported, as we don't have timezone database implementations for them:
51-
/*
52-
target("androidNativeArm32")
53-
target("androidNativeArm64")
54-
target("androidNativeX86")
55-
target("androidNativeX64")
56-
*/
57-
common("darwin") {
58-
common("darwinDevices") {
42+
common("tzdbOnFilesystem") {
43+
common("linux") {
5944
// Tier 1
60-
target("macosX64")
61-
target("macosArm64")
45+
target("linuxX64")
6246
// Tier 2
63-
target("watchosX64")
64-
target("watchosArm32")
65-
target("watchosArm64")
66-
target("tvosX64")
67-
target("tvosArm64")
68-
target("iosArm64")
69-
// Tier 3
70-
target("watchosDeviceArm64")
47+
target("linuxArm64")
48+
// Tier 4 (deprecated, but still in demand)
49+
target("linuxArm32Hfp")
7150
}
72-
common("darwinSimulator") {
73-
// Tier 1
74-
target("iosSimulatorArm64")
75-
target("iosX64")
76-
// Tier 2
77-
target("watchosSimulatorArm64")
78-
target("tvosSimulatorArm64")
51+
common("darwin") {
52+
common("darwinDevices") {
53+
// Tier 1
54+
target("macosX64")
55+
target("macosArm64")
56+
// Tier 2
57+
target("watchosX64")
58+
target("watchosArm32")
59+
target("watchosArm64")
60+
target("tvosX64")
61+
target("tvosArm64")
62+
target("iosArm64")
63+
// Tier 3
64+
target("watchosDeviceArm64")
65+
}
66+
common("darwinSimulator") {
67+
// Tier 1
68+
target("iosSimulatorArm64")
69+
target("iosX64")
70+
// Tier 2
71+
target("watchosSimulatorArm64")
72+
target("tvosSimulatorArm64")
73+
}
7974
}
8075
}
76+
common("androidNative") {
77+
target("androidNativeArm32")
78+
target("androidNativeArm64")
79+
target("androidNativeX86")
80+
target("androidNativeX64")
81+
}
8182
}
8283
// Tier 3
8384
common("windows") {
@@ -157,10 +158,11 @@ kotlin {
157158
}
158159
}
159160
}
160-
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> {
161-
// do nothing special
162-
}
163-
konanTarget.family.isAppleFamily -> {
161+
162+
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX ||
163+
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.ANDROID ||
164+
konanTarget.family.isAppleFamily ->
165+
{
164166
// do nothing special
165167
}
166168
else -> {

core/common/src/internal/BinaryDataReader.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,17 @@ internal class BinaryDataReader(private val bytes: ByteArray, private var positi
4242
(bytes[position + 6].toLong() and 0xFF shl 8) or
4343
(bytes[position + 7].toLong() and 0xFF).also { position += 8 }
4444

45-
fun readUtf8String(length: Int) =
46-
bytes.decodeToString(position, position + length).also { position += length }
45+
fun readUtf8String(exactLength: Int) =
46+
bytes.decodeToString(position, position + exactLength).also { position += exactLength }
47+
48+
fun readNullTerminatedUtf8String(fieldSize: Int): String {
49+
var exactLength = 0
50+
while (position + exactLength < bytes.size && bytes[position + exactLength] != 0.toByte() && exactLength < fieldSize) {
51+
++exactLength
52+
}
53+
return bytes.decodeToString(position, position + exactLength)
54+
.also { position += fieldSize }
55+
}
4756

4857
fun readAsciiChar(): Char = readByte().toInt().toChar()
4958

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
@file:OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
7+
package kotlinx.datetime.internal
8+
9+
import kotlinx.cinterop.*
10+
import platform.posix.*
11+
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
20+
}
21+
22+
internal fun Path.traverseDirectory(exclude: Set<String> = emptySet(), stripLeadingComponents: Int = this.components.size, actionOnFile: (Path) -> Unit) {
23+
val handler = opendir(this.toString()) ?: return
24+
try {
25+
while (true) {
26+
val entry = readdir(handler) ?: break
27+
val name = entry.pointed.d_name.toKString()
28+
if (name == "." || name == "..") continue
29+
if (name in exclude) continue
30+
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 {
37+
actionOnFile(Path(false, path.components.drop(stripLeadingComponents)))
38+
}
39+
}
40+
} finally {
41+
closedir(handler)
42+
}
43+
}

core/nix/test/TimeZoneRulesCompleteTest.kt renamed to core/tzdbOnFilesystem/test/TimeZoneRulesCompleteTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private inline fun runUnixCommand(command: String): Sequence<String> = sequence
6767
// read line by line
6868
while (true) {
6969
val linePtr = alloc<CPointerVar<ByteVar>>()
70-
val nPtr = alloc<ULongVar>()
70+
val nPtr = alloc<size_tVar>()
7171
try {
7272
val result = getline(linePtr.ptr, nPtr.ptr, pipe)
7373
if (result != (-1).convert<ssize_t>()) {
File renamed without changes.

core/nix/src/internal/filesystem.kt renamed to core/tzfile/src/internal/filesystem.kt

+1-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
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

@@ -53,45 +53,12 @@ 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-
6656
// `stat(2)` lists the other available fields
6757
internal interface PathInfo {
6858
val isDirectory: Boolean
6959
val isSymlink: Boolean
7060
}
7161

72-
internal fun Path.traverseDirectory(exclude: Set<String> = emptySet(), stripLeadingComponents: Int = this.components.size, actionOnFile: (Path) -> Unit) {
73-
val handler = opendir(this.toString()) ?: return
74-
try {
75-
while (true) {
76-
val entry = readdir(handler) ?: break
77-
val name = entry.pointed.d_name.toKString()
78-
if (name == "." || name == "..") continue
79-
if (name in exclude) continue
80-
val path = Path(isAbsolute, components + name)
81-
val info = path.check() ?: continue // skip broken symlinks
82-
if (info.isDirectory) {
83-
if (!info.isSymlink) {
84-
path.traverseDirectory(exclude, stripLeadingComponents, actionOnFile)
85-
}
86-
} else {
87-
actionOnFile(Path(false, path.components.drop(stripLeadingComponents)))
88-
}
89-
}
90-
} finally {
91-
closedir(handler)
92-
}
93-
}
94-
9562
internal fun Path.readBytes(): ByteArray {
9663
val handler = fopen(this.toString(), "rb") ?: throw RuntimeException("Cannot open file $this")
9764
try {
File renamed without changes.

license/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,8 @@ may apply:
2727
https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml
2828
- License: Unicode ([license/thirdparty/unicode_license.txt](thirdparty/unicode_license.txt))
2929

30+
- Path: `core/androidNative/src`
31+
- Origin: implementation is based on the bionic project.
32+
- License: BSD ([license/thirdparty/bionic_license.txt](thirdparty/bionic_license.txt))
3033

3134
[threetenbp]: thirdparty/threetenbp_license.txt

license/thirdparty/bionic_license.txt

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright (C) 2017 The Android Open Source Project
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions
6+
are met:
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in
11+
the documentation and/or other materials provided with the
12+
distribution.
13+
14+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
17+
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
18+
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
19+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
20+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
21+
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
22+
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
24+
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25+
SUCH DAMAGE.

0 commit comments

Comments
 (0)