1use std::{
5 ffi::CStr,
6 fmt::Debug,
7 fs::File,
8 io::{Error, Read, Result, Seek, SeekFrom},
9};
10
11const SIZEOF_TZNAME: usize = 40;
13const SIZEOF_INDEX_ENTRY_OHOS: usize = SIZEOF_TZNAME + 2 * size_of::<u32>();
15const SIZEOF_INDEX_ENTRY_ANDROID: usize = SIZEOF_TZNAME + 3 * size_of::<u32>();
18
19#[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 pub fn new<R: Read>(mut data: R) -> Result<Self> {
31 const TZDATA_VERSION_SIZE: usize = 12;
33 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
59pub 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
76pub struct TzDataIndexes {
78 indexes: Vec<TzDataIndex>,
79}
80
81impl TzDataIndexes {
82 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 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 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 pub fn timezones(&self) -> &[TzDataIndex] {
122 &self.indexes
123 }
124
125 pub fn find_timezone(&self, timezone: &[u8]) -> Option<&TzDataIndex> {
127 self.indexes.binary_search_by_key(&timezone, |x| &x.name).map(|x| &self.indexes[x]).ok()
129 }
130
131 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
145pub 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
159pub 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
170pub 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
196pub 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}