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