Fix OpenHarmony's local timezone fetching and tzdata parsing

This commit is contained in:
ldm0 2025-07-08 22:17:34 +08:00 committed by Dirkjan Ochtman
parent 9245a8f38f
commit 23c132fc4f
7 changed files with 277 additions and 70 deletions

View File

@ -23,7 +23,7 @@ alloc = []
libc = [] libc = []
winapi = ["windows-link"] winapi = ["windows-link"]
std = ["alloc"] std = ["alloc"]
clock = ["winapi", "iana-time-zone", "android-tzdata", "now"] clock = ["winapi", "iana-time-zone", "now"]
now = ["std"] now = ["std"]
oldtime = [] oldtime = []
wasmbind = ["wasm-bindgen", "js-sys"] wasmbind = ["wasm-bindgen", "js-sys"]
@ -57,9 +57,6 @@ windows-bindgen = { version = "0.62" } # MSRV is 1.74
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
iana-time-zone = { version = "0.1.45", optional = true, features = ["fallback"] } iana-time-zone = { version = "0.1.45", optional = true, features = ["fallback"] }
[target.'cfg(target_os = "android")'.dependencies]
android-tzdata = { version = "0.1.1", optional = true }
[dev-dependencies] [dev-dependencies]
serde_json = { version = "1" } serde_json = { version = "1" }
serde_derive = { version = "1", default-features = false } serde_derive = { version = "1", default-features = false }

View File

@ -28,6 +28,9 @@ mod inner;
#[allow(unreachable_pub)] #[allow(unreachable_pub)]
mod win_bindings; mod win_bindings;
#[cfg(all(any(target_os = "android", target_env = "ohos", test), feature = "clock"))]
mod tz_data;
#[cfg(all( #[cfg(all(
not(unix), not(unix),
not(windows), not(windows),

267
src/offset/local/tz_data.rs Normal file
View File

@ -0,0 +1,267 @@
//! Rust parser of ZoneInfoDb(`tzdata`) on Android and OpenHarmony
//!
//! Ported from: https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java
use std::{
ffi::CStr,
fmt::Debug,
fs::File,
io::{Error, ErrorKind, Read, Result, Seek, SeekFrom},
};
/// Get timezone data from the `tzdata` file of HarmonyOS NEXT.
#[cfg(target_env = "ohos")]
pub(crate) fn for_zone(tz_string: &str) -> Result<Option<Vec<u8>>> {
let mut file = File::open("/system/etc/zoneinfo/tzdata")?;
find_tz_data::<OHOS_ENTRY_LEN>(&mut file, tz_string.as_bytes())
}
/// Get timezone data from the `tzdata` file of Android.
#[cfg(target_os = "android")]
pub(crate) fn for_zone(tz_string: &str) -> Result<Option<Vec<u8>>> {
let mut file = open_android_tz_data_file()?;
find_tz_data::<ANDROID_ENTRY_LEN>(&mut file, tz_string.as_bytes())
}
/// Open the `tzdata` file of Android from the environment variables.
#[cfg(target_os = "android")]
fn open_android_tz_data_file() -> Result<File> {
for (env_var, path) in
[("ANDROID_DATA", "/misc/zoneinfo"), ("ANDROID_ROOT", "/usr/share/zoneinfo")]
{
if let Ok(env_value) = std::env::var(env_var) {
if let Ok(file) = File::open(format!("{}{}/tzdata", env_value, path)) {
return Ok(file);
}
}
}
Err(Error::from(ErrorKind::NotFound))
}
/// Get timezone data from the `tzdata` file reader
#[cfg(any(test, target_env = "ohos", target_os = "android"))]
fn find_tz_data<const ENTRY_LEN: usize>(
mut reader: impl Read + Seek,
tz_name: &[u8],
) -> Result<Option<Vec<u8>>> {
let header = TzDataHeader::new(&mut reader)?;
let index = TzDataIndexes::new::<ENTRY_LEN>(&mut reader, &header)?;
Ok(if let Some(entry) = index.find_timezone(tz_name) {
Some(index.find_tzdata(reader, &header, entry)?)
} else {
None
})
}
/// Header of the `tzdata` file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TzDataHeader {
version: [u8; 5],
index_offset: u32,
data_offset: u32,
zonetab_offset: u32,
}
impl TzDataHeader {
/// Parse the header of the `tzdata` file.
fn new(mut data: impl Read) -> Result<Self> {
let version = {
let mut magic = [0; TZDATA_VERSION_LEN];
data.read_exact(&mut magic)?;
if !magic.starts_with(b"tzdata") || magic[TZDATA_VERSION_LEN - 1] != 0 {
return Err(Error::new(ErrorKind::Other, "invalid tzdata header magic"));
}
let mut version = [0; 5];
version.copy_from_slice(&magic[6..11]);
version
};
let mut offset = [0; 4];
data.read_exact(&mut offset)?;
let index_offset = u32::from_be_bytes(offset);
data.read_exact(&mut offset)?;
let data_offset = u32::from_be_bytes(offset);
data.read_exact(&mut offset)?;
let zonetab_offset = u32::from_be_bytes(offset);
Ok(Self { version, index_offset, data_offset, zonetab_offset })
}
}
/// Indexes of the `tzdata` file.
struct TzDataIndexes {
indexes: Vec<TzDataIndex>,
}
impl TzDataIndexes {
/// Create a new `TzDataIndexes` from the `tzdata` file reader.
fn new<const ENTRY_LEN: usize>(mut reader: impl Read, header: &TzDataHeader) -> Result<Self> {
let mut buf = vec![0; header.data_offset.saturating_sub(header.index_offset) as usize];
reader.read_exact(&mut buf)?;
// replace chunks with array_chunks when it's stable
Ok(TzDataIndexes {
indexes: buf
.chunks(ENTRY_LEN)
.filter_map(|chunk| {
from_bytes_until_nul(&chunk[..TZ_NAME_LEN]).map(|name| {
let name = name.to_bytes().to_vec().into_boxed_slice();
let offset = u32::from_be_bytes(
chunk[TZ_NAME_LEN..TZ_NAME_LEN + 4].try_into().unwrap(),
);
let length = u32::from_be_bytes(
chunk[TZ_NAME_LEN + 4..TZ_NAME_LEN + 8].try_into().unwrap(),
);
TzDataIndex { name, offset, length }
})
})
.collect(),
})
}
/// Find a timezone by name.
fn find_timezone(&self, timezone: &[u8]) -> Option<&TzDataIndex> {
// timezones in tzdata are sorted by name.
self.indexes.binary_search_by_key(&timezone, |x| &x.name).map(|x| &self.indexes[x]).ok()
}
/// Retrieve a chunk of timezone data by the index.
fn find_tzdata(
&self,
mut reader: impl Read + Seek,
header: &TzDataHeader,
index: &TzDataIndex,
) -> Result<Vec<u8>> {
reader.seek(SeekFrom::Start(index.offset as u64 + header.data_offset as u64))?;
let mut buffer = vec![0; index.length as usize];
reader.read_exact(&mut buffer)?;
Ok(buffer)
}
}
/// Index entry of the `tzdata` file.
struct TzDataIndex {
name: Box<[u8]>,
offset: u32,
length: u32,
}
/// TODO: Change this `CStr::from_bytes_until_nul` once MSRV was bumped above 1.72.0
fn from_bytes_until_nul(bytes: &[u8]) -> Option<&CStr> {
let nul_pos = bytes.iter().position(|&b| b == 0)?;
// SAFETY:
// 1. nul_pos + 1 <= bytes.len()
// 2. We know there is a nul byte at nul_pos, so this slice (ending at the nul byte) is a well-formed C string.
Some(unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=nul_pos]) })
}
/// Ohos tzdata index entry size: `name + offset + length`
#[cfg(any(test, target_env = "ohos"))]
const OHOS_ENTRY_LEN: usize = TZ_NAME_LEN + 2 * size_of::<u32>();
/// Android tzdata index entry size: `name + offset + length + raw_utc_offset(legacy)`:
/// [reference](https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java#271)
#[cfg(any(test, target_os = "android"))]
const ANDROID_ENTRY_LEN: usize = TZ_NAME_LEN + 3 * size_of::<u32>();
/// The database reserves 40 bytes for each id.
const TZ_NAME_LEN: usize = 40;
/// Size of the version string in the header of `tzdata` file.
/// e.g. `tzdata2024b\0`
const TZDATA_VERSION_LEN: usize = 12;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ohos_tzdata_header_and_index() {
let file = File::open("./tests/ohos/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
assert_eq!(header.version, *b"2024a");
assert_eq!(header.index_offset, 24);
assert_eq!(header.data_offset, 21240);
assert_eq!(header.zonetab_offset, 272428);
let iter = TzDataIndexes::new::<OHOS_ENTRY_LEN>(&file, &header).unwrap();
assert_eq!(iter.indexes.len(), 442);
assert!(iter.find_timezone(b"Asia/Shanghai").is_some());
assert!(iter.find_timezone(b"Pacific/Noumea").is_some());
}
#[test]
fn test_ohos_tzdata_loading() {
let file = File::open("./tests/ohos/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
let iter = TzDataIndexes::new::<OHOS_ENTRY_LEN>(&file, &header).unwrap();
let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap();
let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap();
assert_eq!(tzdata.len(), 393);
}
#[test]
fn test_invalid_tzdata_header() {
TzDataHeader::new(&b"tzdaaa2024aaaaaaaaaaaaaaa\0"[..]).unwrap_err();
}
#[test]
fn test_android_tzdata_header_and_index() {
let file = File::open("./tests/android/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
assert_eq!(header.version, *b"2021a");
assert_eq!(header.index_offset, 24);
assert_eq!(header.data_offset, 30860);
assert_eq!(header.zonetab_offset, 491837);
let iter = TzDataIndexes::new::<ANDROID_ENTRY_LEN>(&file, &header).unwrap();
assert_eq!(iter.indexes.len(), 593);
assert!(iter.find_timezone(b"Asia/Shanghai").is_some());
assert!(iter.find_timezone(b"Pacific/Noumea").is_some());
}
#[test]
fn test_android_tzdata_loading() {
let file = File::open("./tests/android/tzdata").unwrap();
let header = TzDataHeader::new(&file).unwrap();
let iter = TzDataIndexes::new::<ANDROID_ENTRY_LEN>(&file, &header).unwrap();
let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap();
let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap();
assert_eq!(tzdata.len(), 573);
}
#[test]
fn test_ohos_tzdata_find() {
let file = File::open("./tests/ohos/tzdata").unwrap();
let tzdata = find_tz_data::<OHOS_ENTRY_LEN>(file, b"Asia/Shanghai").unwrap().unwrap();
assert_eq!(tzdata.len(), 393);
}
#[test]
fn test_ohos_tzdata_find_missing() {
let file = File::open("./tests/ohos/tzdata").unwrap();
assert!(find_tz_data::<OHOS_ENTRY_LEN>(file, b"Asia/Sjasdfai").unwrap().is_none());
}
#[test]
fn test_android_tzdata_find() {
let file = File::open("./tests/android/tzdata").unwrap();
let tzdata = find_tz_data::<ANDROID_ENTRY_LEN>(file, b"Asia/Shanghai").unwrap().unwrap();
assert_eq!(tzdata.len(), 573);
}
#[test]
fn test_android_tzdata_find_missing() {
let file = File::open("./tests/android/tzdata").unwrap();
assert!(find_tz_data::<ANDROID_ENTRY_LEN>(file, b"Asia/S000000i").unwrap().is_none());
}
#[cfg(target_env = "ohos")]
#[test]
fn test_ohos_machine_tz_data_loading() {
let tzdata = for_zone(b"Asia/Shanghai").unwrap().unwrap();
assert!(!tzdata.is_empty());
}
#[cfg(target_os = "android")]
#[test]
fn test_android_machine_tz_data_loading() {
let tzdata = for_zone(b"Asia/Shanghai").unwrap().unwrap();
assert!(!tzdata.is_empty());
}
}

View File

@ -8,8 +8,6 @@ use std::{cmp::Ordering, fmt, str};
use super::rule::{AlternateTime, TransitionRule}; use super::rule::{AlternateTime, TransitionRule};
use super::{DAYS_PER_WEEK, Error, SECONDS_PER_DAY, parser}; use super::{DAYS_PER_WEEK, Error, SECONDS_PER_DAY, parser};
use crate::NaiveDateTime; use crate::NaiveDateTime;
#[cfg(target_env = "ohos")]
use crate::offset::local::tz_info::parser::Cursor;
/// Time zone /// Time zone
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
@ -47,19 +45,13 @@ impl TimeZone {
} }
// attributes are not allowed on if blocks in Rust 1.38 // attributes are not allowed on if blocks in Rust 1.38
#[cfg(target_os = "android")] #[cfg(any(target_os = "android", target_env = "ohos"))]
{ {
if let Ok(bytes) = android_tzdata::find_tz_data(tz_string) { if let Ok(Some(bytes)) = crate::offset::local::tz_data::for_zone(tz_string) {
return Self::from_tz_data(&bytes); return Self::from_tz_data(&bytes);
} }
} }
// ohos merge all file into tzdata since ver35
#[cfg(target_env = "ohos")]
{
return Self::from_tz_data(&find_ohos_tz_data(tz_string)?);
}
let mut chars = tz_string.chars(); let mut chars = tz_string.chars();
if chars.next() == Some(':') { if chars.next() == Some(':') {
return Self::from_file(&mut find_tz_file(chars.as_str())?); return Self::from_file(&mut find_tz_file(chars.as_str())?);
@ -636,58 +628,6 @@ fn find_tz_file(path: impl AsRef<Path>) -> Result<File, Error> {
} }
} }
#[cfg(target_env = "ohos")]
fn from_tzdata_bytes(bytes: &mut Vec<u8>, tz_string: &str) -> Result<Vec<u8>, Error> {
const VERSION_SIZE: usize = 12;
const OFFSET_SIZE: usize = 4;
const INDEX_CHUNK_SIZE: usize = 48;
const ZONENAME_SIZE: usize = 40;
let mut cursor = Cursor::new(&bytes);
// version head
let _ = cursor.read_exact(VERSION_SIZE)?;
let index_offset_offset = cursor.read_be_u32()?;
let data_offset_offset = cursor.read_be_u32()?;
// final offset
let _ = cursor.read_be_u32()?;
cursor.seek_after(index_offset_offset as usize)?;
let mut idx = index_offset_offset;
while idx < data_offset_offset {
let index_buf = cursor.read_exact(ZONENAME_SIZE)?;
let offset = cursor.read_be_u32()?;
let length = cursor.read_be_u32()?;
let zone_name = str::from_utf8(index_buf)?.trim_end_matches('\0');
if zone_name != tz_string {
idx += INDEX_CHUNK_SIZE as u32;
continue;
}
cursor.seek_after((data_offset_offset + offset) as usize)?;
return match cursor.read_exact(length as usize) {
Ok(result) => Ok(result.to_vec()),
Err(_err) => Err(Error::InvalidTzFile("invalid ohos tzdata chunk")),
};
}
Err(Error::InvalidTzString("cannot find tz string within tzdata"))
}
#[cfg(target_env = "ohos")]
fn from_tzdata_file(file: &mut File, tz_string: &str) -> Result<Vec<u8>, Error> {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
from_tzdata_bytes(&mut bytes, tz_string)
}
#[cfg(target_env = "ohos")]
fn find_ohos_tz_data(tz_string: &str) -> Result<Vec<u8>, Error> {
const TZDATA_PATH: &str = "/system/etc/zoneinfo/tzdata";
match File::open(TZDATA_PATH) {
Ok(mut file) => from_tzdata_file(&mut file, tz_string),
Err(err) => Err(err.into()),
}
}
// Possible system timezone directories // Possible system timezone directories
#[cfg(unix)] #[cfg(unix)]
const ZONE_INFO_DIRECTORIES: [&str; 4] = const ZONE_INFO_DIRECTORIES: [&str; 4] =

View File

@ -74,15 +74,15 @@ struct Cache {
#[cfg(target_os = "aix")] #[cfg(target_os = "aix")]
const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo"; const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo";
#[cfg(not(any(target_os = "android", target_os = "aix")))] #[cfg(not(any(target_os = "android", target_os = "aix", target_env = "ohos")))]
const TZDB_LOCATION: &str = "/usr/share/zoneinfo"; const TZDB_LOCATION: &str = "/usr/share/zoneinfo";
fn fallback_timezone() -> Option<TimeZone> { fn fallback_timezone() -> Option<TimeZone> {
let tz_name = iana_time_zone::get_timezone().ok()?; let tz_name = iana_time_zone::get_timezone().ok()?;
#[cfg(not(target_os = "android"))] #[cfg(not(any(target_os = "android", target_env = "ohos")))]
let bytes = fs::read(format!("{TZDB_LOCATION}/{tz_name}")).ok()?; let bytes = fs::read(format!("{TZDB_LOCATION}/{tz_name}")).ok()?;
#[cfg(target_os = "android")] #[cfg(any(target_os = "android", target_env = "ohos"))]
let bytes = android_tzdata::find_tz_data(&tz_name).ok()?; let bytes = crate::offset::local::tz_data::for_zone(&tz_name).ok()??;
TimeZone::from_tz_data(&bytes).ok() TimeZone::from_tz_data(&bytes).ok()
} }

BIN
tests/android/tzdata Normal file

Binary file not shown.

BIN
tests/ohos/tzdata Normal file

Binary file not shown.