/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
/**
* @file
*
* This file defines zone info reader.
*/
package std.time
import std.binary.*
import std.io.*
// -1: failed, (>= 0): file size
foreign func CJ_TIME_GetFileSize(path: CPointer<Byte>, pathLen: Int64): Int64
// -1: failed, (>= 0): number of read bytes
foreign func CJ_TIME_ReadAllBytesFromFile(path: CPointer<Byte>, pathLen: Int64, buf: CPointer<Byte>, bufLen: Int64): Int64
/**
* @throws Exception if the length of source is less than or equal to zero
* @throws InvalidDataException if fileSize is less than zero
* @throws InvalidDataException if readNum is not equal to fileSize
* @throws IllegalMemoryException if failed to call loadFromTZData(cjvm).
*/
func loadTimeZone(name: String, source: String): TimeZone {
return TimeZone.loadFromTZData(name, loadTZifData(name, source))
}
/**
* @throws Exception if the length of source is less than or equal to zero
* @throws InvalidDataException if fileSize is less than zero
* @throws InvalidDataException if readNum is not equal to fileSize
*/
func loadTZifData(name: String, source: String): Array<UInt8> {
let srcLen = source.size
if (srcLen <= 0) {
throw InvalidDataException("invalid timezone source")
}
let path = source + SLASH_CHAR + name
return loadTZifData(path)
}
func loadTZifData(path: String): Array<UInt8> {
readZoneInfoFile(path, false)
}
func readZoneInfoFile(path: String, keepTerminator: Bool): Array<UInt8> {
unsafe {
let cPath: CPointerHandle<UInt8> = acquireArrayRawData(path.rawData())
let pathLen: Int64 = path.size
let fileSize = CJ_TIME_GetFileSize(cPath.pointer, pathLen)
releaseArrayRawData(cPath)
if (fileSize < 0) {
throw InvalidDataException("An error occurred when accessing file `${path}`!")
}
let bufLen: Int64 = fileSize + 1
var arr: Array<UInt8> = Array<UInt8>(fileSize + 1, repeat: 0)
let cPath2: CPointerHandle<UInt8> = acquireArrayRawData(path.rawData())
var arrPtr: CPointerHandle<UInt8> = acquireArrayRawData(arr)
var readNum = CJ_TIME_ReadAllBytesFromFile(cPath2.pointer, pathLen, arrPtr.pointer, bufLen)
releaseArrayRawData(arrPtr)
releaseArrayRawData(cPath2)
if (readNum != fileSize) {
throw InvalidDataException("An error occurred when reading data from file `${path}`!")
}
if (keepTerminator) {
arr
} else {
arr.slice(0, fileSize)
}
}
}
func loadTimeZoneFromTzData(name: String, source: String): TimeZone {
return TimeZone.loadFromTZData(name, loadTZData(name, source))
}
func loadTZData(name: String, path: String): Array<UInt8> {
unsafe {
let arr = readZoneInfoFile(path, true)
var buffer = ByteBuffer(arr)
// do not need the first 12 bytes,so move the buffer's read position forward by 12 bytes to skip them
buffer.seek(Begin(12))
// next 4 bytes is the byte offset where the timezone index starts
var indexOffsetArr = Array<Byte>(4, repeat: 0)
// next 4 bytes is the byte offset where the actual, concatenated TZif data block begins
var dataOffsetArr = Array<Byte>(4, repeat: 0)
buffer.read(indexOffsetArr)
buffer.read(dataOffsetArr)
var indexOffset = Int32.readBigEndian(indexOffsetArr)
var dataOffset = Int32.readBigEndian(dataOffsetArr)
buffer.seek(Begin(Int64(indexOffset)))
while (buffer.position < Int64(dataOffset)) {
// Each timezone ID in the index is stored in a fixed-size 40-byte field
var tempArr = Array<Byte>(40, repeat: 0)
buffer.read(tempArr)
var currentId = String.fromUtf8(tempArr).trimEnd([r"\u{0000}"])
// next 4 bytes is the offset of this specific timezone's data relative to the start of the main data block
var zoneDataOffsetArr = Array<Byte>(4, repeat: 0)
// next 4 bytes is the length of this specific timezone's TZif data.
var zoneDataLengthArr = Array<Byte>(4, repeat: 0)
buffer.read(zoneDataOffsetArr)
buffer.read(zoneDataLengthArr)
// unused 4 bytes,skip
buffer.seek(Current(4))
var zoneDataOffset = Int32.readBigEndian(zoneDataOffsetArr)
var zoneDataLength = Int32.readBigEndian(zoneDataLengthArr)
if (currentId == name) {
var zoneData = Array<Byte>(Int64(zoneDataLength), repeat: 0)
arr.copyTo(zoneData, Int64(dataOffset + zoneDataOffset), 0, Int64(zoneDataLength))
return zoneData
}
}
throw InvalidDataException("An error occurred when accessing file `${path}`!")
}
}
/**
* Parse time zone information formated (TZif) data.
*
* @param data TZif data formated with RFC8536.
* @return a List containing a number of LocalTime, a List containing a number of TransTime and extend zone name.
*
* @throws InvalidDataException
*
* @since 0.18.4
*/
func parseTZinfoData(data: Array<UInt8>): (Array<LocalTime>, Array<TransTime>, String) {
var arrReader = ArrReader(data)
/*
* Now we start to read and parse header of TZif data.
* A TZif header is structured as follows (the lengths of multi-octet fields are shown in parentheses):
* +---------------+---+
* | magic (4) |ver|
* +---------------+---+---------------------------------------+
* | [unused - reserved for future use] (15) |
* +---------------+---------------+---------------+-----------+
* | isutcnt (4) | isstdcnt (4) | leapcnt (4) |
* +---------------+---------------+---------------+
* | timecnt (4) | typecnt (4) | charcnt (4) |
* +---------------+---------------+---------------+
*/
let (isUTCnt, isStdCnt, leapCnt, timeCnt, typeCnt, charCnt) = (0, 1, 2, 3, 4, 5)
var (version, fields) = parseHeader(arrReader)
/*
* If the version of TZif data is 2 or 3, we can only discard the data block of Version 1.
* In the version 1 data block, time values are 32 bits (TIME_SIZE = 4 octets).
* The data block is structured as follows (the lengths of multi-octet fields are shown in parentheses):
* +---------------------------------------------------------+
* | transition times (timecnt x TIME_SIZE) |
* +---------------------------------------------------------+
* | transition types (timecnt) |
* +---------------------------------------------------------+
* | local time type records (typecnt x 6) |
* +---------------------------------------------------------+
* | time zone designations (charcnt) |
* +---------------------------------------------------------+
* | leap-second records (leapcnt x (TIME_SIZE + 4)) |
* +---------------------------------------------------------+
* | standard/wall indicators (isstdcnt) |
* +---------------------------------------------------------+
* | UT/local indicators (isutcnt) |
* +---------------------------------------------------------+
*/
var timeSize = 4
if (version > 1) {
arrReader.skip(
fields[timeCnt] * timeSize + fields[timeCnt] + fields[typeCnt] * 6 + fields[charCnt] + fields[leapCnt] * (4 +
timeSize) + fields[isStdCnt] + fields[isUTCnt])
/*
* In the version 2+ data block, present only in version 2 and 3 files,
* time values are 64 bits (TIME_SIZE = 8 octets).
*/
timeSize = 8
/*
* Although the version 1 and 2+ headers have the same format, magic number, and version fields,
* their count fields may differ, because the version 1 data can be a subset of the version 2+ data.
* So we need to re-read the header to get the right information.
*/
(version, fields) = parseHeader(arrReader)
}
var transTimes = arrReader.getNext(fields[timeCnt] * timeSize)
var transTypes = arrReader.getNext(fields[timeCnt])
var typeRecords = arrReader.getNext(fields[typeCnt] * 6)
var zoneDes = arrReader.getNext(fields[charCnt])
arrReader.getNext(fields[leapCnt] * (timeSize + 4))
arrReader.getNext(fields[isStdCnt])
arrReader.getNext(fields[isUTCnt])
let zoneNum = fields[typeCnt]
var zones: Array<LocalTime> = parseZones(zoneNum, typeRecords, zoneDes)
if (zones.size <= 0) {
throw InvalidDataException("Failed to parse the timezone file.")
}
let timeNum = fields[timeCnt]
var TransTime: Array<TransTime> = parseZoneTrans(timeNum, zoneNum, transTimes, transTypes, timeSize)
/*
* The TZif footer is structured as follows (the lengths of multi-octet fields are shown in parentheses):
* +---+--------------------+---+
* | NL| TZ string (0...) |NL |
* +---+--------------------+---+
*/
var footer = ""
let last = arrReader.getLast()
let asciiCodeOfLn: UInt8 = b'\n'
if (version == 1) {
if (last.size == 0) {
return (zones, TransTime, footer)
}
throw InvalidDataException("Failed to parse the timezone file.")
}
if (last.size > 2 && last[0] == asciiCodeOfLn && last[last.size - 1] == asciiCodeOfLn) {
footer = itos(last[1..last.size - 1], false)
if (footer == "") {
throw InvalidDataException("Failed to parse the timezone file.")
}
}
return (zones, TransTime, footer)
}
func parseHeader(arrReader: ArrReader): (Int64, Array<Int64>) {
let magic = arrReader.getNext(4)
if (magic != [b'T', b'Z', b'i', b'f']) {
throw InvalidDataException("Failed to parse the timezone file.")
}
var version: Int64
match (arrReader.getNext(1)[0]) {
case b'\0' => version = 1
case b'2' => version = 2
case b'3' => version = 3
case _ => throw InvalidDataException("Failed to parse the timezone file.")
}
arrReader.getNext(15) /* Unused 15 bytes */
return (version, parseFields(arrReader))
}
/**
* Parses fields except magic and version in the header.
*
* @param reader a DataReader need to be parsed fields except magic and version in the header.
* @param fields an array used to storage fields that parsed by this function.
* @return true if no anything wrong occured, otherwise false.
*
* @throws InvalidDataException
*
* @since 0.19.3
*/
@OverflowWrapping
func parseFields(arrReader: ArrReader): Array<Int64> {
let fields = Array<Int64>(6, repeat: 0)
let fieldCnt = 6
for (i in 0..fieldCnt) {
fields[i] = Int64(Int32((UInt32(arrReader.arr[arrReader.index + 3]) | (UInt32(arrReader.arr[arrReader.index + 2]) <<
8) | (UInt32(arrReader.arr[arrReader.index + 1]) << 16) | (UInt32(arrReader.arr[arrReader.index]) << 24))))
arrReader.skip(4) // Jump 4 bytes of one UInt32
}
let (isUTCnt, isStdCnt, leapCnt, timeCnt, typeCnt, charCnt) = (0, 1, 2, 3, 4, 5)
let maxTransitions = 20000
let maxTypes = 256
let maxChars = 100000
if (fields[timeCnt] > maxTransitions) {
throw InvalidDataException("Failed to parse the timezone file: timeCnt exceeds maximum.")
}
if (fields[typeCnt] > maxTypes) {
throw InvalidDataException("Failed to parse the timezone file: typeCnt exceeds maximum.")
}
if (fields[charCnt] > maxChars) {
throw InvalidDataException("Failed to parse the timezone file: charCnt exceeds maximum.")
}
if (fields[leapCnt] > maxTransitions) {
throw InvalidDataException("Failed to parse the timezone file: leapCnt exceeds maximum.")
}
if (fields[isStdCnt] > maxTypes) {
throw InvalidDataException("Failed to parse the timezone file: isStdCnt exceeds maximum.")
}
if (fields[isUTCnt] > maxTypes) {
throw InvalidDataException("Failed to parse the timezone file: isUTCnt exceeds maximum.")
}
return fields
}
/**
* Parses zones of data block of TZif data. Each record has the following format (the lengths of multi-octet
* fields are shown in parentheses):
* +---------------+---+---+
* | utoff (4) |dst|idx|
* +---------------+---+---+
*
* @param zoneNum the number of records, the "typecnt" field in the header.
* @param typeRecords a series of six-octet records specifying a local time type, which stored in data block of TZif.
* @param zoneDes a series of octets constituting an array of NUL-terminated (0x00) time zone designation strings,
* which stored in data block of TZif.
* @return a Result containing an array of LocalTime parsed from @p typeRecords, or an Exception.
*
* @since 0.19.3
*/
@OverflowWrapping
func parseZones(zoneNum: Int64, typeRecords: Array<UInt8>, zoneDes: Array<UInt8>): Array<LocalTime> {
if (zoneNum <= 0) {
throw InvalidDataException("Failed to parse the timezone file.")
}
var zonedata = ArrReader(typeRecords)
var zone = Array<LocalTime>(zoneNum, {_ => LocalTime()})
let minUTOff = -89999
let maxUTOff = 93599
for (i in 0..zoneNum) {
let utoffArr = zonedata.getNext(4)
let utoff = Int32((UInt32(utoffArr[3])) | (UInt32(utoffArr[2]) << 8) | (UInt32(utoffArr[1]) << 16) | (UInt32(utoffArr[0]) <<
24))
if (Int64(utoff) < minUTOff || Int64(utoff) > maxUTOff) {
throw InvalidDataException("Failed to parse the timezone file.")
}
zone[i].offset = utoff
let dst = (zonedata.getNext(1))[0]
zone[i].isDST = dst != 0
let idx = (zonedata.getNext(1))[0]
if (Int64(idx) >= zoneDes.size) {
throw InvalidDataException("Failed to parse the timezone file.")
}
zone[i].des = itos(zoneDes[Int64(idx)..zoneDes.size], true)
}
return zone
}
/*
* The string returned must be an ASCII string,
* so we use the unsafe String.fromUtf8Unchecked method.
* @throws InvalidDataException if failed to parse timezone.
*/
func itos(arr: Array<UInt8>, canNul: Bool): String {
for (i in 0..arr.size) {
if (arr[i] == 0x00) {
if (!canNul) {
return ""
}
return unsafe { String.fromUtf8Unchecked(arr[..i]) }
}
if (!arr[i].isAsciiWhiteSpace() && !arr[i].isAsciiGraphic()) {
throw InvalidDataException(
"Failed to parse the timezone file, since ${arr[i]} is not a supported ASCII code.")
}
}
return unsafe { String.fromUtf8Unchecked(arr) }
}
/**
* Parses transition times of data block of TZif data.
*
* @param timeNum the number of records, the "timecnt" field in the header.
* @param zoneNum the number of records, the "typecnt" field in the header.
* @param transTimes a series of four- or eight-octet UNIX leap-time values sorted in strictly ascending order,
* which stored in data block of TZif.
* @param transTypes a series of one-octet unsigned integers specifying the type of local time of the corresponding
* transition time, which stored in data block of TZif.
* @param stdInd a series of one-octet values indicating whether the transition times associated with local time
* types were specified as standard time or wall-clock time, which stored in data block of TZif.
* @param utInd a series of one-octet values indicating whether the transition times associated with local time
* types were specified as UT or local time, which stored in data block of TZif.
* @param is64 a Bool indicates whether each record is four- or eight-byte.
* @return a Result containing an array of TransTime parsed from @p transTimes, or an Exception.
*
* @since 0.19.3
*/
@OverflowWrapping
func parseZoneTrans(
timeNum: Int64,
zoneNum: Int64,
transTimes: Array<UInt8>,
transTypes: Array<UInt8>,
timeSize: Int64
): Array<TransTime> {
var transArr = Array<TransTime>(timeNum, {_ => TransTime()})
let ttReader = ArrReader(transTimes)
if (timeNum == 0) {
return Array<TransTime>(1, repeat: TransTime(Int64.Min, 0))
}
var prevTrans: Int64 = Int64.Min
for (i in 0..timeNum) {
if (timeSize == 4) {
let when = ttReader.getNext(4)
transArr[i].trans = Int64(Int32((UInt32(when[3])) | (UInt32(when[2]) << 8) | (UInt32(when[1]) << 16) | (UInt32(when[0]) <<
24)))
} else if (timeSize == 8) {
let when = ttReader.getNext(8)
transArr[i].trans = Int64((UInt64(when[7])) | (UInt64(when[6]) << 8) | (UInt64(when[5]) << 16) | (UInt64(when[4]) <<
24) | (UInt64(when[3]) << 32) | (UInt64(when[2]) << 40) | (UInt64(when[1]) << 48) | (UInt64(when[0]) <<
56))
} else {
throw InvalidDataException("Failed to parse the timezone file.")
}
if (Int64(transTypes[i]) >= zoneNum) {
throw InvalidDataException("Failed to parse the timezone file: transition type index out of range.")
}
if (i > 0 && transArr[i].trans <= prevTrans) {
throw InvalidDataException(
"Failed to parse the timezone file: transition times must be strictly increasing.")
}
prevTrans = transArr[i].trans
transArr[i].index = transTypes[i]
}
return transArr
}
class ArrReader {
var arr: Array<UInt8>
var index: Int64 = 0
init(arr: Array<UInt8>) {
this.arr = arr
}
func skip(len: Int64): Unit {
index += len
if (index >= arr.size) {
throw InvalidDataException("Failed to parse the timezone file.")
}
}
func getNext(len: Int64): Array<UInt8> {
var subArr = Array<UInt8>()
try {
subArr = arr.slice(index, len)
index += len
} catch (_) {
throw InvalidDataException("Failed to parse the timezone file.")
}
return subArr
}
func getLast(): Array<UInt8> {
var subArr = Array<UInt8>()
try {
subArr = arr.slice(index, arr.size - index)
index = arr.size
} catch (_) {
throw InvalidDataException("Failed to parse the timezone file.")
}
return subArr
}
}