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