chrono/offset/local/tz_info/
timezone.rs

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::MappedLocalTime<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::MappedLocalTime<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 (MappedLocalTime::Ambiguous) or Skipping (MappedLocalTime::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::MappedLocalTime::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::MappedLocalTime::Ambiguous(prev, after_ltt));
255                            } else {
256                                return Ok(crate::MappedLocalTime::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::MappedLocalTime::Single(prev));
264                        } else if local_leap_time == transition_end {
265                            if prev.ut_offset < after_ltt.ut_offset {
266                                return Ok(crate::MappedLocalTime::Ambiguous(prev, after_ltt));
267                            } else {
268                                return Ok(crate::MappedLocalTime::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::MappedLocalTime::Single(prev));
277                        } else if local_leap_time < transition_end {
278                            return Ok(crate::MappedLocalTime::None);
279                        } else if local_leap_time == transition_end {
280                            return Ok(crate::MappedLocalTime::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::MappedLocalTime::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.as_ref();
618        if path.is_absolute() {
619            return Ok(File::open(path)?);
620        }
621
622        for folder in &ZONE_INFO_DIRECTORIES {
623            if let Ok(file) = File::open(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}