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