Skip to main content

embedded_tz/
lib.rs

1//! `tzfile` is a [`chrono::TimeZone`] implementation using the system tz
2//! database. It can parse compiled (binary) time zone files inside
3//! `/usr/share/zoneinfo` into time zone objects for `chrono`.
4#![no_std]
5use alloc::rc::Rc;
6use alloc::sync::Arc;
7use alloc::vec;
8use byteorder::{ByteOrder, BE};
9use chrono::{FixedOffset, LocalResult, NaiveDate, NaiveDateTime, TimeZone};
10
11extern crate alloc;
12use alloc::{boxed::Box, string::ToString, vec::Vec};
13use core::cmp::Ordering;
14use core::fmt;
15use core::ops::Deref;
16use core::str::from_utf8;
17
18#[cfg(feature = "bundled-tzdb")]
19/// Helpers for reading and parsing the bundled tz database.
20pub mod bundled;
21
22/// `Oz` ("offset zone") represents a continuous period of time where the offset
23/// from UTC is constant and has the same abbreviation.
24#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
25struct Oz {
26    /// The time zone offset from UTC.
27    offset: FixedOffset,
28    /// The byte index to the time zone abbreviation.
29    name: u8,
30}
31
32impl Oz {
33    /// Converts a UTC timestamp to a local timestamp.
34    fn to_local(self, utc_ts: i64) -> i64 {
35        utc_ts + i64::from(self.offset.local_minus_utc())
36    }
37}
38
39/// Time zone parsed from a tz database file.
40///
41/// When a time zone has complex transition rules, a `Tz` object can be very
42/// large and expensive to clone. As every [`DateTime`](chrono::DateTime)
43/// instant would store a copy of the time zone object, it would be very slow to
44/// support `DateTime<Tz>` directly. Therefore, `Tz` itself does not implement
45/// [`TimeZone`]. Rather, you may use one of the following instead:
46///
47/// * `&'a Tz` — zero cost to clone, but only valid within the lifetime `'a`.
48/// * [`RcTz`] — uses reference counting ([`Rc`]) to support shallow cloning,
49///   but is not thread-safe.
50/// * [`ArcTz`] — uses atomic reference counting ([`Arc`]) to support shallow
51///   cloning, slightly more expensive than [`RcTz`] but is thread-safe.
52///
53/// # Examples
54///
55/// Read the time zone information from the system, and use `&Tz` as `TimeZone`.
56///
57/// ```
58/// # #[cfg(unix)] {
59/// use chrono::{Utc, TimeZone};
60/// use embedded_tz::Tz;
61///
62/// let tz = Tz::named("America/New_York")?;
63/// let dt1 = Utc.with_ymd_and_hms(2019, 3, 10, 6, 45, 0).unwrap();
64/// assert_eq!(dt1.with_timezone(&&tz).to_string(), "2019-03-10 01:45:00 EST");
65/// let dt2 = Utc.with_ymd_and_hms(2019, 3, 10, 7, 15, 0).unwrap();
66/// assert_eq!(dt2.with_timezone(&&tz).to_string(), "2019-03-10 03:15:00 EDT");
67///
68/// # } Ok::<_, embedded_tz::Error>(())
69/// ```
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub struct Tz {
72    /// Null-terminated time zone abbreviations concatenated as a single string.
73    names: Box<str>,
74    /// Sorted array of UTC-to-local conversion results. The first item of the
75    /// tuple is the starting UTC timestamp, and second item is the
76    /// corresponding local offset.
77    utc_to_local: Box<[(i64, Oz)]>,
78    /// Sorted array of local-to-UTC conversion results. The first item of the
79    /// tuple is the starting local timestamp, and second item is the
80    /// corresponding local offset (with ambiguity/invalid time handling).
81    local_to_utc: Box<[(i64, LocalResult<Oz>)]>,
82}
83
84/// Extracts the lower bound index from the result of standard `binary_search`.
85fn to_lower_bound(bsr: Result<usize, usize>) -> usize {
86    bsr.unwrap_or_else(|i| i - 1)
87}
88
89impl Tz {
90    /// Obtains an [`Oz`] at the local date.
91    ///
92    /// Returns `None` if the date is invalid.
93    fn oz_from_local_date(&self, local_date: NaiveDate) -> Option<Oz> {
94        #[allow(deprecated)]
95        let min_ts = local_date.and_hms(0, 0, 0).timestamp();
96        self.oz_from_local_timestamp(min_ts)
97            .earliest()
98            .or_else(|| self.oz_from_local_timestamp(min_ts + 86399).earliest())
99    }
100
101    /// Obtains the [`Oz`] at the local timestamp.
102    fn oz_from_local_timestamp(&self, local_ts: i64) -> LocalResult<Oz> {
103        let index = to_lower_bound(
104            self.local_to_utc
105                .binary_search_by(|&(local, _)| local.cmp(&local_ts)),
106        );
107        self.local_to_utc[index].1
108    }
109
110    /// Obtains the [`Oz`] at the UTC timestamp.
111    fn oz_from_utc_timestamp(&self, timestamp: i64) -> Oz {
112        let index = to_lower_bound(
113            self.utc_to_local
114                .binary_search_by(|&(utc, _)| utc.cmp(&timestamp)),
115        );
116        self.utc_to_local[index].1
117    }
118}
119
120/// Offset type associated with [`Tz`].
121#[derive(Clone, Hash, PartialEq, Eq)]
122pub struct Offset<T> {
123    oz: Oz,
124    tz: T,
125}
126
127impl<T: Clone + Deref<Target = Tz>> chrono::Offset for Offset<T> {
128    fn fix(&self) -> FixedOffset {
129        self.oz.offset
130    }
131}
132
133impl<T: Deref<Target = Tz>> fmt::Display for Offset<T> {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        let suffix = &self.tz.names[usize::from(self.oz.name)..];
136        let len = suffix.find('\0').unwrap_or(suffix.len());
137        f.write_str(&suffix[..len])
138    }
139}
140
141impl<T: Deref<Target = Tz>> fmt::Debug for Offset<T> {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        fmt::Display::fmt(self, f)
144    }
145}
146
147/// Reference-counted time zone.
148///
149/// This type is equivalent to [`Rc`]`<`[`Tz`]`>`, but needed to workaround
150/// Rust's coherence rule to implement [`TimeZone`].
151#[derive(Clone, Debug, Hash, Eq, PartialEq)]
152pub struct RcTz(pub Rc<Tz>);
153
154impl RcTz {
155    /// Wraps an existing [`Tz`] object in this reference-counted container.
156    pub fn new(tz: Tz) -> Self {
157        Self(Rc::new(tz))
158    }
159
160    /// Reads and parses a system time zone.
161    ///
162    /// Equivalent to calling [`Tz::named()`] and wraps the result in this
163    /// reference-counted container.
164    ///
165    /// This function is currently only supported when the `bundled-tzdb` feature is enabled.
166    #[cfg(feature = "bundled-tzdb")]
167    pub fn named(name: &str) -> Result<Self, Error> {
168        bundled::parse(name).map(Self::new)
169    }
170}
171
172impl Deref for RcTz {
173    type Target = Tz;
174    fn deref(&self) -> &Tz {
175        &self.0
176    }
177}
178
179impl From<Tz> for RcTz {
180    fn from(tz: Tz) -> Self {
181        Self::new(tz)
182    }
183}
184
185/// Atomic reference-counted time zone.
186///
187/// This type is equivalent to [`Arc`]`<`[`Tz`]`>`, but needed to workaround
188/// Rust's coherence rule to implement [`TimeZone`].
189#[derive(Clone, Debug, Hash, Eq, PartialEq)]
190pub struct ArcTz(pub Arc<Tz>);
191
192impl ArcTz {
193    /// Wraps an existing [`Tz`] object in this atomic reference-counted
194    /// container.
195    pub fn new(tz: Tz) -> Self {
196        Self(Arc::new(tz))
197    }
198
199    /// Reads and parses a system time zone.
200    ///
201    /// Equivalent to calling [`Tz::named()`] and wraps the result in this
202    /// atomic reference-counted container.
203    ///
204    /// This function is currently only supported when the `bundled-tzdb` feature is enabled.
205    #[cfg(feature = "bundled-tzdb")]
206    pub fn named(name: &str) -> Result<Self, Error> {
207        bundled::parse(name).map(Self::new)
208    }
209}
210
211impl Deref for ArcTz {
212    type Target = Tz;
213    fn deref(&self) -> &Tz {
214        &self.0
215    }
216}
217
218impl From<Tz> for ArcTz {
219    fn from(tz: Tz) -> Self {
220        Self::new(tz)
221    }
222}
223
224macro_rules! implements_time_zone {
225    () => {
226        type Offset = Offset<Self>;
227
228        fn from_offset(offset: &Self::Offset) -> Self {
229            Self::clone(&offset.tz)
230        }
231
232        fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
233            #[allow(deprecated)]
234            Offset {
235                oz: self.oz_from_utc_timestamp(utc.timestamp()),
236                tz: self.clone(),
237            }
238        }
239
240        fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
241            #[allow(deprecated)]
242            self.offset_from_utc_datetime(&utc.and_hms(12, 0, 0))
243        }
244
245        fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<Self::Offset> {
246            if let Some(oz) = self.oz_from_local_date(*local) {
247                LocalResult::Single(Offset {
248                    oz,
249                    tz: self.clone(),
250                })
251            } else {
252                LocalResult::None
253            }
254        }
255
256        fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset> {
257            #[allow(deprecated)]
258            let timestamp = local.timestamp();
259            self.oz_from_local_timestamp(timestamp).map(|oz| Offset {
260                oz,
261                tz: self.clone(),
262            })
263        }
264    };
265}
266
267impl TimeZone for &Tz {
268    implements_time_zone!();
269}
270
271impl TimeZone for RcTz {
272    implements_time_zone!();
273}
274
275impl TimeZone for ArcTz {
276    implements_time_zone!();
277}
278
279/// Parse errors from [`Tz::parse()`].
280#[derive(Debug, PartialEq, Eq, Clone)]
281pub enum Error {
282    /// The source bytes is too short to parse the header.
283    HeaderTooShort,
284    /// The source does not start with the correct magic string (`"TZif"`).
285    InvalidMagic,
286    /// Unsupported tzfile version. Currently we only support versions 2 and 3.
287    UnsupportedVersion,
288    /// The lengths of several related arrays in the file are not the same,
289    /// making the file invalid.
290    InconsistentTypeCount,
291    /// The tzfile contains no time zone information.
292    NoTypes,
293    /// The time zone offset exceeds ±86400s (1 day).
294    OffsetOverflow,
295    /// Some time zone abbreviations are not valid UTF-8.
296    NonUtf8Abbr,
297    /// The source bytes is too short to parse the content.
298    DataTooShort,
299    /// Invalid time zone file name.
300    InvalidTimeZoneFileName,
301    /// The time zone transition type is invalid.
302    InvalidType,
303    /// Name offset is out of bounds.
304    NameOffsetOutOfBounds,
305}
306
307impl fmt::Display for Error {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        f.write_str("tzfile error: ")?;
310        f.write_str(match self {
311            Error::HeaderTooShort => "header too short",
312            Error::InvalidMagic => "invalid magic",
313            Error::UnsupportedVersion => "unsupported version",
314            Error::InconsistentTypeCount => "inconsistent type count",
315            Error::NoTypes => "no types",
316            Error::OffsetOverflow => "time zone offset overflow",
317            Error::NonUtf8Abbr => "non-UTF-8 time zone abbreviations",
318            Error::DataTooShort => "data too short",
319            Error::InvalidTimeZoneFileName => "invalid time zone file name",
320            Error::InvalidType => "invalid time zone transition type",
321            Error::NameOffsetOutOfBounds => "name offset out of bounds",
322        })
323    }
324}
325
326static MAGIC: [u8; 4] = *b"TZif";
327
328struct Header {
329    tzh_ttisgmtcnt: usize,
330    tzh_ttisstdcnt: usize,
331    tzh_leapcnt: usize,
332    tzh_timecnt: usize,
333    tzh_typecnt: usize,
334    tzh_charcnt: usize,
335}
336
337impl Header {
338    /// Parses the header from the prefix of the `source`.
339    fn parse(source: &[u8]) -> Result<Self, Error> {
340        if source.len() < Self::HEADER_LEN {
341            return Err(Error::HeaderTooShort);
342        }
343        if source[..4] != MAGIC {
344            return Err(Error::InvalidMagic);
345        }
346        match source[4] {
347            b'2' | b'3' => {}
348            _ => return Err(Error::UnsupportedVersion),
349        }
350        let tzh_ttisgmtcnt = BE::read_u32(&source[20..24]) as usize;
351        let tzh_ttisstdcnt = BE::read_u32(&source[24..28]) as usize;
352        let tzh_leapcnt = BE::read_u32(&source[28..32]) as usize;
353        let tzh_timecnt = BE::read_u32(&source[32..36]) as usize;
354        let tzh_typecnt = BE::read_u32(&source[36..40]) as usize;
355        let tzh_charcnt = BE::read_u32(&source[40..44]) as usize;
356
357        if (tzh_ttisgmtcnt != 0 && tzh_ttisgmtcnt != tzh_typecnt)
358            || (tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt)
359        {
360            return Err(Error::InconsistentTypeCount);
361        }
362        if tzh_typecnt == 0 {
363            return Err(Error::NoTypes);
364        }
365
366        Ok(Header {
367            tzh_ttisgmtcnt,
368            tzh_ttisstdcnt,
369            tzh_leapcnt,
370            tzh_timecnt,
371            tzh_typecnt,
372            tzh_charcnt,
373        })
374    }
375
376    /// The length of the header.
377    const HEADER_LEN: usize = 44;
378
379    /// The length of the content, when `time_t` is represented by type `L`.
380    fn data_len<L>(&self) -> usize {
381        self.tzh_timecnt * (size_of::<L>() + 1)
382            + self.tzh_typecnt * 6
383            + self.tzh_charcnt
384            + self.tzh_leapcnt * (size_of::<L>() + 4)
385            + self.tzh_ttisstdcnt
386            + self.tzh_ttisgmtcnt
387    }
388
389    /// Parses the time zone information from the prefix of `content`.
390    fn parse_content(&self, content: &[u8]) -> Result<Tz, Error> {
391        // Obtain the byte indices where each array ends.
392        let trans_encoded_end = self.tzh_timecnt * 8;
393        let local_time_types_end = trans_encoded_end + self.tzh_timecnt;
394        let infos_end = local_time_types_end + self.tzh_typecnt * 6;
395        let abbr_end = infos_end + self.tzh_charcnt;
396
397        // Collect the timezone abbreviations.
398        let names = from_utf8(&content[infos_end..abbr_end]).map_err(|_| Error::NonUtf8Abbr)?;
399
400        // Collect the timezone infos.
401        let ozs = content[local_time_types_end..infos_end]
402            .chunks_exact(6)
403            .map(|encoded| {
404                let seconds = BE::read_i32(&encoded[..4]);
405                let offset = FixedOffset::east_opt(seconds).ok_or(Error::OffsetOverflow)?;
406                let name = encoded[5];
407                if usize::from(name) >= names.len() {
408                    return Err(Error::NameOffsetOutOfBounds);
409                }
410                Ok(Oz { offset, name })
411            })
412            .collect::<Result<Vec<_>, Error>>()?;
413
414        // Collect the transition times.
415        let trans_encoded = &content[..trans_encoded_end];
416        let local_time_types = &content[trans_encoded_end..local_time_types_end];
417
418        let mut prev_oz = ozs[0];
419
420        let mut utc_to_local = Vec::with_capacity(self.tzh_timecnt + 1);
421        utc_to_local.push((i64::MIN, prev_oz));
422        for (te, &ltt) in trans_encoded.chunks_exact(8).zip(local_time_types) {
423            let oz = *ozs.get(usize::from(ltt)).ok_or(Error::InvalidType)?;
424            let timestamp = BE::read_i64(te);
425            utc_to_local.push((timestamp, oz));
426        }
427
428        let mut local_to_utc = Vec::with_capacity(self.tzh_timecnt * 2 + 1);
429        local_to_utc.push((i64::MIN, LocalResult::Single(prev_oz)));
430        for &(utc_ts, cur_oz) in &utc_to_local[1..] {
431            let prev_local_ts = prev_oz.to_local(utc_ts);
432            let cur_local_ts = cur_oz.to_local(utc_ts);
433            match prev_local_ts.cmp(&cur_local_ts) {
434                Ordering::Less => {
435                    local_to_utc.push((prev_local_ts, LocalResult::None));
436                    local_to_utc.push((cur_local_ts, LocalResult::Single(cur_oz)));
437                }
438                Ordering::Equal => {
439                    local_to_utc.push((cur_local_ts, LocalResult::Single(cur_oz)));
440                }
441                Ordering::Greater => {
442                    local_to_utc.push((cur_local_ts, LocalResult::Ambiguous(prev_oz, cur_oz)));
443                    local_to_utc.push((prev_local_ts, LocalResult::Single(cur_oz)));
444                }
445            };
446            prev_oz = cur_oz;
447        }
448
449        Ok(Tz {
450            names: names.into(),
451            utc_to_local: utc_to_local.into_boxed_slice(),
452            local_to_utc: local_to_utc.into_boxed_slice(),
453        })
454    }
455}
456
457impl Tz {
458    /// Parses the content of the tz database file.
459    ///
460    /// This crate can only recognize version 2 and 3 of the tz database. Like
461    /// `chrono`, leap second information is ignored. The embedded POSIX TZ
462    /// string, which describes non-hard-coded transition rules in the far
463    /// future, is also not handled.
464    ///
465    /// # Examples
466    ///
467    /// Read a file into bytes and then parse it.
468    ///
469    /// ```rust
470    /// # #[cfg(unix)] {
471    /// use embedded_tz::Tz;
472    ///
473    /// let content = std::fs::read("/usr/share/zoneinfo/Etc/UTC").unwrap();
474    /// let tz = Tz::parse("Etc/UTC", &content)?;
475    ///
476    /// # } Ok::<_, embedded_tz::Error>(())
477    /// ```
478    pub fn parse(_name: &str, source: &[u8]) -> Result<Self, Error> {
479        let header = Header::parse(source)?;
480        let first_ver_len = Header::HEADER_LEN + header.data_len::<i32>();
481        let source = source.get(first_ver_len..).ok_or(Error::DataTooShort)?;
482        let header = Header::parse(source)?;
483        let second_ver_len = Header::HEADER_LEN + header.data_len::<i64>();
484        if source.len() < second_ver_len {
485            return Err(Error::DataTooShort);
486        }
487        header.parse_content(&source[Header::HEADER_LEN..])
488    }
489
490    /// Reads and parses a system time zone.
491    ///
492    /// This function is equivalent to reading `/usr/share/zoneinfo/{name}` and
493    /// the constructs a time zone via [`parse()`](Tz::parse).
494    ///
495    /// This function is currently only supported when the `bundled-tzdb` feature is enabled.
496    #[cfg(feature = "bundled-tzdb")]
497    pub fn named(name: &str) -> Result<Self, Error> {
498        bundled::parse(name).map_err(|_| Error::InvalidTimeZoneFileName)
499    }
500}
501
502impl From<chrono::Utc> for Tz {
503    fn from(_: chrono::Utc) -> Self {
504        #[allow(deprecated)]
505        let oz = Oz {
506            offset: FixedOffset::east(0),
507            name: 0,
508        };
509        Self {
510            names: "UTC\0".into(),
511            utc_to_local: vec![(i64::MIN, oz)].into_boxed_slice(),
512            local_to_utc: vec![(i64::MIN, LocalResult::Single(oz))].into_boxed_slice(),
513        }
514    }
515}
516
517impl From<FixedOffset> for Tz {
518    fn from(offset: FixedOffset) -> Self {
519        let mut name = offset.to_string();
520        name.push('\0');
521        let oz = Oz { offset, name: 0 };
522        Self {
523            names: name.into_boxed_str(),
524            utc_to_local: vec![(i64::MIN, oz)].into_boxed_slice(),
525            local_to_utc: vec![(i64::MIN, LocalResult::Single(oz))].into_boxed_slice(),
526        }
527    }
528}
529
530#[cfg(test)]
531#[allow(deprecated)]
532mod tests {
533    use super::*;
534    use chrono::TimeZone;
535
536    #[test]
537    fn tz_from_fixed_offset() {
538        let utc_tz = Tz::from(chrono::Utc);
539        let fixed_1_tz = Tz::from(chrono::FixedOffset::east(3600));
540
541        let dt_0 = (&utc_tz).ymd(1000, 1, 1).and_hms(15, 0, 0);
542        let dt_1_converted = dt_0.with_timezone(&&fixed_1_tz);
543        let dt_1_expected = (&fixed_1_tz).ymd(1000, 1, 1).and_hms(16, 0, 0);
544        assert_eq!(dt_1_converted, dt_1_expected);
545    }
546
547    #[test]
548    fn parse_valid_contents() {
549        let contents: Vec<&[u8]> = vec![
550            // #0
551            b"TZif2\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\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\0\0\
552              TZif2\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\0\0\0\0\x01\0\0\0\x01\0\0\0\x04\xF8\0\0\0\0\0\0\0\0\0\0\0\0\0\0UTC\0\0\0\nUTC0\n",
553
554            // #1
555            b"TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\
556              TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\nUTC0\n",
557        ];
558
559        for (i, content) in contents.into_iter().enumerate() {
560            assert!(
561                Tz::parse("__valid__", content).is_ok(),
562                "test #{}: should be able to parse {:x?}",
563                i,
564                content
565            );
566        }
567    }
568
569    #[test]
570    fn parse_invalid_contents() {
571        let contents: Vec<(&[u8], Error)> = vec![
572            (
573                // #0
574                b"",
575                Error::HeaderTooShort,
576            ),
577            (
578                // #1
579                b"not valid",
580                Error::HeaderTooShort,
581            ),
582            (
583                // #2
584                b"TZif",
585                Error::HeaderTooShort,
586            ),
587            (
588                // #3
589                b"TZif\0",
590                Error::HeaderTooShort,
591            ),
592            (
593                // #4
594                b"file with invalid magic should produce error",
595                Error::InvalidMagic,
596            ),
597            (
598                // #5
599                b"TZifxxxxxxxxxxxxxxxx\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
600                Error::UnsupportedVersion,
601            ),
602            (
603                // #6
604                b"TZif1xxxxxxxxxxxxxxx\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
605                Error::UnsupportedVersion,
606            ),
607            (
608                // #7
609                b"TZif3xxxxxxxxxxxxxxx\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
610                Error::NoTypes,
611            ),
612            (
613                // #8
614                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x02\0\0\0\0\0\0\0\0\0\0\0\x03\0\0\0\0",
615                Error::InconsistentTypeCount,
616            ),
617            (
618                // #9
619                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x02\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\0",
620                Error::InconsistentTypeCount,
621            ),
622            (
623                // #10
624                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\0",
625                Error::InconsistentTypeCount,
626            ),
627            (
628                // #11
629                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0",
630                Error::DataTooShort,
631            ),
632            (
633                // #12
634                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0xxxxxxxx",
635                Error::HeaderTooShort,
636            ),
637            (
638                // #13
639                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0xxxxxxxxfile with invalid magic should produce error",
640                Error::InvalidMagic,
641            ),
642            (
643                // #14
644                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0xxxxxxxxTZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0",
645                Error::DataTooShort,
646            ),
647            (
648                // #15
649                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0xxxxxxxxTZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0aaaaaaaa",
650                Error::OffsetOverflow,
651            ),
652            (
653                // #16
654                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0xxxxxxxxTZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0aaaaaa",
655                Error::NameOffsetOutOfBounds,
656            ),
657            (
658                // #17
659                b"TZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0xxxxxxxxTZif2xxxxxxxxxxxxxxx\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x02tttttttti\0\0aaa\0A\0aa",
660                Error::InvalidType,
661            ),
662        ];
663
664        for (i, (content, error)) in contents.into_iter().enumerate() {
665            assert_eq!(
666                Tz::parse("__invalid__", content),
667                Err(error),
668                "test #{}: should not be able to parse {:x?}",
669                i,
670                content
671            );
672        }
673    }
674
675    #[test]
676    #[cfg(feature = "bundled-tzdb")]
677    fn invalid_timezone_name() {
678        let err = Tz::named("invalid_timezone_name").unwrap_err();
679        assert_eq!(err, Error::InvalidTimeZoneFileName);
680    }
681
682    #[test]
683    #[cfg(feature = "bundled-tzdb")]
684    fn rctz_arctz() {
685        let tz = Tz::named("Europe/London").unwrap();
686        let rctz = RcTz::named("Europe/London").unwrap();
687        let arctz = ArcTz::named("Europe/London").unwrap();
688        let rctz2 = RcTz::new(tz.clone());
689        let arctz2 = ArcTz::new(tz.clone());
690        assert_eq!(tz, *rctz);
691        assert_eq!(tz, *arctz);
692        assert_eq!(rctz, rctz2);
693        assert_eq!(arctz, arctz2);
694    }
695}
696
697#[cfg(all(test, feature = "bundled-tzdb"))]
698#[allow(deprecated)]
699mod chrono_tz_tests {
700    use super::Tz;
701    use alloc::string::ToString;
702    use chrono::{Duration, TimeZone};
703    use lazy_static::lazy_static;
704    extern crate std;
705    use std::format;
706
707    lazy_static! {
708        static ref ADELAIDE: Tz = Tz::named("Australia/Adelaide").unwrap();
709        static ref APIA: Tz = Tz::named("Pacific/Apia").unwrap();
710        static ref AMSTERDAM: Tz = Tz::named("Europe/Amsterdam").unwrap();
711        static ref BERLIN: Tz = Tz::named("Europe/Berlin").unwrap();
712        static ref DANMARKSHAVN: Tz = Tz::named("America/Danmarkshavn").unwrap();
713        static ref DHAKA: Tz = Tz::named("Asia/Dhaka").unwrap();
714        static ref EASTERN: Tz = Tz::named("US/Eastern").unwrap();
715        static ref GAZA: Tz = Tz::named("Asia/Gaza").unwrap();
716        static ref JERUSALEM: Tz = Tz::named("Asia/Jerusalem").unwrap();
717        static ref KATHMANDU: Tz = Tz::named("Asia/Kathmandu").unwrap();
718        static ref LONDON: Tz = Tz::named("Europe/London").unwrap();
719        static ref MOSCOW: Tz = Tz::named("Europe/Moscow").unwrap();
720        static ref NEW_YORK: Tz = Tz::named("America/New_York").unwrap();
721        static ref TAHITI: Tz = Tz::named("Pacific/Tahiti").unwrap();
722        static ref NOUMEA: Tz = Tz::named("Pacific/Noumea").unwrap();
723        static ref TRIPOLI: Tz = Tz::named("Africa/Tripoli").unwrap();
724        static ref UTC: Tz = Tz::named("Etc/UTC").unwrap();
725        static ref VILNIUS: Tz = Tz::named("Europe/Vilnius").unwrap();
726        static ref WARSAW: Tz = Tz::named("Europe/Warsaw").unwrap();
727    }
728
729    #[test]
730    fn london_to_berlin() {
731        let dt = (&*LONDON).ymd(2016, 10, 8).and_hms(17, 0, 0);
732        let converted = dt.with_timezone(&&*BERLIN);
733        let expected = (&*BERLIN).ymd(2016, 10, 8).and_hms(18, 0, 0);
734        assert_eq!(converted, expected);
735    }
736
737    #[test]
738    fn us_eastern_dst_commutativity() {
739        let dt = (&*UTC).ymd(2002, 4, 7).and_hms(7, 0, 0);
740        for days in -420..720 {
741            let dt1 = (dt.clone() + Duration::days(days)).with_timezone(&&*EASTERN);
742            let dt2 = dt.with_timezone(&&*EASTERN) + Duration::days(days);
743            assert_eq!(dt1, dt2);
744        }
745    }
746
747    #[test]
748    fn warsaw_tz_name() {
749        let dt = (&*UTC).ymd(1915, 8, 4).and_hms(22, 35, 59);
750        assert_eq!(dt.with_timezone(&&*WARSAW).format("%Z").to_string(), "WMT");
751        let dt = dt + Duration::seconds(1);
752        assert_eq!(dt.with_timezone(&&*WARSAW).format("%Z").to_string(), "CET");
753    }
754
755    #[test]
756    fn vilnius_utc_offset() {
757        let dt = (&*UTC)
758            .ymd(1916, 12, 31)
759            .and_hms(22, 35, 59)
760            .with_timezone(&&*VILNIUS);
761        assert_eq!(dt, (&*VILNIUS).ymd(1916, 12, 31).and_hms(23, 59, 59));
762        let dt = dt + Duration::seconds(1);
763        assert_eq!(dt, (&*VILNIUS).ymd(1917, 1, 1).and_hms(0, 11, 36));
764    }
765
766    #[test]
767    fn victorian_times() {
768        let dt = (&*UTC)
769            .ymd(1847, 12, 1)
770            .and_hms(0, 1, 14)
771            .with_timezone(&&*LONDON);
772        assert_eq!(dt, (&&*LONDON).ymd(1847, 11, 30).and_hms(23, 59, 59));
773        let dt = dt + Duration::seconds(1);
774        assert_eq!(dt, (&&*LONDON).ymd(1847, 12, 1).and_hms(0, 1, 15));
775    }
776
777    #[test]
778    fn london_dst() {
779        let dt = (&*LONDON).ymd(2016, 3, 10).and_hms(5, 0, 0);
780        let later = dt + Duration::days(180);
781        let expected = (&*LONDON).ymd(2016, 9, 6).and_hms(6, 0, 0);
782        assert_eq!(later, expected);
783    }
784
785    #[test]
786    fn international_date_line_change() {
787        let dt = (&*UTC)
788            .ymd(2011, 12, 30)
789            .and_hms(9, 59, 59)
790            .with_timezone(&&*APIA);
791        assert_eq!(dt, (&*APIA).ymd(2011, 12, 29).and_hms(23, 59, 59));
792        let dt = dt + Duration::seconds(1);
793        assert_eq!(dt, (&*APIA).ymd(2011, 12, 31).and_hms(0, 0, 0));
794    }
795
796    #[test]
797    fn negative_offset_with_minutes_and_seconds() {
798        let dt = (&*UTC)
799            .ymd(1900, 1, 1)
800            .and_hms(12, 0, 0)
801            .with_timezone(&&*DANMARKSHAVN);
802        assert_eq!(dt, (&*DANMARKSHAVN).ymd(1900, 1, 1).and_hms(10, 45, 20));
803    }
804
805    #[test]
806    fn monotonicity() {
807        let mut dt = (&*NOUMEA).ymd(1800, 1, 1).and_hms(12, 0, 0);
808        for _ in 0..24 * 356 * 400 {
809            let new = dt.clone() + Duration::hours(1);
810            assert!(new > dt);
811            assert!(new.with_timezone(&&*UTC) > dt.with_timezone(&&*UTC));
812            dt = new;
813        }
814    }
815
816    fn test_inverse<T: TimeZone>(tz: T, begin: i32, end: i32) {
817        for y in begin..end {
818            for d in 1..366 {
819                for h in 0..24 {
820                    for m in 0..60 {
821                        let dt = (&*UTC).yo(y, d).and_hms(h, m, 0);
822                        let with_tz = dt.with_timezone(&tz);
823                        let utc = with_tz.with_timezone(&&*UTC);
824                        assert_eq!(dt, utc);
825                    }
826                }
827            }
828        }
829    }
830
831    #[test]
832    fn inverse_london() {
833        test_inverse(&*LONDON, 1989, 1994);
834    }
835
836    #[test]
837    fn inverse_dhaka() {
838        test_inverse(&*DHAKA, 1995, 2000);
839    }
840
841    #[test]
842    fn inverse_apia() {
843        test_inverse(&*APIA, 2011, 2012);
844    }
845
846    #[test]
847    fn inverse_tahiti() {
848        test_inverse(&*TAHITI, 1911, 1914);
849    }
850
851    #[test]
852    fn string_representation() {
853        let dt = (&*UTC)
854            .ymd(2000, 9, 1)
855            .and_hms(12, 30, 15)
856            .with_timezone(&&*ADELAIDE);
857        assert_eq!(dt.to_string(), "2000-09-01 22:00:15 ACST");
858        assert_eq!(format!("{:?}", dt), "2000-09-01T22:00:15ACST");
859        assert_eq!(dt.to_rfc3339(), "2000-09-01T22:00:15+09:30");
860        assert_eq!(format!("{}", dt), "2000-09-01 22:00:15 ACST");
861    }
862
863    #[test]
864    fn tahiti() {
865        let dt = (&*UTC)
866            .ymd(1912, 10, 1)
867            .and_hms(9, 58, 16)
868            .with_timezone(&&*TAHITI);
869        let before = dt.clone() - Duration::hours(1);
870        assert_eq!(before, (&*TAHITI).ymd(1912, 9, 30).and_hms(23, 0, 0));
871        let after = dt + Duration::hours(1);
872        assert_eq!(after, (&*TAHITI).ymd(1912, 10, 1).and_hms(0, 58, 16));
873    }
874
875    #[test]
876    #[should_panic]
877    fn nonexistent_time() {
878        let _ = (&*LONDON).ymd(2016, 3, 27).and_hms(1, 30, 0);
879    }
880
881    #[test]
882    #[should_panic]
883    fn nonexistent_time_2() {
884        let _ = (&*LONDON).ymd(2016, 3, 27).and_hms(1, 0, 0);
885    }
886
887    #[test]
888    fn time_exists() {
889        let _ = (&*LONDON).ymd(2016, 3, 27).and_hms(2, 0, 0);
890    }
891
892    #[test]
893    #[should_panic]
894    fn ambiguous_time() {
895        let _ = (&*LONDON).ymd(2016, 10, 30).and_hms(1, 0, 0);
896    }
897
898    #[test]
899    #[should_panic]
900    fn ambiguous_time_2() {
901        let _ = (&*LONDON).ymd(2016, 10, 30).and_hms(1, 30, 0);
902    }
903
904    #[test]
905    #[should_panic]
906    fn ambiguous_time_3() {
907        let _ = (&*MOSCOW).ymd(2014, 10, 26).and_hms(1, 30, 0);
908    }
909
910    #[test]
911    #[should_panic]
912    fn ambiguous_time_4() {
913        let _ = (&*MOSCOW).ymd(2014, 10, 26).and_hms(1, 0, 0);
914    }
915
916    #[test]
917    fn unambiguous_time() {
918        let _ = (&*LONDON).ymd(2016, 10, 30).and_hms(2, 0, 0);
919    }
920
921    #[test]
922    fn unambiguous_time_2() {
923        let _ = (&*MOSCOW).ymd(2014, 10, 26).and_hms(2, 0, 0);
924    }
925
926    // the numberphile tests
927
928    #[test]
929    fn test_london_5_days_ago_to_new_york() {
930        let from = (&*LONDON).ymd(2013, 12, 25).and_hms(14, 0, 0);
931        let to = (&*NEW_YORK).ymd(2013, 12, 30).and_hms(14, 0, 0);
932        assert_eq!(
933            to.signed_duration_since(from),
934            Duration::days(5) + Duration::hours(5)
935        );
936    }
937
938    #[test]
939    fn london_to_australia() {
940        // at the time Tom was speaking, Adelaide was 10 1/2 hours ahead
941        // many other parts of Australia use different time zones
942        let from = (&*LONDON).ymd(2013, 12, 25).and_hms(14, 0, 0);
943        let to = (&*ADELAIDE).ymd(2013, 12, 30).and_hms(14, 0, 0);
944        assert_eq!(
945            to.signed_duration_since(from),
946            Duration::days(5) - Duration::minutes(630)
947        );
948    }
949
950    #[test]
951    fn london_to_nepal() {
952        // note Tom gets this wrong, it's 5 3/4 hours as he is speaking
953        let from = (&*LONDON).ymd(2013, 12, 25).and_hms(14, 0, 0);
954        let to = (&*KATHMANDU).ymd(2013, 12, 30).and_hms(14, 0, 0);
955        assert_eq!(
956            to.signed_duration_since(from),
957            Duration::days(5) - Duration::minutes(345)
958        );
959    }
960
961    #[test]
962    fn autumn() {
963        let from = (&*LONDON).ymd(2013, 10, 25).and_hms(12, 0, 0);
964        let to = (&*LONDON).ymd(2013, 11, 1).and_hms(12, 0, 0);
965        assert_eq!(
966            to.signed_duration_since(from),
967            Duration::days(7) + Duration::hours(1)
968        );
969    }
970
971    #[test]
972    fn earlier_daylight_savings_in_new_york() {
973        let from = (&*NEW_YORK).ymd(2013, 10, 25).and_hms(12, 0, 0);
974        let to = (&*NEW_YORK).ymd(2013, 11, 1).and_hms(12, 0, 0);
975        assert_eq!(to.signed_duration_since(from), Duration::days(7));
976    }
977
978    #[test]
979    fn southern_hemisphere_clocks_forward() {
980        let from = (&*ADELAIDE).ymd(2013, 10, 1).and_hms(12, 0, 0);
981        let to = (&*ADELAIDE).ymd(2013, 11, 1).and_hms(12, 0, 0);
982        assert_eq!(
983            to.signed_duration_since(from),
984            Duration::days(31) - Duration::hours(1)
985        );
986    }
987
988    #[test]
989    fn samoa_skips_a_day() {
990        let from = (&*APIA).ymd(2011, 12, 29).and_hms(12, 0, 0);
991        let to = (&*APIA).ymd(2011, 12, 31).and_hms(12, 0, 0);
992        assert_eq!(to.signed_duration_since(from), Duration::days(1));
993    }
994
995    #[test]
996    fn double_bst() {
997        let from = (&*LONDON).ymd(1942, 6, 1).and_hms(12, 0, 0);
998        let to = (&*UTC).ymd(1942, 6, 1).and_hms(12, 0, 0);
999        assert_eq!(to.signed_duration_since(from), Duration::hours(2));
1000    }
1001
1002    #[test]
1003    fn libya_2013() {
1004        // Libya actually put their clocks *forward* in 2013, but not in any other year
1005        let from = (&*TRIPOLI).ymd(2012, 3, 1).and_hms(12, 0, 0);
1006        let to = (&*TRIPOLI).ymd(2012, 4, 1).and_hms(12, 0, 0);
1007        assert_eq!(to.signed_duration_since(from), Duration::days(31));
1008
1009        let from = (&*TRIPOLI).ymd(2013, 3, 1).and_hms(12, 0, 0);
1010        let to = (&*TRIPOLI).ymd(2013, 4, 1).and_hms(12, 0, 0);
1011        assert_eq!(
1012            to.signed_duration_since(from),
1013            Duration::days(31) - Duration::hours(1)
1014        );
1015
1016        let from = (&*TRIPOLI).ymd(2014, 3, 1).and_hms(12, 0, 0);
1017        let to = (&*TRIPOLI).ymd(2014, 4, 1).and_hms(12, 0, 0);
1018        assert_eq!(to.signed_duration_since(from), Duration::days(31));
1019    }
1020
1021    #[test]
1022    fn israel_palestine() {
1023        let from = (&*JERUSALEM).ymd(2016, 10, 29).and_hms(12, 0, 0);
1024        let to = (&*GAZA).ymd(2016, 10, 29).and_hms(12, 0, 0);
1025        assert_eq!(to.signed_duration_since(from), Duration::hours(1));
1026    }
1027
1028    #[test]
1029    fn leapsecond() {
1030        let from = (&*UTC).ymd(2016, 6, 30).and_hms(23, 59, 59);
1031        let to = (&*UTC).ymd(2016, 6, 30).and_hms_milli(23, 59, 59, 1000);
1032        assert_eq!(to.signed_duration_since(from), Duration::seconds(1));
1033    }
1034}