use std::{
ffi::CStr,
fmt::Debug,
fs::File,
io::{Error, ErrorKind, Read, Result, Seek, SeekFrom},
};
#[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())
}
#[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())
}
#[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))
}
#[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
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TzDataHeader {
version: [u8; 5],
index_offset: u32,
data_offset: u32,
zonetab_offset: u32,
}
impl TzDataHeader {
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 })
}
}
struct TzDataIndexes {
indexes: Vec<TzDataIndex>,
}
impl TzDataIndexes {
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)?;
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(),
})
}
fn find_timezone(&self, timezone: &[u8]) -> Option<&TzDataIndex> {
self.indexes.binary_search_by_key(&timezone, |x| &x.name).map(|x| &self.indexes[x]).ok()
}
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)
}
}
struct TzDataIndex {
name: Box<[u8]>,
offset: u32,
length: u32,
}
fn from_bytes_until_nul(bytes: &[u8]) -> Option<&CStr> {
let nul_pos = bytes.iter().position(|&b| b == 0)?;
Some(unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=nul_pos]) })
}
#[cfg(any(test, target_env = "ohos"))]
const OHOS_ENTRY_LEN: usize = TZ_NAME_LEN + 2 * size_of::<u32>();
#[cfg(any(test, target_os = "android"))]
const ANDROID_ENTRY_LEN: usize = TZ_NAME_LEN + 3 * size_of::<u32>();
const TZ_NAME_LEN: usize = 40;
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());
}
}