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 | |
| 4 | use oxedyne_fe2o3_core::prelude::*; |
| 5 | |
| 6 | use std::{ |
| 7 | fs, |
| 8 | io::{Cursor, Read}, |
| 9 | path::Path, |
| 10 | }; |
| 11 | |
| 12 | /// RFC 8536, versions 1 to 3. |
| 13 | pub struct TZifParser { |
| 14 | data: Vec<u8>, |
| 15 | timezone_data: Option<TZifData>, |
| 16 | } |
| 17 | |
| 18 | #[derive(Clone, Debug)] |
| 19 | pub 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)] |
| 32 | pub 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)] |
| 39 | pub 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)] |
| 46 | pub 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 | |
| 52 | impl 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)] |
| 279 | struct 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 | |
| 289 | impl 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 | |
| 391 | fn 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 | |
| 398 | fn 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 | |
| 405 | fn 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)] |
| 413 | mod 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 | } |