Skip to content

Commit

Permalink
Implement kotlinx-datetime for the Android native targets (#344)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dkhalanskyjb authored Mar 1, 2024
1 parent ab0a40d commit 290a666
Show file tree
Hide file tree
Showing 13 changed files with 224 additions and 76 deletions.
27 changes: 27 additions & 0 deletions core/androidNative/src/internal/TimeZoneNative.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2019-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

@file:OptIn(ExperimentalForeignApi::class)
package kotlinx.datetime.internal

import kotlinx.cinterop.*
import platform.posix.*

internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()

private val tzdb = runCatching { TzdbBionic() }

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> = memScoped {
val name = readSystemProperty("persist.sys.timezone")
?: throw IllegalStateException("The system property 'persist.sys.timezone' should contain the system timezone")
return name to null
}

private fun readSystemProperty(name: String): String? = memScoped {
// see https://android.googlesource.com/platform/bionic/+/froyo/libc/include/sys/system_properties.h
val result = allocArray<ByteVar>(92)
val error = __system_property_get(name, result)
if (error == 0) null else result.toKString()
}
72 changes: 72 additions & 0 deletions core/androidNative/src/internal/TzdbBionic.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2019-2024 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.
*/
/*
* Based on the bionic project.
* Copyright (C) 2017 The Android Open Source Project
*/

package kotlinx.datetime.internal

private class TzdbBionic(private val rules: Map<String, Entry>) : TimeZoneDatabase {
override fun rulesForId(id: String): TimeZoneRules =
rules[id]?.readRules() ?: throw IllegalStateException("Unknown time zone $id")

override fun availableTimeZoneIds(): Set<String> = rules.keys

class Entry(val file: ByteArray, val offset: Int, val length: Int) {
fun readRules(): TimeZoneRules = readTzFile(file.copyOfRange(offset, offset + length)).toTimeZoneRules()
}
}

// see https://android.googlesource.com/platform/bionic/+/master/libc/tzcode/bionic.cpp for the format
internal fun TzdbBionic(): TimeZoneDatabase = TzdbBionic(buildMap<String, TzdbBionic.Entry> {
for (path in listOf(
Path.fromString("/system/usr/share/zoneinfo/tzdata"), // immutable fallback tzdb
Path.fromString("/apex/com.android.tzdata/etc/tz/tzdata"), // an up-to-date tzdb, may not exist
)) {
if (path.check() == null) continue // the file does not exist
// be careful to only read each file a single time and keep many references to the same ByteArray in memory.
val content = path.readBytes()
val header = BionicTzdbHeader.parse(content)
val indexSize = header.data_offset - header.index_offset
check(indexSize % 52 == 0) { "Invalid index size: $indexSize (must be a multiple of 52)" }
val reader = BinaryDataReader(content, header.index_offset)
repeat(indexSize / 52) {
val name = reader.readNullTerminatedUtf8String(40)
val start = reader.readInt()
val length = reader.readInt()
reader.readInt() // unused
// intentionally overwrite the older entries
put(name, TzdbBionic.Entry(content, header.data_offset + start, length))
}
}
})

// bionic_tzdata_header_t
private class BionicTzdbHeader(
val version: String,
val index_offset: Int,
val data_offset: Int,
val final_offset: Int,
) {
override fun toString(): String =
"BionicTzdbHeader(version='$version', index_offset=$index_offset, " +
"data_offset=$data_offset, final_offset=$final_offset)"

companion object {
fun parse(content: ByteArray): BionicTzdbHeader =
with(BinaryDataReader(content)) {
BionicTzdbHeader(
version = readNullTerminatedUtf8String(12),
index_offset = readInt(),
data_offset = readInt(),
final_offset = readInt(),
)
}.apply {
check(version.startsWith("tzdata") && version.length < 12) { "Unknown tzdata version: $version" }
check(index_offset <= data_offset) { "Invalid data and index offsets: $data_offset and $index_offset" }
}
}
}
80 changes: 41 additions & 39 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,47 +37,48 @@ kotlin {
explicitApi()

infra {
common("nix") {
common("tzfile") {
// Tiers are in accordance with <https://kotlinlang.org/docs/native-target-support.html>
common("linux") {
// Tier 1
target("linuxX64")
// Tier 2
target("linuxArm64")
// Tier 4 (deprecated, but still in demand)
target("linuxArm32Hfp")
}
// the following targets are not supported, as we don't have timezone database implementations for them:
/*
target("androidNativeArm32")
target("androidNativeArm64")
target("androidNativeX86")
target("androidNativeX64")
*/
common("darwin") {
common("darwinDevices") {
common("tzdbOnFilesystem") {
common("linux") {
// Tier 1
target("macosX64")
target("macosArm64")
target("linuxX64")
// Tier 2
target("watchosX64")
target("watchosArm32")
target("watchosArm64")
target("tvosX64")
target("tvosArm64")
target("iosArm64")
// Tier 3
target("watchosDeviceArm64")
target("linuxArm64")
// Tier 4 (deprecated, but still in demand)
target("linuxArm32Hfp")
}
common("darwinSimulator") {
// Tier 1
target("iosSimulatorArm64")
target("iosX64")
// Tier 2
target("watchosSimulatorArm64")
target("tvosSimulatorArm64")
common("darwin") {
common("darwinDevices") {
// Tier 1
target("macosX64")
target("macosArm64")
// Tier 2
target("watchosX64")
target("watchosArm32")
target("watchosArm64")
target("tvosX64")
target("tvosArm64")
target("iosArm64")
// Tier 3
target("watchosDeviceArm64")
}
common("darwinSimulator") {
// Tier 1
target("iosSimulatorArm64")
target("iosX64")
// Tier 2
target("watchosSimulatorArm64")
target("tvosSimulatorArm64")
}
}
}
common("androidNative") {
target("androidNativeArm32")
target("androidNativeArm64")
target("androidNativeX86")
target("androidNativeX64")
}
}
// Tier 3
common("windows") {
Expand Down Expand Up @@ -157,10 +158,11 @@ kotlin {
}
}
}
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> {
// do nothing special
}
konanTarget.family.isAppleFamily -> {

konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX ||
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.ANDROID ||
konanTarget.family.isAppleFamily ->
{
// do nothing special
}
else -> {
Expand Down
13 changes: 11 additions & 2 deletions core/common/src/internal/BinaryDataReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,17 @@ internal class BinaryDataReader(private val bytes: ByteArray, private var positi
(bytes[position + 6].toLong() and 0xFF shl 8) or
(bytes[position + 7].toLong() and 0xFF).also { position += 8 }

fun readUtf8String(length: Int) =
bytes.decodeToString(position, position + length).also { position += length }
fun readUtf8String(exactLength: Int) =
bytes.decodeToString(position, position + exactLength).also { position += exactLength }

fun readNullTerminatedUtf8String(fieldSize: Int): String {
var exactLength = 0
while (position + exactLength < bytes.size && bytes[position + exactLength] != 0.toByte() && exactLength < fieldSize) {
++exactLength
}
return bytes.decodeToString(position, position + exactLength)
.also { position += fieldSize }
}

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

Expand Down
File renamed without changes.
43 changes: 43 additions & 0 deletions core/tzdbOnFilesystem/src/internal/filesystem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2019-2023 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.
*/

@file:OptIn(ExperimentalForeignApi::class, UnsafeNumber::class)
package kotlinx.datetime.internal

import kotlinx.cinterop.*
import platform.posix.*

internal fun Path.chaseSymlinks(maxDepth: Int = 100): Path {
var realPath = this
var depth = maxDepth
while (true) {
realPath = realPath.readLink() ?: break
if (depth-- == 0) throw RuntimeException("Too many levels of symbolic links")
}
return realPath
}

internal fun Path.traverseDirectory(exclude: Set<String> = emptySet(), stripLeadingComponents: Int = this.components.size, actionOnFile: (Path) -> Unit) {
val handler = opendir(this.toString()) ?: return
try {
while (true) {
val entry = readdir(handler) ?: break
val name = entry.pointed.d_name.toKString()
if (name == "." || name == "..") continue
if (name in exclude) continue
val path = Path(isAbsolute, components + name)
val info = path.check() ?: continue // skip broken symlinks
if (info.isDirectory) {
if (!info.isSymlink) {
path.traverseDirectory(exclude, stripLeadingComponents, actionOnFile)
}
} else {
actionOnFile(Path(false, path.components.drop(stripLeadingComponents)))
}
}
} finally {
closedir(handler)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ private inline fun runUnixCommand(command: String): Sequence<String> = sequence
// read line by line
while (true) {
val linePtr = alloc<CPointerVar<ByteVar>>()
val nPtr = alloc<ULongVar>()
val nPtr = alloc<size_tVar>()
try {
val result = getline(linePtr.ptr, nPtr.ptr, pipe)
if (result != (-1).convert<ssize_t>()) {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
* Copyright 2019-2024 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.
*/

Expand Down Expand Up @@ -53,45 +53,12 @@ internal class Path(val isAbsolute: Boolean, val components: List<String>) {
}
}

internal fun Path.chaseSymlinks(maxDepth: Int = 100): Path {
var realPath = this
var depth = maxDepth
while (true) {
realPath = realPath.readLink() ?: break
if (depth-- == 0) throw RuntimeException("Too many levels of symbolic links")
}
return realPath
}

// `stat(2)` lists the other available fields
internal interface PathInfo {
val isDirectory: Boolean
val isSymlink: Boolean
}

internal fun Path.traverseDirectory(exclude: Set<String> = emptySet(), stripLeadingComponents: Int = this.components.size, actionOnFile: (Path) -> Unit) {
val handler = opendir(this.toString()) ?: return
try {
while (true) {
val entry = readdir(handler) ?: break
val name = entry.pointed.d_name.toKString()
if (name == "." || name == "..") continue
if (name in exclude) continue
val path = Path(isAbsolute, components + name)
val info = path.check() ?: continue // skip broken symlinks
if (info.isDirectory) {
if (!info.isSymlink) {
path.traverseDirectory(exclude, stripLeadingComponents, actionOnFile)
}
} else {
actionOnFile(Path(false, path.components.drop(stripLeadingComponents)))
}
}
} finally {
closedir(handler)
}
}

internal fun Path.readBytes(): ByteArray {
val handler = fopen(this.toString(), "rb") ?: throw RuntimeException("Cannot open file $this")
try {
Expand Down
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions license/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ may apply:
https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml
- License: Unicode ([license/thirdparty/unicode_license.txt](thirdparty/unicode_license.txt))

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

[threetenbp]: thirdparty/threetenbp_license.txt
25 changes: 25 additions & 0 deletions license/thirdparty/bionic_license.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright (C) 2017 The Android Open Source Project
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.

0 comments on commit 290a666

Please sign in to comment.