zoneinfo_db/
lib.rs

1//! Rust parser of ZoneInfoDb(`tzdata`) on Android and OpenHarmony
2//!
3//! Ported from: https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java
4use std::{
5    ffi::CStr,
6    fmt::Debug,
7    fs::File,
8    io::{Error, Read, Result, Seek, SeekFrom},
9};
10
11// The database reserves 40 bytes for each id.
12const SIZEOF_TZNAME: usize = 40;
13/// Ohos tzdata index entry size: `name + offset + length`
14const SIZEOF_INDEX_ENTRY_OHOS: usize = SIZEOF_TZNAME + 2 * size_of::<u32>();
15/// Android tzdata index entry size: `name + offset + length + raw_utc_offset(legacy)`:
16/// [reference](https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java#271)
17const SIZEOF_INDEX_ENTRY_ANDROID: usize = SIZEOF_TZNAME + 3 * size_of::<u32>();
18
19/// Header of the `tzdata` file.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct TzDataHeader {
22    pub version: [u8; 5],
23    pub index_offset: u32,
24    pub data_offset: u32,
25    pub zonetab_offset: u32,
26}
27
28impl TzDataHeader {
29    /// Parse the header of the `tzdata` file.
30    pub fn new<R: Read>(mut data: R) -> Result<Self> {
31        /// e.g. `tzdata2024b\0`
32        const TZDATA_VERSION_SIZE: usize = 12;
33        /// Magic header of `tzdata` file
34        const TZDATA_MAGIC_HEADER: &[u8] = b"tzdata";
35
36        let version = {
37            let mut magic = [0; TZDATA_VERSION_SIZE];
38            data.read_exact(&mut magic)?;
39            if !magic.starts_with(TZDATA_MAGIC_HEADER) || magic[TZDATA_VERSION_SIZE - 1] != 0 {
40                return Err(Error::other("invalid tzdata header magic"));
41            }
42            let mut version = [0; 5];
43            version.copy_from_slice(&magic[6..11]);
44            version
45        };
46
47        let mut offset = [0; 4];
48        data.read_exact(&mut offset)?;
49        let index_offset = u32::from_be_bytes(offset);
50        data.read_exact(&mut offset)?;
51        let data_offset = u32::from_be_bytes(offset);
52        data.read_exact(&mut offset)?;
53        let zonetab_offset = u32::from_be_bytes(offset);
54
55        Ok(Self { version, index_offset, data_offset, zonetab_offset })
56    }
57}
58
59/// Index entry of the `tzdata` file.
60pub struct TzDataIndex {
61    pub name: Box<[u8]>,
62    pub offset: u32,
63    pub length: u32,
64}
65
66impl Debug for TzDataIndex {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("TzDataIndex")
69            .field("name", &String::from_utf8_lossy(&self.name))
70            .field("offset", &self.offset)
71            .field("length", &self.length)
72            .finish()
73    }
74}
75
76/// Indexes of the `tzdata` file.
77pub struct TzDataIndexes {
78    indexes: Vec<TzDataIndex>,
79}
80
81impl TzDataIndexes {
82    /// Parse the indexes of the `tzdata` file of Android.
83    pub fn new_android<R: Read>(reader: R, header: &TzDataHeader) -> Result<Self> {
84        Self::new::<SIZEOF_INDEX_ENTRY_ANDROID, R>(reader, header)
85    }
86
87    /// Parse the indexes of the `tzdata` file of HarmonyOS NEXT.
88    pub fn new_ohos<R: Read>(reader: R, header: &TzDataHeader) -> Result<Self> {
89        Self::new::<SIZEOF_INDEX_ENTRY_OHOS, R>(reader, header)
90    }
91
92    fn new<const SIZEOF_INDEX_ENTRY: usize, R: Read>(
93        mut reader: R,
94        header: &TzDataHeader,
95    ) -> Result<Self> {
96        let mut buf = vec![0; header.data_offset.saturating_sub(header.index_offset) as usize];
97        reader.read_exact(&mut buf)?;
98        // replace chunks with array_chunks when it's stable
99        Ok(TzDataIndexes {
100            indexes: buf
101                .chunks(SIZEOF_INDEX_ENTRY)
102                .filter_map(|chunk| {
103                    if let Ok(name) = CStr::from_bytes_until_nul(&chunk[..SIZEOF_TZNAME]) {
104                        let name = name.to_bytes().to_vec().into_boxed_slice();
105                        let offset = u32::from_be_bytes(
106                            chunk[SIZEOF_TZNAME..SIZEOF_TZNAME + 4].try_into().unwrap(),
107                        );
108                        let length = u32::from_be_bytes(
109                            chunk[SIZEOF_TZNAME + 4..SIZEOF_TZNAME + 8].try_into().unwrap(),
110                        );
111                        Some(TzDataIndex { name, offset, length })
112                    } else {
113                        None
114                    }
115                })
116                .collect(),
117        })
118    }
119
120    /// Get all timezones.
121    pub fn timezones(&self) -> &[TzDataIndex] {
122        &self.indexes
123    }
124
125    /// Find a timezone by name.
126    pub fn find_timezone(&self, timezone: &[u8]) -> Option<&TzDataIndex> {
127        // timezones in tzdata are sorted by name.
128        self.indexes.binary_search_by_key(&timezone, |x| &x.name).map(|x| &self.indexes[x]).ok()
129    }
130
131    /// Retrieve a chunk of timezone data by the index.
132    pub fn find_tzdata<R: Read + Seek>(
133        &self,
134        mut reader: R,
135        header: &TzDataHeader,
136        index: &TzDataIndex,
137    ) -> Result<Vec<u8>> {
138        reader.seek(SeekFrom::Start(index.offset as u64 + header.data_offset as u64))?;
139        let mut buffer = vec![0; index.length as usize];
140        reader.read_exact(&mut buffer)?;
141        Ok(buffer)
142    }
143}
144
145/// Get timezone data from the `tzdata` file reader of Android.
146pub fn find_tz_data_android(
147    mut reader: impl Read + Seek,
148    tz_name: &[u8],
149) -> Result<Option<Vec<u8>>> {
150    let header = TzDataHeader::new(&mut reader)?;
151    let index = TzDataIndexes::new_android(&mut reader, &header)?;
152    Ok(if let Some(entry) = index.find_timezone(tz_name) {
153        Some(index.find_tzdata(reader, &header, entry)?)
154    } else {
155        None
156    })
157}
158
159/// Get timezone data from the `tzdata` file reader of HarmonyOS NEXT.
160pub fn find_tz_data_ohos(mut reader: impl Read + Seek, tz_name: &[u8]) -> Result<Option<Vec<u8>>> {
161    let header = TzDataHeader::new(&mut reader)?;
162    let index = TzDataIndexes::new_ohos(&mut reader, &header)?;
163    Ok(if let Some(entry) = index.find_timezone(tz_name) {
164        Some(index.find_tzdata(reader, &header, entry)?)
165    } else {
166        None
167    })
168}
169
170/// Get timezone data from the `tzdata` file of Android.
171pub fn find_tz_data_android_from_fs(tz_string: &str) -> Result<Option<Vec<u8>>> {
172    fn open_android_tz_data_file() -> Result<File> {
173        struct TzdataLocation {
174            env_var: &'static str,
175            path: &'static str,
176        }
177
178        const TZDATA_LOCATIONS: [TzdataLocation; 2] = [
179            TzdataLocation { env_var: "ANDROID_DATA", path: "/misc/zoneinfo" },
180            TzdataLocation { env_var: "ANDROID_ROOT", path: "/usr/share/zoneinfo" },
181        ];
182
183        for location in &TZDATA_LOCATIONS {
184            if let Ok(env_value) = std::env::var(location.env_var) {
185                if let Ok(file) = File::open(format!("{}{}/tzdata", env_value, location.path)) {
186                    return Ok(file);
187                }
188            }
189        }
190        Err(std::io::Error::from(std::io::ErrorKind::NotFound))
191    }
192    let mut file = open_android_tz_data_file()?;
193    find_tz_data_android(&mut file, tz_string.as_bytes())
194}
195
196/// Get timezone data from the `tzdata` file of HarmonyOS NEXT.
197pub fn find_tz_data_ohos_from_fs(tz_string: &str) -> Result<Option<Vec<u8>>> {
198    const TZDATA_PATH: &str = "/system/etc/zoneinfo/tzdata";
199    match File::open(TZDATA_PATH) {
200        Ok(mut file) => Ok(find_tz_data_ohos(&mut file, tz_string.as_bytes())?),
201        Err(err) => Err(err),
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_ohos_tzdata_header_and_index() {
211        let file = File::open("./tests/ohos/tzdata").unwrap();
212        let header = TzDataHeader::new(&file).unwrap();
213        assert_eq!(header.version, *b"2024a");
214        assert_eq!(header.index_offset, 24);
215        assert_eq!(header.data_offset, 21240);
216        assert_eq!(header.zonetab_offset, 272428);
217
218        let iter = TzDataIndexes::new_ohos(&file, &header).unwrap();
219        assert_eq!(iter.timezones().len(), 442);
220        assert!(iter.find_timezone(b"Asia/Shanghai").is_some());
221        assert!(iter.find_timezone(b"Pacific/Noumea").is_some());
222    }
223
224    #[test]
225    fn test_ohos_tzdata_loading() {
226        let file = File::open("./tests/ohos/tzdata").unwrap();
227        let header = TzDataHeader::new(&file).unwrap();
228        let iter = TzDataIndexes::new_ohos(&file, &header).unwrap();
229        let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap();
230        let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap();
231        assert_eq!(tzdata.len(), 393);
232    }
233
234    #[test]
235    fn test_android_tzdata_header_and_index() {
236        let file = File::open("./tests/android/tzdata").unwrap();
237        let header = TzDataHeader::new(&file).unwrap();
238        assert_eq!(header.version, *b"2021a");
239        assert_eq!(header.index_offset, 24);
240        assert_eq!(header.data_offset, 30860);
241        assert_eq!(header.zonetab_offset, 491837);
242
243        let iter = TzDataIndexes::new_android(&file, &header).unwrap();
244        assert_eq!(iter.timezones().len(), 593);
245        assert!(iter.find_timezone(b"Asia/Shanghai").is_some());
246        assert!(iter.find_timezone(b"Pacific/Noumea").is_some());
247    }
248
249    #[test]
250    fn test_android_tzdata_loading() {
251        let file = File::open("./tests/android/tzdata").unwrap();
252        let header = TzDataHeader::new(&file).unwrap();
253        let iter = TzDataIndexes::new_android(&file, &header).unwrap();
254        let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap();
255        let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap();
256        assert_eq!(tzdata.len(), 573);
257    }
258
259    #[test]
260    fn test_ohos_tzdata_find() {
261        let file = File::open("./tests/ohos/tzdata").unwrap();
262        let tzdata = find_tz_data_ohos(file, b"Asia/Shanghai").unwrap().unwrap();
263        assert_eq!(tzdata.len(), 393);
264    }
265
266    #[test]
267    fn test_android_tzdata_find() {
268        let file = File::open("./tests/android/tzdata").unwrap();
269        let tzdata = find_tz_data_android(file, b"Asia/Shanghai").unwrap().unwrap();
270        assert_eq!(tzdata.len(), 573);
271    }
272
273    #[cfg(target_env = "ohos")]
274    #[test]
275    fn test_ohos_machine_tz_data_loading() {
276        let file = File::open("/system/etc/zoneinfo/tzdata").unwrap();
277        let tzdata = find_tz_data_ohos(file, b"Asia/Shanghai").unwrap().unwrap();
278        assert!(!tzdata.is_empty());
279    }
280}