| 1 | //! Types related to a time zone. |
| 2 | |
| 3 | use std::fs::{self, File}; |
| 4 | use std::io::{self, Read}; |
| 5 | use std::path::{Path, PathBuf}; |
| 6 | use std::{cmp::Ordering, fmt, str}; |
| 7 | |
| 8 | use super::rule::{AlternateTime, TransitionRule}; |
| 9 | use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY}; |
| 10 | |
| 11 | #[cfg (target_env = "ohos" )] |
| 12 | use crate::offset::local::tz_info::parser::Cursor; |
| 13 | |
| 14 | /// Time zone |
| 15 | #[derive (Debug, Clone, Eq, PartialEq)] |
| 16 | pub(crate) struct TimeZone { |
| 17 | /// List of transitions |
| 18 | transitions: Vec<Transition>, |
| 19 | /// List of local time types (cannot be empty) |
| 20 | local_time_types: Vec<LocalTimeType>, |
| 21 | /// List of leap seconds |
| 22 | leap_seconds: Vec<LeapSecond>, |
| 23 | /// Extra transition rule applicable after the last transition |
| 24 | extra_rule: Option<TransitionRule>, |
| 25 | } |
| 26 | |
| 27 | impl TimeZone { |
| 28 | /// Returns local time zone. |
| 29 | /// |
| 30 | /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. |
| 31 | pub(crate) fn local(env_tz: Option<&str>) -> Result<Self, Error> { |
| 32 | match env_tz { |
| 33 | Some(tz) => Self::from_posix_tz(tz), |
| 34 | None => Self::from_posix_tz("localtime" ), |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). |
| 39 | fn from_posix_tz(tz_string: &str) -> Result<Self, Error> { |
| 40 | if tz_string.is_empty() { |
| 41 | return Err(Error::InvalidTzString("empty TZ string" )); |
| 42 | } |
| 43 | |
| 44 | if tz_string == "localtime" { |
| 45 | return Self::from_tz_data(&fs::read("/etc/localtime" )?); |
| 46 | } |
| 47 | |
| 48 | // attributes are not allowed on if blocks in Rust 1.38 |
| 49 | #[cfg (target_os = "android" )] |
| 50 | { |
| 51 | if let Ok(bytes) = android_tzdata::find_tz_data(tz_string) { |
| 52 | return Self::from_tz_data(&bytes); |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // ohos merge all file into tzdata since ver35 |
| 57 | #[cfg (target_env = "ohos" )] |
| 58 | { |
| 59 | return Self::from_tz_data(&find_ohos_tz_data(tz_string)?); |
| 60 | } |
| 61 | |
| 62 | let mut chars = tz_string.chars(); |
| 63 | if chars.next() == Some(':' ) { |
| 64 | return Self::from_file(&mut find_tz_file(chars.as_str())?); |
| 65 | } |
| 66 | |
| 67 | if let Ok(mut file) = find_tz_file(tz_string) { |
| 68 | return Self::from_file(&mut file); |
| 69 | } |
| 70 | |
| 71 | // TZ string extensions are not allowed |
| 72 | let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace()); |
| 73 | let rule = TransitionRule::from_tz_string(tz_string.as_bytes(), false)?; |
| 74 | Self::new( |
| 75 | vec![], |
| 76 | match rule { |
| 77 | TransitionRule::Fixed(local_time_type) => vec![local_time_type], |
| 78 | TransitionRule::Alternate(AlternateTime { std, dst, .. }) => vec![std, dst], |
| 79 | }, |
| 80 | vec![], |
| 81 | Some(rule), |
| 82 | ) |
| 83 | } |
| 84 | |
| 85 | /// Construct a time zone |
| 86 | pub(super) fn new( |
| 87 | transitions: Vec<Transition>, |
| 88 | local_time_types: Vec<LocalTimeType>, |
| 89 | leap_seconds: Vec<LeapSecond>, |
| 90 | extra_rule: Option<TransitionRule>, |
| 91 | ) -> Result<Self, Error> { |
| 92 | let new = Self { transitions, local_time_types, leap_seconds, extra_rule }; |
| 93 | new.as_ref().validate()?; |
| 94 | Ok(new) |
| 95 | } |
| 96 | |
| 97 | /// Construct a time zone from the contents of a time zone file |
| 98 | fn from_file(file: &mut File) -> Result<Self, Error> { |
| 99 | let mut bytes = Vec::new(); |
| 100 | file.read_to_end(&mut bytes)?; |
| 101 | Self::from_tz_data(&bytes) |
| 102 | } |
| 103 | |
| 104 | /// Construct a time zone from the contents of a time zone file |
| 105 | /// |
| 106 | /// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536). |
| 107 | pub(crate) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> { |
| 108 | parser::parse(bytes) |
| 109 | } |
| 110 | |
| 111 | /// Construct a time zone with the specified UTC offset in seconds |
| 112 | fn fixed(ut_offset: i32) -> Result<Self, Error> { |
| 113 | Ok(Self { |
| 114 | transitions: Vec::new(), |
| 115 | local_time_types: vec![LocalTimeType::with_offset(ut_offset)?], |
| 116 | leap_seconds: Vec::new(), |
| 117 | extra_rule: None, |
| 118 | }) |
| 119 | } |
| 120 | |
| 121 | /// Construct the time zone associated to UTC |
| 122 | pub(crate) fn utc() -> Self { |
| 123 | Self { |
| 124 | transitions: Vec::new(), |
| 125 | local_time_types: vec![LocalTimeType::UTC], |
| 126 | leap_seconds: Vec::new(), |
| 127 | extra_rule: None, |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | /// Find the local time type associated to the time zone at the specified Unix time in seconds |
| 132 | pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> { |
| 133 | self.as_ref().find_local_time_type(unix_time) |
| 134 | } |
| 135 | |
| 136 | // should we pass NaiveDateTime all the way through to this fn? |
| 137 | pub(crate) fn find_local_time_type_from_local( |
| 138 | &self, |
| 139 | local_time: i64, |
| 140 | year: i32, |
| 141 | ) -> Result<crate::MappedLocalTime<LocalTimeType>, Error> { |
| 142 | self.as_ref().find_local_time_type_from_local(local_time, year) |
| 143 | } |
| 144 | |
| 145 | /// Returns a reference to the time zone |
| 146 | fn as_ref(&self) -> TimeZoneRef { |
| 147 | TimeZoneRef { |
| 148 | transitions: &self.transitions, |
| 149 | local_time_types: &self.local_time_types, |
| 150 | leap_seconds: &self.leap_seconds, |
| 151 | extra_rule: &self.extra_rule, |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | /// Reference to a time zone |
| 157 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
| 158 | pub(crate) struct TimeZoneRef<'a> { |
| 159 | /// List of transitions |
| 160 | transitions: &'a [Transition], |
| 161 | /// List of local time types (cannot be empty) |
| 162 | local_time_types: &'a [LocalTimeType], |
| 163 | /// List of leap seconds |
| 164 | leap_seconds: &'a [LeapSecond], |
| 165 | /// Extra transition rule applicable after the last transition |
| 166 | extra_rule: &'a Option<TransitionRule>, |
| 167 | } |
| 168 | |
| 169 | impl<'a> TimeZoneRef<'a> { |
| 170 | /// Find the local time type associated to the time zone at the specified Unix time in seconds |
| 171 | pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, Error> { |
| 172 | let extra_rule = match self.transitions.last() { |
| 173 | None => match self.extra_rule { |
| 174 | Some(extra_rule) => extra_rule, |
| 175 | None => return Ok(&self.local_time_types[0]), |
| 176 | }, |
| 177 | Some(last_transition) => { |
| 178 | let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) { |
| 179 | Ok(unix_leap_time) => unix_leap_time, |
| 180 | Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)), |
| 181 | Err(err) => return Err(err), |
| 182 | }; |
| 183 | |
| 184 | if unix_leap_time >= last_transition.unix_leap_time { |
| 185 | match self.extra_rule { |
| 186 | Some(extra_rule) => extra_rule, |
| 187 | None => { |
| 188 | // RFC 8536 3.2: |
| 189 | // "Local time for timestamps on or after the last transition is |
| 190 | // specified by the TZ string in the footer (Section 3.3) if present |
| 191 | // and nonempty; otherwise, it is unspecified." |
| 192 | // |
| 193 | // Older versions of macOS (1.12 and before?) have TZif file with a |
| 194 | // missing TZ string, and use the offset given by the last transition. |
| 195 | return Ok( |
| 196 | &self.local_time_types[last_transition.local_time_type_index] |
| 197 | ); |
| 198 | } |
| 199 | } |
| 200 | } else { |
| 201 | let index = match self |
| 202 | .transitions |
| 203 | .binary_search_by_key(&unix_leap_time, Transition::unix_leap_time) |
| 204 | { |
| 205 | Ok(x) => x + 1, |
| 206 | Err(x) => x, |
| 207 | }; |
| 208 | |
| 209 | let local_time_type_index = if index > 0 { |
| 210 | self.transitions[index - 1].local_time_type_index |
| 211 | } else { |
| 212 | 0 |
| 213 | }; |
| 214 | return Ok(&self.local_time_types[local_time_type_index]); |
| 215 | } |
| 216 | } |
| 217 | }; |
| 218 | |
| 219 | match extra_rule.find_local_time_type(unix_time) { |
| 220 | Ok(local_time_type) => Ok(local_time_type), |
| 221 | Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)), |
| 222 | err => err, |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | pub(crate) fn find_local_time_type_from_local( |
| 227 | &self, |
| 228 | local_time: i64, |
| 229 | year: i32, |
| 230 | ) -> Result<crate::MappedLocalTime<LocalTimeType>, Error> { |
| 231 | // #TODO: this is wrong as we need 'local_time_to_local_leap_time ? |
| 232 | // but ... does the local time even include leap seconds ?? |
| 233 | // let unix_leap_time = match self.unix_time_to_unix_leap_time(local_time) { |
| 234 | // Ok(unix_leap_time) => unix_leap_time, |
| 235 | // Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)), |
| 236 | // Err(err) => return Err(err), |
| 237 | // }; |
| 238 | let local_leap_time = local_time; |
| 239 | |
| 240 | // if we have at least one transition, |
| 241 | // we must check _all_ of them, incase of any Overlapping (MappedLocalTime::Ambiguous) or Skipping (MappedLocalTime::None) transitions |
| 242 | let offset_after_last = if !self.transitions.is_empty() { |
| 243 | let mut prev = self.local_time_types[0]; |
| 244 | |
| 245 | for transition in self.transitions { |
| 246 | let after_ltt = self.local_time_types[transition.local_time_type_index]; |
| 247 | |
| 248 | // the end and start here refers to where the time starts prior to the transition |
| 249 | // and where it ends up after. not the temporal relationship. |
| 250 | let transition_end = transition.unix_leap_time + i64::from(after_ltt.ut_offset); |
| 251 | let transition_start = transition.unix_leap_time + i64::from(prev.ut_offset); |
| 252 | |
| 253 | match transition_start.cmp(&transition_end) { |
| 254 | Ordering::Greater => { |
| 255 | // backwards transition, eg from DST to regular |
| 256 | // this means a given local time could have one of two possible offsets |
| 257 | if local_leap_time < transition_end { |
| 258 | return Ok(crate::MappedLocalTime::Single(prev)); |
| 259 | } else if local_leap_time >= transition_end |
| 260 | && local_leap_time <= transition_start |
| 261 | { |
| 262 | if prev.ut_offset < after_ltt.ut_offset { |
| 263 | return Ok(crate::MappedLocalTime::Ambiguous(prev, after_ltt)); |
| 264 | } else { |
| 265 | return Ok(crate::MappedLocalTime::Ambiguous(after_ltt, prev)); |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | Ordering::Equal => { |
| 270 | // should this ever happen? presumably we have to handle it anyway. |
| 271 | if local_leap_time < transition_start { |
| 272 | return Ok(crate::MappedLocalTime::Single(prev)); |
| 273 | } else if local_leap_time == transition_end { |
| 274 | if prev.ut_offset < after_ltt.ut_offset { |
| 275 | return Ok(crate::MappedLocalTime::Ambiguous(prev, after_ltt)); |
| 276 | } else { |
| 277 | return Ok(crate::MappedLocalTime::Ambiguous(after_ltt, prev)); |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | Ordering::Less => { |
| 282 | // forwards transition, eg from regular to DST |
| 283 | // this means that times that are skipped are invalid local times |
| 284 | if local_leap_time <= transition_start { |
| 285 | return Ok(crate::MappedLocalTime::Single(prev)); |
| 286 | } else if local_leap_time < transition_end { |
| 287 | return Ok(crate::MappedLocalTime::None); |
| 288 | } else if local_leap_time == transition_end { |
| 289 | return Ok(crate::MappedLocalTime::Single(after_ltt)); |
| 290 | } |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | // try the next transition, we are fully after this one |
| 295 | prev = after_ltt; |
| 296 | } |
| 297 | |
| 298 | prev |
| 299 | } else { |
| 300 | self.local_time_types[0] |
| 301 | }; |
| 302 | |
| 303 | if let Some(extra_rule) = self.extra_rule { |
| 304 | match extra_rule.find_local_time_type_from_local(local_time, year) { |
| 305 | Ok(local_time_type) => Ok(local_time_type), |
| 306 | Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)), |
| 307 | err => err, |
| 308 | } |
| 309 | } else { |
| 310 | Ok(crate::MappedLocalTime::Single(offset_after_last)) |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | /// Check time zone inputs |
| 315 | fn validate(&self) -> Result<(), Error> { |
| 316 | // Check local time types |
| 317 | let local_time_types_size = self.local_time_types.len(); |
| 318 | if local_time_types_size == 0 { |
| 319 | return Err(Error::TimeZone("list of local time types must not be empty" )); |
| 320 | } |
| 321 | |
| 322 | // Check transitions |
| 323 | let mut i_transition = 0; |
| 324 | while i_transition < self.transitions.len() { |
| 325 | if self.transitions[i_transition].local_time_type_index >= local_time_types_size { |
| 326 | return Err(Error::TimeZone("invalid local time type index" )); |
| 327 | } |
| 328 | |
| 329 | if i_transition + 1 < self.transitions.len() |
| 330 | && self.transitions[i_transition].unix_leap_time |
| 331 | >= self.transitions[i_transition + 1].unix_leap_time |
| 332 | { |
| 333 | return Err(Error::TimeZone("invalid transition" )); |
| 334 | } |
| 335 | |
| 336 | i_transition += 1; |
| 337 | } |
| 338 | |
| 339 | // Check leap seconds |
| 340 | if !(self.leap_seconds.is_empty() |
| 341 | || self.leap_seconds[0].unix_leap_time >= 0 |
| 342 | && self.leap_seconds[0].correction.saturating_abs() == 1) |
| 343 | { |
| 344 | return Err(Error::TimeZone("invalid leap second" )); |
| 345 | } |
| 346 | |
| 347 | let min_interval = SECONDS_PER_28_DAYS - 1; |
| 348 | |
| 349 | let mut i_leap_second = 0; |
| 350 | while i_leap_second < self.leap_seconds.len() { |
| 351 | if i_leap_second + 1 < self.leap_seconds.len() { |
| 352 | let x0 = &self.leap_seconds[i_leap_second]; |
| 353 | let x1 = &self.leap_seconds[i_leap_second + 1]; |
| 354 | |
| 355 | let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time); |
| 356 | let abs_diff_correction = |
| 357 | x1.correction.saturating_sub(x0.correction).saturating_abs(); |
| 358 | |
| 359 | if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) { |
| 360 | return Err(Error::TimeZone("invalid leap second" )); |
| 361 | } |
| 362 | } |
| 363 | i_leap_second += 1; |
| 364 | } |
| 365 | |
| 366 | // Check extra rule |
| 367 | let (extra_rule, last_transition) = match (&self.extra_rule, self.transitions.last()) { |
| 368 | (Some(rule), Some(trans)) => (rule, trans), |
| 369 | _ => return Ok(()), |
| 370 | }; |
| 371 | |
| 372 | let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index]; |
| 373 | let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) { |
| 374 | Ok(unix_time) => unix_time, |
| 375 | Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)), |
| 376 | Err(err) => return Err(err), |
| 377 | }; |
| 378 | |
| 379 | let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) { |
| 380 | Ok(rule_local_time_type) => rule_local_time_type, |
| 381 | Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)), |
| 382 | Err(err) => return Err(err), |
| 383 | }; |
| 384 | |
| 385 | let check = last_local_time_type.ut_offset == rule_local_time_type.ut_offset |
| 386 | && last_local_time_type.is_dst == rule_local_time_type.is_dst |
| 387 | && match (&last_local_time_type.name, &rule_local_time_type.name) { |
| 388 | (Some(x), Some(y)) => x.equal(y), |
| 389 | (None, None) => true, |
| 390 | _ => false, |
| 391 | }; |
| 392 | |
| 393 | if !check { |
| 394 | return Err(Error::TimeZone( |
| 395 | "extra transition rule is inconsistent with the last transition" , |
| 396 | )); |
| 397 | } |
| 398 | |
| 399 | Ok(()) |
| 400 | } |
| 401 | |
| 402 | /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone |
| 403 | const fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result<i64, Error> { |
| 404 | let mut unix_leap_time = unix_time; |
| 405 | |
| 406 | let mut i = 0; |
| 407 | while i < self.leap_seconds.len() { |
| 408 | let leap_second = &self.leap_seconds[i]; |
| 409 | |
| 410 | if unix_leap_time < leap_second.unix_leap_time { |
| 411 | break; |
| 412 | } |
| 413 | |
| 414 | unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) { |
| 415 | Some(unix_leap_time) => unix_leap_time, |
| 416 | None => return Err(Error::OutOfRange("out of range operation" )), |
| 417 | }; |
| 418 | |
| 419 | i += 1; |
| 420 | } |
| 421 | |
| 422 | Ok(unix_leap_time) |
| 423 | } |
| 424 | |
| 425 | /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone |
| 426 | fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result<i64, Error> { |
| 427 | if unix_leap_time == i64::MIN { |
| 428 | return Err(Error::OutOfRange("out of range operation" )); |
| 429 | } |
| 430 | |
| 431 | let index = match self |
| 432 | .leap_seconds |
| 433 | .binary_search_by_key(&(unix_leap_time - 1), LeapSecond::unix_leap_time) |
| 434 | { |
| 435 | Ok(x) => x + 1, |
| 436 | Err(x) => x, |
| 437 | }; |
| 438 | |
| 439 | let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 }; |
| 440 | |
| 441 | match unix_leap_time.checked_sub(correction as i64) { |
| 442 | Some(unix_time) => Ok(unix_time), |
| 443 | None => Err(Error::OutOfRange("out of range operation" )), |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | /// The UTC time zone |
| 448 | const UTC: TimeZoneRef<'static> = TimeZoneRef { |
| 449 | transitions: &[], |
| 450 | local_time_types: &[LocalTimeType::UTC], |
| 451 | leap_seconds: &[], |
| 452 | extra_rule: &None, |
| 453 | }; |
| 454 | } |
| 455 | |
| 456 | /// Transition of a TZif file |
| 457 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
| 458 | pub(super) struct Transition { |
| 459 | /// Unix leap time |
| 460 | unix_leap_time: i64, |
| 461 | /// Index specifying the local time type of the transition |
| 462 | local_time_type_index: usize, |
| 463 | } |
| 464 | |
| 465 | impl Transition { |
| 466 | /// Construct a TZif file transition |
| 467 | pub(super) const fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self { |
| 468 | Self { unix_leap_time, local_time_type_index } |
| 469 | } |
| 470 | |
| 471 | /// Returns Unix leap time |
| 472 | const fn unix_leap_time(&self) -> i64 { |
| 473 | self.unix_leap_time |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | /// Leap second of a TZif file |
| 478 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
| 479 | pub(super) struct LeapSecond { |
| 480 | /// Unix leap time |
| 481 | unix_leap_time: i64, |
| 482 | /// Leap second correction |
| 483 | correction: i32, |
| 484 | } |
| 485 | |
| 486 | impl LeapSecond { |
| 487 | /// Construct a TZif file leap second |
| 488 | pub(super) const fn new(unix_leap_time: i64, correction: i32) -> Self { |
| 489 | Self { unix_leap_time, correction } |
| 490 | } |
| 491 | |
| 492 | /// Returns Unix leap time |
| 493 | const fn unix_leap_time(&self) -> i64 { |
| 494 | self.unix_leap_time |
| 495 | } |
| 496 | } |
| 497 | |
| 498 | /// ASCII-encoded fixed-capacity string, used for storing time zone names |
| 499 | #[derive (Copy, Clone, Eq, PartialEq)] |
| 500 | struct TimeZoneName { |
| 501 | /// Length-prefixed string buffer |
| 502 | bytes: [u8; 8], |
| 503 | } |
| 504 | |
| 505 | impl TimeZoneName { |
| 506 | /// Construct a time zone name |
| 507 | /// |
| 508 | /// man tzfile(5): |
| 509 | /// Time zone designations should consist of at least three (3) and no more than six (6) ASCII |
| 510 | /// characters from the set of alphanumerics, “-”, and “+”. This is for compatibility with |
| 511 | /// POSIX requirements for time zone abbreviations. |
| 512 | fn new(input: &[u8]) -> Result<Self, Error> { |
| 513 | let len = input.len(); |
| 514 | |
| 515 | if !(3..=7).contains(&len) { |
| 516 | return Err(Error::LocalTimeType( |
| 517 | "time zone name must have between 3 and 7 characters" , |
| 518 | )); |
| 519 | } |
| 520 | |
| 521 | let mut bytes = [0; 8]; |
| 522 | bytes[0] = input.len() as u8; |
| 523 | |
| 524 | let mut i = 0; |
| 525 | while i < len { |
| 526 | let b = input[i]; |
| 527 | match b { |
| 528 | b'0' ..=b'9' | b'A' ..=b'Z' | b'a' ..=b'z' | b'+' | b'-' => {} |
| 529 | _ => return Err(Error::LocalTimeType("invalid characters in time zone name" )), |
| 530 | } |
| 531 | |
| 532 | bytes[i + 1] = b; |
| 533 | i += 1; |
| 534 | } |
| 535 | |
| 536 | Ok(Self { bytes }) |
| 537 | } |
| 538 | |
| 539 | /// Returns time zone name as a byte slice |
| 540 | fn as_bytes(&self) -> &[u8] { |
| 541 | match self.bytes[0] { |
| 542 | 3 => &self.bytes[1..4], |
| 543 | 4 => &self.bytes[1..5], |
| 544 | 5 => &self.bytes[1..6], |
| 545 | 6 => &self.bytes[1..7], |
| 546 | 7 => &self.bytes[1..8], |
| 547 | _ => unreachable!(), |
| 548 | } |
| 549 | } |
| 550 | |
| 551 | /// Check if two time zone names are equal |
| 552 | fn equal(&self, other: &Self) -> bool { |
| 553 | self.bytes == other.bytes |
| 554 | } |
| 555 | } |
| 556 | |
| 557 | impl AsRef<str> for TimeZoneName { |
| 558 | fn as_ref(&self) -> &str { |
| 559 | // SAFETY: ASCII is valid UTF-8 |
| 560 | unsafe { str::from_utf8_unchecked(self.as_bytes()) } |
| 561 | } |
| 562 | } |
| 563 | |
| 564 | impl fmt::Debug for TimeZoneName { |
| 565 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 566 | self.as_ref().fmt(f) |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | /// Local time type associated to a time zone |
| 571 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
| 572 | pub(crate) struct LocalTimeType { |
| 573 | /// Offset from UTC in seconds |
| 574 | pub(super) ut_offset: i32, |
| 575 | /// Daylight Saving Time indicator |
| 576 | is_dst: bool, |
| 577 | /// Time zone name |
| 578 | name: Option<TimeZoneName>, |
| 579 | } |
| 580 | |
| 581 | impl LocalTimeType { |
| 582 | /// Construct a local time type |
| 583 | pub(super) fn new(ut_offset: i32, is_dst: bool, name: Option<&[u8]>) -> Result<Self, Error> { |
| 584 | if ut_offset == i32::MIN { |
| 585 | return Err(Error::LocalTimeType("invalid UTC offset" )); |
| 586 | } |
| 587 | |
| 588 | let name = match name { |
| 589 | Some(name) => TimeZoneName::new(name)?, |
| 590 | None => return Ok(Self { ut_offset, is_dst, name: None }), |
| 591 | }; |
| 592 | |
| 593 | Ok(Self { ut_offset, is_dst, name: Some(name) }) |
| 594 | } |
| 595 | |
| 596 | /// Construct a local time type with the specified UTC offset in seconds |
| 597 | pub(super) const fn with_offset(ut_offset: i32) -> Result<Self, Error> { |
| 598 | if ut_offset == i32::MIN { |
| 599 | return Err(Error::LocalTimeType("invalid UTC offset" )); |
| 600 | } |
| 601 | |
| 602 | Ok(Self { ut_offset, is_dst: false, name: None }) |
| 603 | } |
| 604 | |
| 605 | /// Returns offset from UTC in seconds |
| 606 | pub(crate) const fn offset(&self) -> i32 { |
| 607 | self.ut_offset |
| 608 | } |
| 609 | |
| 610 | /// Returns daylight saving time indicator |
| 611 | pub(super) const fn is_dst(&self) -> bool { |
| 612 | self.is_dst |
| 613 | } |
| 614 | |
| 615 | pub(super) const UTC: LocalTimeType = Self { ut_offset: 0, is_dst: false, name: None }; |
| 616 | } |
| 617 | |
| 618 | /// Open the TZif file corresponding to a TZ string |
| 619 | fn find_tz_file(path: impl AsRef<Path>) -> Result<File, Error> { |
| 620 | // Don't check system timezone directories on non-UNIX platforms |
| 621 | #[cfg (not(unix))] |
| 622 | return Ok(File::open(path)?); |
| 623 | |
| 624 | #[cfg (unix)] |
| 625 | { |
| 626 | let path: &Path = path.as_ref(); |
| 627 | if path.is_absolute() { |
| 628 | return Ok(File::open(path)?); |
| 629 | } |
| 630 | |
| 631 | for folder: &&str in &ZONE_INFO_DIRECTORIES { |
| 632 | if let Ok(file: File) = File::open(path:PathBuf::from(folder).join(path)) { |
| 633 | return Ok(file); |
| 634 | } |
| 635 | } |
| 636 | |
| 637 | Err(Error::Io(io::ErrorKind::NotFound.into())) |
| 638 | } |
| 639 | } |
| 640 | |
| 641 | #[cfg (target_env = "ohos" )] |
| 642 | fn from_tzdata_bytes(bytes: &mut Vec<u8>, tz_string: &str) -> Result<Vec<u8>, Error> { |
| 643 | const VERSION_SIZE: usize = 12; |
| 644 | const OFFSET_SIZE: usize = 4; |
| 645 | const INDEX_CHUNK_SIZE: usize = 48; |
| 646 | const ZONENAME_SIZE: usize = 40; |
| 647 | |
| 648 | let mut cursor = Cursor::new(&bytes); |
| 649 | // version head |
| 650 | let _ = cursor.read_exact(VERSION_SIZE)?; |
| 651 | let index_offset_offset = cursor.read_be_u32()?; |
| 652 | let data_offset_offset = cursor.read_be_u32()?; |
| 653 | // final offset |
| 654 | let _ = cursor.read_be_u32()?; |
| 655 | |
| 656 | cursor.seek_after(index_offset_offset as usize)?; |
| 657 | let mut idx = index_offset_offset; |
| 658 | while idx < data_offset_offset { |
| 659 | let index_buf = cursor.read_exact(ZONENAME_SIZE)?; |
| 660 | let offset = cursor.read_be_u32()?; |
| 661 | let length = cursor.read_be_u32()?; |
| 662 | let zone_name = str::from_utf8(index_buf)?.trim_end_matches(' \0' ); |
| 663 | if zone_name != tz_string { |
| 664 | idx += INDEX_CHUNK_SIZE as u32; |
| 665 | continue; |
| 666 | } |
| 667 | cursor.seek_after((data_offset_offset + offset) as usize)?; |
| 668 | return match cursor.read_exact(length as usize) { |
| 669 | Ok(result) => Ok(result.to_vec()), |
| 670 | Err(_err) => Err(Error::InvalidTzFile("invalid ohos tzdata chunk" )), |
| 671 | }; |
| 672 | } |
| 673 | |
| 674 | Err(Error::InvalidTzString("cannot find tz string within tzdata" )) |
| 675 | } |
| 676 | |
| 677 | #[cfg (target_env = "ohos" )] |
| 678 | fn from_tzdata_file(file: &mut File, tz_string: &str) -> Result<Vec<u8>, Error> { |
| 679 | let mut bytes = Vec::new(); |
| 680 | file.read_to_end(&mut bytes)?; |
| 681 | from_tzdata_bytes(&mut bytes, tz_string) |
| 682 | } |
| 683 | |
| 684 | #[cfg (target_env = "ohos" )] |
| 685 | fn find_ohos_tz_data(tz_string: &str) -> Result<Vec<u8>, Error> { |
| 686 | const TZDATA_PATH: &str = "/system/etc/zoneinfo/tzdata" ; |
| 687 | match File::open(TZDATA_PATH) { |
| 688 | Ok(mut file) => from_tzdata_file(&mut file, tz_string), |
| 689 | Err(err) => Err(err.into()), |
| 690 | } |
| 691 | } |
| 692 | |
| 693 | // Possible system timezone directories |
| 694 | #[cfg (unix)] |
| 695 | const ZONE_INFO_DIRECTORIES: [&str; 4] = |
| 696 | ["/usr/share/zoneinfo" , "/share/zoneinfo" , "/etc/zoneinfo" , "/usr/share/lib/zoneinfo" ]; |
| 697 | |
| 698 | /// Number of seconds in one week |
| 699 | pub(crate) const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK; |
| 700 | /// Number of seconds in 28 days |
| 701 | const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28; |
| 702 | |
| 703 | #[cfg (test)] |
| 704 | mod tests { |
| 705 | use super::super::Error; |
| 706 | use super::{LeapSecond, LocalTimeType, TimeZone, TimeZoneName, Transition, TransitionRule}; |
| 707 | |
| 708 | #[test ] |
| 709 | fn test_no_dst() -> Result<(), Error> { |
| 710 | let tz_string = b"HST10" ; |
| 711 | let transition_rule = TransitionRule::from_tz_string(tz_string, false)?; |
| 712 | assert_eq!(transition_rule, LocalTimeType::new(-36000, false, Some(b"HST" ))?.into()); |
| 713 | Ok(()) |
| 714 | } |
| 715 | |
| 716 | #[test ] |
| 717 | fn test_error() -> Result<(), Error> { |
| 718 | assert!(matches!( |
| 719 | TransitionRule::from_tz_string(b"IST-1GMT0" , false), |
| 720 | Err(Error::UnsupportedTzString(_)) |
| 721 | )); |
| 722 | assert!(matches!( |
| 723 | TransitionRule::from_tz_string(b"EET-2EEST" , false), |
| 724 | Err(Error::UnsupportedTzString(_)) |
| 725 | )); |
| 726 | |
| 727 | Ok(()) |
| 728 | } |
| 729 | |
| 730 | #[test ] |
| 731 | fn test_v1_file_with_leap_seconds() -> Result<(), Error> { |
| 732 | let bytes = b"TZif \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x1b\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC \0\x04\xb2\x58\0\0\0\0\x01\x05\xa4\xec\x01\0\0\0\x02\x07\x86\x1f\x82\0\0\0\x03\x09\x67\x53\x03\0\0\0\x04\x0b\x48\x86\x84\0\0\0\x05\x0d\x2b\x0b\x85\0\0\0\x06\x0f\x0c\x3f\x06\0\0\0\x07\x10\xed\x72\x87\0\0\0\x08\x12\xce\xa6\x08\0\0\0\x09\x15\x9f\xca\x89\0\0\0\x0a\x17\x80\xfe\x0a\0\0\0\x0b\x19\x62\x31\x8b\0\0\0\x0c\x1d\x25\xea\x0c\0\0\0\x0d\x21\xda\xe5\x0d\0\0\0\x0e\x25\x9e\x9d\x8e\0\0\0\x0f\x27\x7f\xd1\x0f\0\0\0\x10\x2a\x50\xf5\x90\0\0\0\x11\x2c\x32\x29\x11\0\0\0\x12\x2e\x13\x5c\x92\0\0\0\x13\x30\xe7\x24\x13\0\0\0\x14\x33\xb8\x48\x94\0\0\0\x15\x36\x8c\x10\x15\0\0\0\x16\x43\xb7\x1b\x96\0\0\0\x17\x49\x5c\x07\x97\0\0\0\x18\x4f\xef\x93\x18\0\0\0\x19\x55\x93\x2d\x99\0\0\0\x1a\x58\x68\x46\x9a\0\0\0\x1b\0\0" ; |
| 733 | |
| 734 | let time_zone = TimeZone::from_tz_data(bytes)?; |
| 735 | |
| 736 | let time_zone_result = TimeZone::new( |
| 737 | Vec::new(), |
| 738 | vec![LocalTimeType::new(0, false, Some(b"UTC" ))?], |
| 739 | vec![ |
| 740 | LeapSecond::new(78796800, 1), |
| 741 | LeapSecond::new(94694401, 2), |
| 742 | LeapSecond::new(126230402, 3), |
| 743 | LeapSecond::new(157766403, 4), |
| 744 | LeapSecond::new(189302404, 5), |
| 745 | LeapSecond::new(220924805, 6), |
| 746 | LeapSecond::new(252460806, 7), |
| 747 | LeapSecond::new(283996807, 8), |
| 748 | LeapSecond::new(315532808, 9), |
| 749 | LeapSecond::new(362793609, 10), |
| 750 | LeapSecond::new(394329610, 11), |
| 751 | LeapSecond::new(425865611, 12), |
| 752 | LeapSecond::new(489024012, 13), |
| 753 | LeapSecond::new(567993613, 14), |
| 754 | LeapSecond::new(631152014, 15), |
| 755 | LeapSecond::new(662688015, 16), |
| 756 | LeapSecond::new(709948816, 17), |
| 757 | LeapSecond::new(741484817, 18), |
| 758 | LeapSecond::new(773020818, 19), |
| 759 | LeapSecond::new(820454419, 20), |
| 760 | LeapSecond::new(867715220, 21), |
| 761 | LeapSecond::new(915148821, 22), |
| 762 | LeapSecond::new(1136073622, 23), |
| 763 | LeapSecond::new(1230768023, 24), |
| 764 | LeapSecond::new(1341100824, 25), |
| 765 | LeapSecond::new(1435708825, 26), |
| 766 | LeapSecond::new(1483228826, 27), |
| 767 | ], |
| 768 | None, |
| 769 | )?; |
| 770 | |
| 771 | assert_eq!(time_zone, time_zone_result); |
| 772 | |
| 773 | Ok(()) |
| 774 | } |
| 775 | |
| 776 | #[test ] |
| 777 | fn test_v2_file() -> Result<(), Error> { |
| 778 | let bytes = b"TZif2 \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\x80\0\0\0\xbb\x05\x43\x48\xbb\x21\x71\x58\xcb\x89\x3d\xc8\xd2\x23\xf4\x70\xd2\x61\x49\x38\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT \0HST \0HDT \0HWT \0HPT \0\0\0\0\0\x01\0\0\0\0\0\x01\0TZif2 \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\xff\xff\xff\xff\x74\xe0\x70\xbe\xff\xff\xff\xff\xbb\x05\x43\x48\xff\xff\xff\xff\xbb\x21\x71\x58\xff\xff\xff\xff\xcb\x89\x3d\xc8\xff\xff\xff\xff\xd2\x23\xf4\x70\xff\xff\xff\xff\xd2\x61\x49\x38\xff\xff\xff\xff\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT \0HST \0HDT \0HWT \0HPT \0\0\0\0\0\x01\0\0\0\0\0\x01\0\x0aHST10 \x0a" ; |
| 779 | |
| 780 | let time_zone = TimeZone::from_tz_data(bytes)?; |
| 781 | |
| 782 | let time_zone_result = TimeZone::new( |
| 783 | vec![ |
| 784 | Transition::new(-2334101314, 1), |
| 785 | Transition::new(-1157283000, 2), |
| 786 | Transition::new(-1155436200, 1), |
| 787 | Transition::new(-880198200, 3), |
| 788 | Transition::new(-769395600, 4), |
| 789 | Transition::new(-765376200, 1), |
| 790 | Transition::new(-712150200, 5), |
| 791 | ], |
| 792 | vec![ |
| 793 | LocalTimeType::new(-37886, false, Some(b"LMT" ))?, |
| 794 | LocalTimeType::new(-37800, false, Some(b"HST" ))?, |
| 795 | LocalTimeType::new(-34200, true, Some(b"HDT" ))?, |
| 796 | LocalTimeType::new(-34200, true, Some(b"HWT" ))?, |
| 797 | LocalTimeType::new(-34200, true, Some(b"HPT" ))?, |
| 798 | LocalTimeType::new(-36000, false, Some(b"HST" ))?, |
| 799 | ], |
| 800 | Vec::new(), |
| 801 | Some(TransitionRule::from(LocalTimeType::new(-36000, false, Some(b"HST" ))?)), |
| 802 | )?; |
| 803 | |
| 804 | assert_eq!(time_zone, time_zone_result); |
| 805 | |
| 806 | assert_eq!( |
| 807 | *time_zone.find_local_time_type(-1156939200)?, |
| 808 | LocalTimeType::new(-34200, true, Some(b"HDT" ))? |
| 809 | ); |
| 810 | assert_eq!( |
| 811 | *time_zone.find_local_time_type(1546300800)?, |
| 812 | LocalTimeType::new(-36000, false, Some(b"HST" ))? |
| 813 | ); |
| 814 | |
| 815 | Ok(()) |
| 816 | } |
| 817 | |
| 818 | #[test ] |
| 819 | fn test_no_tz_string() -> Result<(), Error> { |
| 820 | // Guayaquil from macOS 10.11 |
| 821 | let bytes = b"TZif \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\x02\0\0\0\0\0\0\0\x01\0\0\0\x02\0\0\0\x08\xb6\xa4B \x18\x01\xff\xff\xb6h \0\0\xff\xff\xb9\xb0\0\x04QMT \0ECT \0\0\0\0\0" ; |
| 822 | |
| 823 | let time_zone = TimeZone::from_tz_data(bytes)?; |
| 824 | dbg!(&time_zone); |
| 825 | |
| 826 | let time_zone_result = TimeZone::new( |
| 827 | vec![Transition::new(-1230749160, 1)], |
| 828 | vec![ |
| 829 | LocalTimeType::new(-18840, false, Some(b"QMT" ))?, |
| 830 | LocalTimeType::new(-18000, false, Some(b"ECT" ))?, |
| 831 | ], |
| 832 | Vec::new(), |
| 833 | None, |
| 834 | )?; |
| 835 | |
| 836 | assert_eq!(time_zone, time_zone_result); |
| 837 | |
| 838 | assert_eq!( |
| 839 | *time_zone.find_local_time_type(-1500000000)?, |
| 840 | LocalTimeType::new(-18840, false, Some(b"QMT" ))? |
| 841 | ); |
| 842 | assert_eq!( |
| 843 | *time_zone.find_local_time_type(0)?, |
| 844 | LocalTimeType::new(-18000, false, Some(b"ECT" ))? |
| 845 | ); |
| 846 | |
| 847 | Ok(()) |
| 848 | } |
| 849 | |
| 850 | #[test ] |
| 851 | fn test_tz_ascii_str() -> Result<(), Error> { |
| 852 | assert!(matches!(TimeZoneName::new(b"" ), Err(Error::LocalTimeType(_)))); |
| 853 | assert!(matches!(TimeZoneName::new(b"A" ), Err(Error::LocalTimeType(_)))); |
| 854 | assert!(matches!(TimeZoneName::new(b"AB" ), Err(Error::LocalTimeType(_)))); |
| 855 | assert_eq!(TimeZoneName::new(b"CET" )?.as_bytes(), b"CET" ); |
| 856 | assert_eq!(TimeZoneName::new(b"CHADT" )?.as_bytes(), b"CHADT" ); |
| 857 | assert_eq!(TimeZoneName::new(b"abcdefg" )?.as_bytes(), b"abcdefg" ); |
| 858 | assert_eq!(TimeZoneName::new(b"UTC+02" )?.as_bytes(), b"UTC+02" ); |
| 859 | assert_eq!(TimeZoneName::new(b"-1230" )?.as_bytes(), b"-1230" ); |
| 860 | assert!(matches!(TimeZoneName::new("−0330" .as_bytes()), Err(Error::LocalTimeType(_)))); // MINUS SIGN (U+2212) |
| 861 | assert!(matches!(TimeZoneName::new(b" \x00123" ), Err(Error::LocalTimeType(_)))); |
| 862 | assert!(matches!(TimeZoneName::new(b"12345678" ), Err(Error::LocalTimeType(_)))); |
| 863 | assert!(matches!(TimeZoneName::new(b"GMT \0\0\0" ), Err(Error::LocalTimeType(_)))); |
| 864 | |
| 865 | Ok(()) |
| 866 | } |
| 867 | |
| 868 | #[test ] |
| 869 | fn test_time_zone() -> Result<(), Error> { |
| 870 | let utc = LocalTimeType::UTC; |
| 871 | let cet = LocalTimeType::with_offset(3600)?; |
| 872 | |
| 873 | let utc_local_time_types = vec![utc]; |
| 874 | let fixed_extra_rule = TransitionRule::from(cet); |
| 875 | |
| 876 | let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?; |
| 877 | let time_zone_2 = |
| 878 | TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?; |
| 879 | let time_zone_3 = |
| 880 | TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?; |
| 881 | let time_zone_4 = TimeZone::new( |
| 882 | vec![Transition::new(i32::MIN.into(), 0), Transition::new(0, 1)], |
| 883 | vec![utc, cet], |
| 884 | Vec::new(), |
| 885 | Some(fixed_extra_rule), |
| 886 | )?; |
| 887 | |
| 888 | assert_eq!(*time_zone_1.find_local_time_type(0)?, utc); |
| 889 | assert_eq!(*time_zone_2.find_local_time_type(0)?, cet); |
| 890 | |
| 891 | assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc); |
| 892 | assert_eq!(*time_zone_3.find_local_time_type(0)?, utc); |
| 893 | |
| 894 | assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc); |
| 895 | assert_eq!(*time_zone_4.find_local_time_type(0)?, cet); |
| 896 | |
| 897 | let time_zone_err = TimeZone::new( |
| 898 | vec![Transition::new(0, 0)], |
| 899 | utc_local_time_types, |
| 900 | vec![], |
| 901 | Some(fixed_extra_rule), |
| 902 | ); |
| 903 | assert!(time_zone_err.is_err()); |
| 904 | |
| 905 | Ok(()) |
| 906 | } |
| 907 | |
| 908 | #[test ] |
| 909 | fn test_time_zone_from_posix_tz() -> Result<(), Error> { |
| 910 | #[cfg (unix)] |
| 911 | { |
| 912 | // if the TZ var is set, this essentially _overrides_ the |
| 913 | // time set by the localtime symlink |
| 914 | // so just ensure that ::local() acts as expected |
| 915 | // in this case |
| 916 | if let Ok(tz) = std::env::var("TZ" ) { |
| 917 | let time_zone_local = TimeZone::local(Some(tz.as_str()))?; |
| 918 | let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?; |
| 919 | assert_eq!(time_zone_local, time_zone_local_1); |
| 920 | } |
| 921 | |
| 922 | // `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have |
| 923 | // a time zone database, like for example some docker containers. |
| 924 | // In that case skip the test. |
| 925 | if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC" ) { |
| 926 | assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0); |
| 927 | } |
| 928 | } |
| 929 | |
| 930 | assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25" ).is_err()); |
| 931 | assert!(TimeZone::from_posix_tz("" ).is_err()); |
| 932 | |
| 933 | Ok(()) |
| 934 | } |
| 935 | |
| 936 | #[test ] |
| 937 | fn test_leap_seconds() -> Result<(), Error> { |
| 938 | let time_zone = TimeZone::new( |
| 939 | Vec::new(), |
| 940 | vec![LocalTimeType::new(0, false, Some(b"UTC" ))?], |
| 941 | vec![ |
| 942 | LeapSecond::new(78796800, 1), |
| 943 | LeapSecond::new(94694401, 2), |
| 944 | LeapSecond::new(126230402, 3), |
| 945 | LeapSecond::new(157766403, 4), |
| 946 | LeapSecond::new(189302404, 5), |
| 947 | LeapSecond::new(220924805, 6), |
| 948 | LeapSecond::new(252460806, 7), |
| 949 | LeapSecond::new(283996807, 8), |
| 950 | LeapSecond::new(315532808, 9), |
| 951 | LeapSecond::new(362793609, 10), |
| 952 | LeapSecond::new(394329610, 11), |
| 953 | LeapSecond::new(425865611, 12), |
| 954 | LeapSecond::new(489024012, 13), |
| 955 | LeapSecond::new(567993613, 14), |
| 956 | LeapSecond::new(631152014, 15), |
| 957 | LeapSecond::new(662688015, 16), |
| 958 | LeapSecond::new(709948816, 17), |
| 959 | LeapSecond::new(741484817, 18), |
| 960 | LeapSecond::new(773020818, 19), |
| 961 | LeapSecond::new(820454419, 20), |
| 962 | LeapSecond::new(867715220, 21), |
| 963 | LeapSecond::new(915148821, 22), |
| 964 | LeapSecond::new(1136073622, 23), |
| 965 | LeapSecond::new(1230768023, 24), |
| 966 | LeapSecond::new(1341100824, 25), |
| 967 | LeapSecond::new(1435708825, 26), |
| 968 | LeapSecond::new(1483228826, 27), |
| 969 | ], |
| 970 | None, |
| 971 | )?; |
| 972 | |
| 973 | let time_zone_ref = time_zone.as_ref(); |
| 974 | |
| 975 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599))); |
| 976 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600))); |
| 977 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600))); |
| 978 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601))); |
| 979 | |
| 980 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621))); |
| 981 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623))); |
| 982 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624))); |
| 983 | |
| 984 | Ok(()) |
| 985 | } |
| 986 | |
| 987 | #[test ] |
| 988 | fn test_leap_seconds_overflow() -> Result<(), Error> { |
| 989 | let time_zone_err = TimeZone::new( |
| 990 | vec![Transition::new(i64::MIN, 0)], |
| 991 | vec![LocalTimeType::UTC], |
| 992 | vec![LeapSecond::new(0, 1)], |
| 993 | Some(TransitionRule::from(LocalTimeType::UTC)), |
| 994 | ); |
| 995 | assert!(time_zone_err.is_err()); |
| 996 | |
| 997 | let time_zone = TimeZone::new( |
| 998 | vec![Transition::new(i64::MAX, 0)], |
| 999 | vec![LocalTimeType::UTC], |
| 1000 | vec![LeapSecond::new(0, 1)], |
| 1001 | None, |
| 1002 | )?; |
| 1003 | assert!(matches!( |
| 1004 | time_zone.find_local_time_type(i64::MAX), |
| 1005 | Err(Error::FindLocalTimeType(_)) |
| 1006 | )); |
| 1007 | |
| 1008 | Ok(()) |
| 1009 | } |
| 1010 | } |
| 1011 | |