Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/time/tzif.rs

15.6 KiB, 55 runs

created by r1870400018:6397, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use oxedyne_fe2o3_core::prelude::*;
5
6use std::{
7 fs,
8 io::{Cursor, Read},
9 path::Path,
10};
11
12/// RFC 8536, versions 1 to 3.
13pub struct TZifParser {
14 data: Vec<u8>,
15 timezone_data: Option<TZifData>,
16}
17
18#[derive(Clone, Debug)]
19pub struct TZifData {
20 pub version: u8, // 1, 2 or 3
21 pub transition_times: Vec<i64>, // UTC seconds since the Unix epoch
22 pub transition_types: Vec<u8>, // indices into local_time_types
23 pub local_time_types: Vec<LocalTimeType>,
24 pub abbreviations: String,
25 pub leap_seconds: Vec<LeapSecond>,
26 pub standard_wall_indicators: Vec<bool>,
27 pub ut_local_indicators: Vec<bool>,
28 pub posix_tz_string: Option<String>, // version 2 and later
29}
30
31#[derive(Clone, Debug, PartialEq)]
32pub struct LocalTimeType {
33 pub utc_offset: i32, // seconds east of UTC
34 pub is_dst: bool,
35 pub abbreviation_index: usize, // into abbreviations
36}
37
38#[derive(Clone, Debug, PartialEq)]
39pub struct LeapSecond {
40 pub transition_time: i64, // UTC seconds since the Unix epoch
41 pub correction: i32, // cumulative, not the step
42}
43
44/// A local time may be reached twice or not at all across a DST transition.
45#[derive(Clone, Debug, PartialEq)]
46pub enum LocalTimeResult<T> {
47 Single(T),
48 Ambiguous(T, T), // the autumn fold: (standard, daylight)
49 None, // the spring gap, where the local time never occurs
50}
51
52impl TZifParser {
53 pub fn new() -> Self {
54 Self {
55 data: Vec::new(),
56 timezone_data: None,
57 }
58 }
59
60 pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> Outcome<()> {
61 self.data = res!(fs::read(path.as_ref()).map_err(|e|
62 err!("Failed to read TZif file {:?}: {}", path.as_ref(), e; IO, File)));
63 self.parse()
64 }
65
66 pub fn load_from_bytes(&mut self, data: &[u8]) -> Outcome<()> {
67 self.data = data.to_vec();
68 self.parse()
69 }
70
71 pub fn timezone_data(&self) -> Option<&TZifData> {
72 self.timezone_data.as_ref()
73 }
74
75 fn parse(&mut self) -> Outcome<()> {
76 if self.data.len() < 44 {
77 return Err(err!("TZif file too short: {} bytes", self.data.len(); Invalid, Input));
78 }
79
80 let mut cursor = Cursor::new(&self.data);
81 let header = res!(self.parse_header(&mut cursor));
82
83 // Parse version 1 data first (required for all versions)
84 let v1_data = res!(self.parse_data_block(&mut cursor, &header, false));
85
86 // For version 2+ files, parse the second data block with 64-bit timestamps
87 let timezone_data = if header.version >= 2 {
88 // Skip version 1 data and parse version 2+ header and data
89 let v2_header = res!(self.parse_header(&mut cursor));
90 let v2_data = res!(self.parse_data_block(&mut cursor, &v2_header, true));
91
92 // Parse POSIX TZ string footer
93 let posix_tz_string = res!(self.parse_posix_footer(&mut cursor));
94
95 TZifData {
96 version: header.version,
97 posix_tz_string: Some(posix_tz_string),
98 ..v2_data
99 }
100 } else {
101 TZifData {
102 version: header.version,
103 posix_tz_string: None,
104 ..v1_data
105 }
106 };
107
108 self.timezone_data = Some(timezone_data);
109 Ok(())
110 }
111
112 fn parse_header(&self, cursor: &mut Cursor<&Vec<u8>>) -> Outcome<TZifHeader> {
113 let mut magic = [0u8; 4];
114 res!(cursor.read_exact(&mut magic).map_err(|e|
115 err!("Failed to read magic number: {}", e; IO)));
116
117 if &magic != b"TZif" {
118 return Err(err!("Invalid TZif magic number: {:?}", magic; Invalid, Input));
119 }
120
121 let mut version_byte = [0u8; 1];
122 res!(cursor.read_exact(&mut version_byte).map_err(|e|
123 err!("Failed to read version: {}", e; IO)));
124
125 let version = match version_byte[0] {
126 0 => 1,
127 b'2' => 2,
128 b'3' => 3,
129 v => return Err(err!("Unsupported TZif version: {}", v; Invalid, Input)),
130 };
131
132 // Skip reserved bytes (15 bytes)
133 let mut reserved = [0u8; 15];
134 res!(cursor.read_exact(&mut reserved).map_err(|e|
135 err!("Failed to read reserved bytes: {}", e; IO)));
136
137 // Read counts (6 * 4 bytes = 24 bytes)
138 let tzh_utcnt = res!(read_u32_be(cursor)); // UT/local indicators
139 let tzh_stdcnt = res!(read_u32_be(cursor)); // standard/wall indicators
140 let tzh_leapcnt = res!(read_u32_be(cursor)); // leap second records
141 let tzh_timecnt = res!(read_u32_be(cursor)); // transition times
142 let tzh_typecnt = res!(read_u32_be(cursor)); // local time types
143 let tzh_charcnt = res!(read_u32_be(cursor)); // abbreviation characters
144
145 Ok(TZifHeader {
146 version,
147 tzh_utcnt,
148 tzh_stdcnt,
149 tzh_leapcnt,
150 tzh_timecnt,
151 tzh_typecnt,
152 tzh_charcnt,
153 })
154 }
155
156 fn parse_data_block(&self, cursor: &mut Cursor<&Vec<u8>>, header: &TZifHeader, is_64bit: bool) -> Outcome<TZifData> {
157 // Parse transition times
158 let mut transition_times = Vec::with_capacity(header.tzh_timecnt as usize);
159 for _ in 0..header.tzh_timecnt {
160 let time = if is_64bit {
161 res!(read_i64_be(cursor))
162 } else {
163 res!(read_i32_be(cursor)) as i64
164 };
165 transition_times.push(time);
166 }
167
168 // Parse transition types
169 let mut transition_types = Vec::with_capacity(header.tzh_timecnt as usize);
170 for _ in 0..header.tzh_timecnt {
171 let mut byte = [0u8; 1];
172 res!(cursor.read_exact(&mut byte).map_err(|e|
173 err!("Failed to read transition type: {}", e; IO)));
174 transition_types.push(byte[0]);
175 }
176
177 // Parse local time types
178 let mut local_time_types = Vec::with_capacity(header.tzh_typecnt as usize);
179 for _ in 0..header.tzh_typecnt {
180 let utc_offset = res!(read_i32_be(cursor));
181
182 let mut is_dst_byte = [0u8; 1];
183 res!(cursor.read_exact(&mut is_dst_byte).map_err(|e|
184 err!("Failed to read DST flag: {}", e; IO)));
185 let is_dst = is_dst_byte[0] != 0;
186
187 let mut abbrev_index_byte = [0u8; 1];
188 res!(cursor.read_exact(&mut abbrev_index_byte).map_err(|e|
189 err!("Failed to read abbreviation index: {}", e; IO)));
190 let abbreviation_index = abbrev_index_byte[0] as usize;
191
192 local_time_types.push(LocalTimeType {
193 utc_offset,
194 is_dst,
195 abbreviation_index,
196 });
197 }
198
199 // Parse abbreviations
200 let mut abbrev_data = vec![0u8; header.tzh_charcnt as usize];
201 res!(cursor.read_exact(&mut abbrev_data).map_err(|e|
202 err!("Failed to read abbreviations: {}", e; IO)));
203 let abbreviations = String::from_utf8_lossy(&abbrev_data).to_string();
204
205 // Parse leap seconds
206 let mut leap_seconds = Vec::with_capacity(header.tzh_leapcnt as usize);
207 for _ in 0..header.tzh_leapcnt {
208 let transition_time = if is_64bit {
209 res!(read_i64_be(cursor))
210 } else {
211 res!(read_i32_be(cursor)) as i64
212 };
213 let correction = res!(read_i32_be(cursor));
214 leap_seconds.push(LeapSecond { transition_time, correction });
215 }
216
217 // Parse standard/wall indicators
218 let mut standard_wall_indicators = Vec::with_capacity(header.tzh_stdcnt as usize);
219 for _ in 0..header.tzh_stdcnt {
220 let mut byte = [0u8; 1];
221 res!(cursor.read_exact(&mut byte).map_err(|e|
222 err!("Failed to read standard/wall indicator: {}", e; IO)));
223 standard_wall_indicators.push(byte[0] != 0);
224 }
225
226 // Parse UT/local indicators
227 let mut ut_local_indicators = Vec::with_capacity(header.tzh_utcnt as usize);
228 for _ in 0..header.tzh_utcnt {
229 let mut byte = [0u8; 1];
230 res!(cursor.read_exact(&mut byte).map_err(|e|
231 err!("Failed to read UT/local indicator: {}", e; IO)));
232 ut_local_indicators.push(byte[0] != 0);
233 }
234
235 Ok(TZifData {
236 version: header.version,
237 transition_times,
238 transition_types,
239 local_time_types,
240 abbreviations,
241 leap_seconds,
242 standard_wall_indicators,
243 ut_local_indicators,
244 posix_tz_string: None,
245 })
246 }
247
248 fn parse_posix_footer(&self, cursor: &mut Cursor<&Vec<u8>>) -> Outcome<String> {
249 // Skip newline
250 let mut newline = [0u8; 1];
251 res!(cursor.read_exact(&mut newline).map_err(|e|
252 err!("Failed to read newline before POSIX string: {}", e; IO)));
253
254 if newline[0] != b'\n' {
255 return Err(err!("Expected newline before POSIX string, got: {}", newline[0]; Invalid, Input));
256 }
257
258 // Read until final newline
259 let mut posix_data = Vec::new();
260 let mut byte = [0u8; 1];
261
262 loop {
263 match cursor.read_exact(&mut byte) {
264 Ok(()) => {
265 if byte[0] == b'\n' {
266 break;
267 }
268 posix_data.push(byte[0]);
269 },
270 Err(_) => break, // EOF
271 }
272 }
273
274 Ok(String::from_utf8_lossy(&posix_data).to_string())
275 }
276}
277
278#[derive(Debug)]
279struct TZifHeader {
280 version: u8,
281 tzh_utcnt: u32, // UT/local indicators count
282 tzh_stdcnt: u32, // standard/wall indicators count
283 tzh_leapcnt: u32, // leap second records count
284 tzh_timecnt: u32, // transition times count
285 tzh_typecnt: u32, // local time types count
286 tzh_charcnt: u32, // abbreviation characters count
287}
288
289impl TZifData {
290 pub fn get_abbreviation(&self, local_time_type: &LocalTimeType) -> Outcome<&str> {
291 if local_time_type.abbreviation_index >= self.abbreviations.len() {
292 return Err(err!(
293 "Abbreviation index {} out of bounds (len: {})",
294 local_time_type.abbreviation_index, self.abbreviations.len();
295 Invalid, Input
296 ));
297 }
298
299 let abbrev_start = local_time_type.abbreviation_index;
300 let abbrev_end = self.abbreviations[abbrev_start..]
301 .find('\0')
302 .map(|pos| abbrev_start + pos)
303 .unwrap_or(self.abbreviations.len());
304
305 Ok(&self.abbreviations[abbrev_start..abbrev_end])
306 }
307
308 pub fn utc_to_local(&self, utc_timestamp: i64) -> LocalTimeResult<(i64, &LocalTimeType)> {
309 // Find the applicable transition
310 let transition_index = self.transition_times
311 .binary_search(&utc_timestamp)
312 .unwrap_or_else(|insert_pos| {
313 if insert_pos == 0 { 0 } else { insert_pos - 1 }
314 });
315
316 if transition_index >= self.transition_types.len() {
317 return LocalTimeResult::None;
318 }
319
320 let type_index = self.transition_types[transition_index] as usize;
321
322 if type_index >= self.local_time_types.len() {
323 return LocalTimeResult::None;
324 }
325
326 let local_time_type = &self.local_time_types[type_index];
327 let local_timestamp = utc_timestamp + local_time_type.utc_offset as i64;
328
329 LocalTimeResult::Single((local_timestamp, local_time_type))
330 }
331
332 pub fn local_to_utc(&self, local_timestamp: i64) -> LocalTimeResult<(i64, &LocalTimeType)> {
333 // This is more complex due to DST transitions creating ambiguous or invalid times
334 let mut candidates = Vec::new();
335
336 // Check all possible timezone rules around this time
337 for (i, &_transition_time) in self.transition_times.iter().enumerate() {
338 if i >= self.transition_types.len() {
339 continue;
340 }
341
342 let type_index = self.transition_types[i] as usize;
343 if type_index >= self.local_time_types.len() {
344 continue;
345 }
346
347 let local_time_type = &self.local_time_types[type_index];
348 let candidate_utc = local_timestamp - local_time_type.utc_offset as i64;
349
350 // Check if this UTC time would produce the given local time
351 if let LocalTimeResult::Single((computed_local, _)) = self.utc_to_local(candidate_utc) {
352 if computed_local == local_timestamp {
353 candidates.push((candidate_utc, local_time_type));
354 }
355 }
356
357 // Only check transitions around the target time (within 24 hours)
358 if (candidate_utc - local_timestamp).abs() > 86400 {
359 continue;
360 }
361 }
362
363 match candidates.len() {
364 0 => LocalTimeResult::None,
365 1 => LocalTimeResult::Single(candidates[0]),
366 2 => LocalTimeResult::Ambiguous(candidates[0], candidates[1]),
367 _ => {
368 // Multiple candidates - take the first valid one
369 LocalTimeResult::Single(candidates[0])
370 }
371 }
372 }
373
374 pub fn get_offset_at_utc(&self, utc_timestamp: i64) -> Outcome<i32> {
375 match self.utc_to_local(utc_timestamp) {
376 LocalTimeResult::Single((_, local_time_type)) => Ok(local_time_type.utc_offset),
377 _ => Err(err!("Could not determine offset for UTC timestamp {}", utc_timestamp; Invalid, Input)),
378 }
379 }
380
381 pub fn is_dst_at_utc(&self, utc_timestamp: i64) -> Outcome<bool> {
382 match self.utc_to_local(utc_timestamp) {
383 LocalTimeResult::Single((_, local_time_type)) => Ok(local_time_type.is_dst),
384 _ => Err(err!("Could not determine DST status for UTC timestamp {}", utc_timestamp; Invalid, Input)),
385 }
386 }
387}
388
389// Helper functions for reading big-endian values
390
391fn read_u32_be(cursor: &mut Cursor<&Vec<u8>>) -> Outcome<u32> {
392 let mut bytes = [0u8; 4];
393 res!(cursor.read_exact(&mut bytes).map_err(|e|
394 err!("Failed to read u32: {}", e; IO)));
395 Ok(u32::from_be_bytes(bytes))
396}
397
398fn read_i32_be(cursor: &mut Cursor<&Vec<u8>>) -> Outcome<i32> {
399 let mut bytes = [0u8; 4];
400 res!(cursor.read_exact(&mut bytes).map_err(|e|
401 err!("Failed to read i32: {}", e; IO)));
402 Ok(i32::from_be_bytes(bytes))
403}
404
405fn read_i64_be(cursor: &mut Cursor<&Vec<u8>>) -> Outcome<i64> {
406 let mut bytes = [0u8; 8];
407 res!(cursor.read_exact(&mut bytes).map_err(|e|
408 err!("Failed to read i64: {}", e; IO)));
409 Ok(i64::from_be_bytes(bytes))
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn test_tzif_header_parsing() {
418 // Create minimal valid TZif header
419 let mut data = Vec::new();
420 data.extend_from_slice(b"TZif"); // Magic
421 data.push(b'2'); // Version 2
422 data.extend_from_slice(&[0u8; 15]); // Reserved
423 // Counts (all zero for minimal test)
424 data.extend_from_slice(&[0u8; 24]); // 6 * 4 bytes of counts
425
426 let mut parser = TZifParser::new();
427 assert!(parser.load_from_bytes(&data).is_ok());
428 }
429
430 #[test]
431 fn test_local_time_type() {
432 let ltt = LocalTimeType {
433 utc_offset: -18000, // EST: -5 hours
434 is_dst: false,
435 abbreviation_index: 0,
436 };
437
438 assert_eq!(ltt.utc_offset, -18000);
439 assert!(!ltt.is_dst);
440 }
441
442 #[test]
443 fn test_leap_second() {
444 let leap = LeapSecond {
445 transition_time: 78796800, // 1972-07-01
446 correction: 1,
447 };
448
449 assert_eq!(leap.transition_time, 78796800);
450 assert_eq!(leap.correction, 1);
451 }
452}