Skip to main content

jiff/tz/db/
mod.rs

1use crate::{
2    error::{tz::db::Error as E, Error},
3    tz::TimeZone,
4    util::{sync::Arc, utf8},
5};
6
7mod bundled;
8mod concatenated;
9mod zoneinfo;
10
11/// Returns a copy of the global [`TimeZoneDatabase`].
12///
13/// This is the same database used for convenience routines like
14/// [`Timestamp::in_tz`](crate::Timestamp::in_tz) and parsing routines
15/// for [`Zoned`](crate::Zoned) that need to do IANA time zone identifier
16/// lookups. Basically, whenever an implicit time zone database is needed,
17/// it is *this* copy of the time zone database that is used.
18///
19/// In feature configurations where a time zone database cannot interact with
20/// the file system (like when `std` is not enabled), this returns a database
21/// where every lookup will fail.
22///
23/// # Example
24///
25/// ```
26/// use jiff::tz;
27///
28/// assert!(tz::db().get("Antarctica/Troll").is_ok());
29/// assert!(tz::db().get("does-not-exist").is_err());
30/// ```
31pub fn db() -> &'static TimeZoneDatabase {
32    // #[cfg(any(not(feature = "std"), miri))]
33    #[cfg(not(feature = "std"))]
34    {
35        static NONE: TimeZoneDatabase = TimeZoneDatabase::none();
36        &NONE
37    }
38    // #[cfg(all(feature = "std", not(miri)))]
39    #[cfg(feature = "std")]
40    {
41        use std::sync::OnceLock;
42
43        static DB: OnceLock<TimeZoneDatabase> = OnceLock::new();
44        DB.get_or_init(|| {
45            let db = TimeZoneDatabase::from_env();
46            debug!("initialized global time zone database: {db:?}");
47            db
48        })
49    }
50}
51
52/// A handle to a [IANA Time Zone Database].
53///
54/// A `TimeZoneDatabase` provides a way to lookup [`TimeZone`]s by their
55/// human readable identifiers, such as `America/Los_Angeles` and
56/// `Europe/Warsaw`.
57///
58/// It is rare to need to create or use this type directly. Routines
59/// like zoned datetime parsing and time zone conversion provide
60/// convenience routines for using an implicit global time zone database
61/// by default. This global time zone database is available via
62/// [`jiff::tz::db`](crate::tz::db()`). But lower level parsing routines
63/// such as
64/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
65/// and
66/// [`civil::DateTime::to_zoned`](crate::civil::DateTime::to_zoned) provide a
67/// means to use a custom copy of a `TimeZoneDatabase`.
68///
69/// # Platform behavior
70///
71/// This behavior is subject to change.
72///
73/// On Unix systems, and when the `tzdb-zoneinfo` crate feature is enabled
74/// (which it is by default), Jiff will read the `/usr/share/zoneinfo`
75/// directory for time zone data.
76///
77/// On Windows systems and when the `tzdb-bundle-platform` crate feature is
78/// enabled (which it is by default), _or_ when the `tzdb-bundle-always` crate
79/// feature is enabled, then the `jiff-tzdb` crate will be used to embed the
80/// entire Time Zone Database into the compiled artifact.
81///
82/// On Android systems, and when the `tzdb-concatenated` crate feature is
83/// enabled (which it is by default), Jiff will attempt to read a concatenated
84/// zoneinfo database using the `ANDROID_DATA` or `ANDROID_ROOT` environment
85/// variables.
86///
87/// In general, using `/usr/share/zoneinfo` (or an equivalent) is heavily
88/// preferred in lieu of embedding the database into your compiled artifact.
89/// The reason is because your system copy of the Time Zone Database may be
90/// updated, perhaps a few times a year, and it is better to get seamless
91/// updates through your system rather than needing to wait on a Rust crate
92/// to update and then rebuild your software. The bundling approach should
93/// only be used when there is no plausible alternative. For example, Windows
94/// has no canonical location for a copy of the Time Zone Database. Indeed,
95/// this is why the Cargo configuration of Jiff specifically does not enabled
96/// bundling by default on Unix systems, but does enable it by default on
97/// Windows systems. Of course, if you really do need a copy of the database
98/// bundled, then you can enable the `tzdb-bundle-always` crate feature.
99///
100/// # Cloning
101///
102/// A `TimeZoneDatabase` can be cheaply cloned. It will share a thread safe
103/// cache with other copies of the same `TimeZoneDatabase`.
104///
105/// # Caching
106///
107/// Because looking up a time zone on disk, reading the file into memory
108/// and parsing the time zone transitions out of that file requires
109/// a fair amount of work, a `TimeZoneDatabase` does a fair bit of
110/// caching. This means that the vast majority of calls to, for example,
111/// [`Timestamp::in_tz`](crate::Timestamp::in_tz) don't actually need to hit
112/// disk. It will just find a cached copy of a [`TimeZone`] and return that.
113///
114/// Of course, with caching comes problems of cache invalidation. Invariably,
115/// there are parameters that Jiff uses to manage when the cache should be
116/// invalidated. Jiff tries to emit log messages about this when it happens. If
117/// you find the caching behavior of Jiff to be sub-optimal for your use case,
118/// please create an issue. (The plan is likely to expose some options for
119/// configuring the behavior of a `TimeZoneDatabase`, but I wanted to collect
120/// user feedback first.)
121///
122/// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
123///
124/// # Example: list all available time zones
125///
126/// ```no_run
127/// use jiff::tz;
128///
129/// for tzid in tz::db().available() {
130///     println!("{tzid}");
131/// }
132/// ```
133///
134/// # Example: using multiple time zone databases
135///
136/// Jiff supports opening and using multiple time zone databases by default.
137/// All you need to do is point [`TimeZoneDatabase::from_dir`] to your own
138/// copy of the Time Zone Database, and it will handle the rest.
139///
140/// This example shows how to utilize multiple databases by parsing a datetime
141/// using an older copy of the IANA Time Zone Database. This example leverages
142/// the fact that the 2018 copy of the database preceded Brazil's announcement
143/// that daylight saving time would be abolished. This meant that datetimes
144/// in the future, when parsed with the older copy of the Time Zone Database,
145/// would still follow the old daylight saving time rules. But a mere update of
146/// the database would otherwise change the meaning of the datetime.
147///
148/// This scenario can come up if one stores datetimes in the future. This is
149/// also why the default offset conflict resolution strategy when parsing zoned
150/// datetimes is [`OffsetConflict::Reject`](crate::tz::OffsetConflict::Reject),
151/// which prevents one from silently re-interpreting datetimes to a different
152/// timestamp.
153///
154/// ```no_run
155/// use jiff::{fmt::temporal::DateTimeParser, tz::{self, TimeZoneDatabase}};
156///
157/// static PARSER: DateTimeParser = DateTimeParser::new();
158///
159/// // Open a version of tzdb from before Brazil announced its abolition
160/// // of daylight saving time.
161/// let tzdb2018 = TimeZoneDatabase::from_dir("path/to/tzdb-2018b")?;
162/// // Open the system tzdb.
163/// let tzdb = tz::db();
164///
165/// // Parse the same datetime string with the same parser, but using two
166/// // different versions of tzdb.
167/// let dt = "2020-01-15T12:00[America/Sao_Paulo]";
168/// let zdt2018 = PARSER.parse_zoned_with(&tzdb2018, dt)?;
169/// let zdt = PARSER.parse_zoned_with(tzdb, dt)?;
170///
171/// // Before DST was abolished, 2020-01-15 was in DST, which corresponded
172/// // to UTC offset -02. Since DST rules applied to datetimes in the
173/// // future, the 2018 version of tzdb would lead one to interpret
174/// // 2020-01-15 as being in DST.
175/// assert_eq!(zdt2018.offset(), tz::offset(-2));
176/// // But DST was abolished in 2019, which means that 2020-01-15 was no
177/// // no longer in DST. So after a tzdb update, the same datetime as above
178/// // now has a different offset.
179/// assert_eq!(zdt.offset(), tz::offset(-3));
180///
181/// // So if you try to parse a datetime serialized from an older copy of
182/// // tzdb, you'll get an error under the default configuration because
183/// // of `OffsetConflict::Reject`. This would succeed if you parsed it
184/// // using tzdb2018!
185/// assert!(PARSER.parse_zoned_with(tzdb, zdt2018.to_string()).is_err());
186///
187/// # Ok::<(), Box<dyn std::error::Error>>(())
188/// ```
189#[derive(Clone)]
190pub struct TimeZoneDatabase {
191    inner: Option<Arc<Kind>>,
192}
193
194#[derive(Debug)]
195// Needed for core-only "dumb" `Arc`.
196#[cfg_attr(not(feature = "alloc"), derive(Clone))]
197enum Kind {
198    ZoneInfo(zoneinfo::Database),
199    Concatenated(concatenated::Database),
200    Bundled(bundled::Database),
201}
202
203impl TimeZoneDatabase {
204    /// Returns a database for which all time zone lookups fail.
205    ///
206    /// # Example
207    ///
208    /// ```
209    /// use jiff::tz::TimeZoneDatabase;
210    ///
211    /// let db = TimeZoneDatabase::none();
212    /// assert_eq!(db.available().count(), 0);
213    /// ```
214    pub const fn none() -> TimeZoneDatabase {
215        TimeZoneDatabase { inner: None }
216    }
217
218    /// Returns a time zone database initialized from the current environment.
219    ///
220    /// This routine never fails, but it may not be able to find a copy of
221    /// your Time Zone Database. When this happens, log messages (with some
222    /// at least at the `WARN` level) will be emitted. They can be viewed by
223    /// installing a [`log`] compatible logger such as [`env_logger`].
224    ///
225    /// Typically, one does not need to call this routine directly. Instead,
226    /// it's done for you as part of [`jiff::tz::db`](crate::tz::db()).
227    /// This does require Jiff's `std` feature to be enabled though. So for
228    /// example, you might use this constructor when the features `alloc`
229    /// and `tzdb-bundle-always` are enabled to get access to a bundled
230    /// copy of the IANA time zone database. (Accessing the system copy at
231    /// `/usr/share/zoneinfo` requires `std`.)
232    ///
233    /// Beware that calling this constructor will create a new _distinct_
234    /// handle from the one returned by `jiff::tz::db` with its own cache.
235    ///
236    /// [`log`]: https://docs.rs/log
237    /// [`env_logger`]: https://docs.rs/env_logger
238    ///
239    /// # Platform behavior
240    ///
241    /// When the `TZDIR` environment variable is set, this will attempt to
242    /// open the Time Zone Database at the directory specified. Otherwise,
243    /// this will search a list of predefined directories for a system
244    /// installation of the Time Zone Database. Typically, it's found at
245    /// `/usr/share/zoneinfo`.
246    ///
247    /// On Windows systems, under the default crate configuration, this will
248    /// return an embedded copy of the Time Zone Database since Windows does
249    /// not have a canonical installation of the Time Zone Database.
250    pub fn from_env() -> TimeZoneDatabase {
251        // On Android, try the concatenated database first, since that's
252        // typically what is used.
253        //
254        // Overall this logic might be sub-optimal. Like, does it really make
255        // sense to check for the zoneinfo or concatenated database on non-Unix
256        // platforms? Probably not to be honest. But these should only be
257        // executed ~once generally, so it doesn't seem like a big deal to try.
258        // And trying makes things a little more flexible I think.
259        #[cfg(not(miri))]
260        {
261            if cfg!(target_os = "android") {
262                let db = concatenated::Database::from_env();
263                if !db.is_definitively_empty() {
264                    return TimeZoneDatabase::new(Kind::Concatenated(db));
265                }
266
267                let db = zoneinfo::Database::from_env();
268                if !db.is_definitively_empty() {
269                    return TimeZoneDatabase::new(Kind::ZoneInfo(db));
270                }
271            } else {
272                let db = zoneinfo::Database::from_env();
273                if !db.is_definitively_empty() {
274                    return TimeZoneDatabase::new(Kind::ZoneInfo(db));
275                }
276
277                let db = concatenated::Database::from_env();
278                if !db.is_definitively_empty() {
279                    return TimeZoneDatabase::new(Kind::Concatenated(db));
280                }
281            }
282        }
283
284        let db = bundled::Database::new();
285        if !db.is_definitively_empty() {
286            return TimeZoneDatabase::new(Kind::Bundled(db));
287        }
288
289        warn!(
290            "could not find zoneinfo, concatenated tzdata or \
291             bundled time zone database",
292        );
293        TimeZoneDatabase::none()
294    }
295
296    /// Returns a time zone database initialized from the given directory.
297    ///
298    /// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
299    /// a copy of the Time Zone Database at the directory given. And if it
300    /// fails to find one at that directory, then an error is returned.
301    ///
302    /// Basically, you should use this when you need to use a _specific_
303    /// copy of the Time Zone Database, and use `TimeZoneDatabase::from_env`
304    /// when you just want Jiff to try and "do the right thing for you."
305    ///
306    /// # Errors
307    ///
308    /// This returns an error if the given directory does not contain a valid
309    /// copy of the Time Zone Database. Generally, this means a directory with
310    /// at least one valid TZif file.
311    #[cfg(feature = "std")]
312    pub fn from_dir<P: AsRef<std::path::Path>>(
313        path: P,
314    ) -> Result<TimeZoneDatabase, Error> {
315        let path = path.as_ref();
316        let db = zoneinfo::Database::from_dir(path)?;
317        if db.is_definitively_empty() {
318            warn!(
319                "could not find zoneinfo data at directory {path}",
320                path = path.display(),
321            );
322        }
323        Ok(TimeZoneDatabase::new(Kind::ZoneInfo(db)))
324    }
325
326    /// Returns a time zone database initialized from a path pointing to a
327    /// concatenated `tzdata` file. This type of format is only known to be
328    /// found on Android environments. The specific format for this file isn't
329    /// defined formally anywhere, but Jiff parses the same format supported
330    /// by the [Android Platform].
331    ///
332    /// Unlike [`TimeZoneDatabase::from_env`], this always attempts to look for
333    /// a copy of the Time Zone Database at the path given. And if it
334    /// fails to find one at that path, then an error is returned.
335    ///
336    /// Basically, you should use this when you need to use a _specific_
337    /// copy of the Time Zone Database in its concatenated format, and use
338    /// `TimeZoneDatabase::from_env` when you just want Jiff to try and "do the
339    /// right thing for you." (`TimeZoneDatabase::from_env` will attempt to
340    /// automatically detect the presence of a system concatenated `tzdata`
341    /// file on Android.)
342    ///
343    /// # Errors
344    ///
345    /// This returns an error if the given path does not contain a valid
346    /// copy of the concatenated Time Zone Database.
347    ///
348    /// [Android Platform]: https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/util/ZoneInfoDB.java
349    #[cfg(feature = "std")]
350    pub fn from_concatenated_path<P: AsRef<std::path::Path>>(
351        path: P,
352    ) -> Result<TimeZoneDatabase, Error> {
353        let path = path.as_ref();
354        let db = concatenated::Database::from_path(path)?;
355        if db.is_definitively_empty() {
356            warn!(
357                "could not find concatenated tzdata in file {path}",
358                path = path.display(),
359            );
360        }
361        Ok(TimeZoneDatabase::new(Kind::Concatenated(db)))
362    }
363
364    /// Returns a time zone database initialized from the bundled copy of
365    /// the [IANA Time Zone Database].
366    ///
367    /// While this API is always available, in order to get a non-empty
368    /// database back, this requires that one of the crate features
369    /// `tzdb-bundle-always` or `tzdb-bundle-platform` is enabled. In the
370    /// latter case, the bundled database is only available on platforms known
371    /// to lack a system copy of the IANA Time Zone Database (i.e., non-Unix
372    /// systems).
373    ///
374    /// This routine is infallible, but it may return a database
375    /// that is definitively empty if the bundled data is not
376    /// available. To query whether the data is empty or not, use
377    /// [`TimeZoneDatabase::is_definitively_empty`].
378    ///
379    /// # Data generation
380    ///
381    /// The data in this crate comes from the [IANA Time Zone Database] "data
382    /// only" distribution. [`jiff-cli`] is used to first compile the release
383    /// into binary TZif data using the `zic` compiler, and secondly, converts
384    /// the binary data into a flattened and de-duplicated representation that
385    /// is embedded into this crate's source code.
386    ///
387    /// The conversion into the TZif binary data uses the following settings:
388    ///
389    /// * The "rearguard" data is used (see below).
390    /// * The binary data itself is compiled using the "slim" format. Which
391    ///   effectively means that the TZif data primarily only uses explicit
392    ///   time zone transitions for historical data and POSIX time zones for
393    ///   current time zone transition rules. This doesn't have any impact
394    ///   on the actual results. The reason that there are "slim" and "fat"
395    ///   formats is to support legacy applications that can't deal with
396    ///   POSIX time zones. For example, `/usr/share/zoneinfo` on my modern
397    ///   Archlinux installation (2025-02-27) is in the "fat" format.
398    ///
399    /// The reason that rearguard data is used is a bit more subtle and has
400    /// to do with a difference in how the IANA Time Zone Database treats its
401    /// internal "daylight saving time" flag and what people in the "real
402    /// world" consider "daylight saving time." For example, in the standard
403    /// distribution of the IANA Time Zone Database, `Europe/Dublin` has its
404    /// daylight saving time flag set to _true_ during Winter and set to
405    /// _false_ during Summer. The actual time shifts are the same as, e.g.,
406    /// `Europe/London`, but which one is actually labeled "daylight saving
407    /// time" is not.
408    ///
409    /// The IANA Time Zone Database does this for `Europe/Dublin`, presumably,
410    /// because _legally_, time during the Summer in Ireland is called `Irish
411    /// Standard Time`, and time during the Winter is called `Greenwich Mean
412    /// Time`. These legal names are reversed from what is typically the case,
413    /// where "standard" time is during the Winter and daylight saving time is
414    /// during the Summer. The IANA Time Zone Database implements this tweak in
415    /// legal language via a "negative daylight saving time offset." This is
416    /// somewhat odd, and some consumers of the IANA Time Zone Database cannot
417    /// handle it. Thus, the rearguard format was born for, seemingly, legacy
418    /// programs.
419    ///
420    /// Jiff can handle negative daylight saving time offsets just fine,
421    /// but we use the rearguard format anyway so that the underlying data
422    /// more accurately reflects on-the-ground reality for humans living in
423    /// `Europe/Dublin`. In particular, using the rearguard data enables
424    /// [localization of time zone names] to be done correctly.
425    ///
426    /// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
427    /// [`jiff-cli`]: https://github.com/BurntSushi/jiff/tree/master/crates/jiff-cli
428    /// [localization of time zone names]: https://github.com/BurntSushi/jiff/issues/258
429    pub fn bundled() -> TimeZoneDatabase {
430        let db = bundled::Database::new();
431        if db.is_definitively_empty() {
432            warn!("could not find embedded/bundled zoneinfo");
433        }
434        TimeZoneDatabase::new(Kind::Bundled(db))
435    }
436
437    /// Creates a new DB from the internal kind.
438    fn new(kind: Kind) -> TimeZoneDatabase {
439        TimeZoneDatabase { inner: Some(Arc::new(kind)) }
440    }
441
442    /// Returns a [`TimeZone`] corresponding to the IANA time zone identifier
443    /// given.
444    ///
445    /// The lookup is performed without regard to ASCII case.
446    ///
447    /// To see a list of all available time zone identifiers for this database,
448    /// use [`TimeZoneDatabase::available`].
449    ///
450    /// It is guaranteed that if the given time zone name is case insensitively
451    /// equivalent to `UTC`, then the time zone returned will be equivalent to
452    /// `TimeZone::UTC`. Similarly for `Etc/Unknown` and `TimeZone::unknown()`.
453    ///
454    /// # Example
455    ///
456    /// ```
457    /// use jiff::tz;
458    ///
459    /// let tz = tz::db().get("america/NEW_YORK")?;
460    /// assert_eq!(tz.iana_name(), Some("America/New_York"));
461    ///
462    /// # Ok::<(), Box<dyn std::error::Error>>(())
463    /// ```
464    pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
465        let inner = self
466            .inner
467            .as_deref()
468            .ok_or_else(|| E::failed_time_zone_no_database_configured(name))?;
469        match *inner {
470            Kind::ZoneInfo(ref db) => {
471                if let Some(tz) = db.get(name) {
472                    trace!("found time zone `{name}` in {db:?}", db = self);
473                    return Ok(tz);
474                }
475            }
476            Kind::Concatenated(ref db) => {
477                if let Some(tz) = db.get(name) {
478                    trace!("found time zone `{name}` in {db:?}", db = self);
479                    return Ok(tz);
480                }
481            }
482            Kind::Bundled(ref db) => {
483                if let Some(tz) = db.get(name) {
484                    trace!("found time zone `{name}` in {db:?}", db = self);
485                    return Ok(tz);
486                }
487            }
488        }
489        Err(Error::from(E::failed_time_zone(name)))
490    }
491
492    /// Returns a list of all available time zone identifiers from this
493    /// database.
494    ///
495    /// Note that time zone identifiers are more of a machine readable
496    /// abstraction and not an end user level abstraction. Still, users
497    /// comfortable with configuring their system's default time zone through
498    /// IANA time zone identifiers are probably comfortable interacting with
499    /// the identifiers returned here.
500    ///
501    /// # Example
502    ///
503    /// ```no_run
504    /// use jiff::tz;
505    ///
506    /// for tzid in tz::db().available() {
507    ///     println!("{tzid}");
508    /// }
509    /// ```
510    pub fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
511        let Some(inner) = self.inner.as_deref() else {
512            return TimeZoneNameIter::empty();
513        };
514        match *inner {
515            Kind::ZoneInfo(ref db) => db.available(),
516            Kind::Concatenated(ref db) => db.available(),
517            Kind::Bundled(ref db) => db.available(),
518        }
519    }
520
521    /// Resets the internal cache of this database.
522    ///
523    /// Subsequent interactions with this database will need to re-read time
524    /// zone data from disk.
525    ///
526    /// It might be useful to call this if you know the time zone database
527    /// has changed on disk and want to force Jiff to re-load it immediately
528    /// without spawning a new process or waiting for Jiff's internal cache
529    /// invalidation heuristics to kick in.
530    pub fn reset(&self) {
531        let Some(inner) = self.inner.as_deref() else { return };
532        match *inner {
533            Kind::ZoneInfo(ref db) => db.reset(),
534            Kind::Concatenated(ref db) => db.reset(),
535            Kind::Bundled(ref db) => db.reset(),
536        }
537    }
538
539    /// Returns true if it is known that this time zone database is empty.
540    ///
541    /// When this returns true, it is guaranteed that all
542    /// [`TimeZoneDatabase::get`] calls will fail, and that
543    /// [`TimeZoneDatabase::available`] will always return an empty iterator.
544    ///
545    /// Note that if this returns false, it is still possible for this database
546    /// to be empty.
547    ///
548    /// # Example
549    ///
550    /// ```
551    /// use jiff::tz::TimeZoneDatabase;
552    ///
553    /// let db = TimeZoneDatabase::none();
554    /// assert!(db.is_definitively_empty());
555    /// ```
556    pub fn is_definitively_empty(&self) -> bool {
557        let Some(inner) = self.inner.as_deref() else { return true };
558        match *inner {
559            Kind::ZoneInfo(ref db) => db.is_definitively_empty(),
560            Kind::Concatenated(ref db) => db.is_definitively_empty(),
561            Kind::Bundled(ref db) => db.is_definitively_empty(),
562        }
563    }
564}
565
566impl core::fmt::Debug for TimeZoneDatabase {
567    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
568        f.write_str("TimeZoneDatabase(")?;
569        let Some(inner) = self.inner.as_deref() else {
570            return f.write_str("unavailable)");
571        };
572        match *inner {
573            Kind::ZoneInfo(ref db) => core::fmt::Debug::fmt(db, f)?,
574            Kind::Concatenated(ref db) => core::fmt::Debug::fmt(db, f)?,
575            Kind::Bundled(ref db) => core::fmt::Debug::fmt(db, f)?,
576        }
577        f.write_str(")")
578    }
579}
580
581/// An iterator over the time zone identifiers in a [`TimeZoneDatabase`].
582///
583/// This iterator is created by [`TimeZoneDatabase::available`].
584///
585/// There are no guarantees about the order in which this iterator yields
586/// time zone identifiers.
587///
588/// The lifetime parameter corresponds to the lifetime of the
589/// `TimeZoneDatabase` from which this iterator was created.
590#[derive(Clone, Debug)]
591pub struct TimeZoneNameIter<'d> {
592    #[cfg(feature = "alloc")]
593    it: alloc::vec::IntoIter<TimeZoneName<'d>>,
594    #[cfg(not(feature = "alloc"))]
595    it: core::iter::Empty<TimeZoneName<'d>>,
596}
597
598impl<'d> TimeZoneNameIter<'d> {
599    /// Creates a time zone name iterator that never yields any elements.
600    fn empty() -> TimeZoneNameIter<'d> {
601        #[cfg(feature = "alloc")]
602        {
603            TimeZoneNameIter { it: alloc::vec::Vec::new().into_iter() }
604        }
605        #[cfg(not(feature = "alloc"))]
606        {
607            TimeZoneNameIter { it: core::iter::empty() }
608        }
609    }
610
611    /// Creates a time zone name iterator that yields the elements from the
612    /// iterator given. (They are collected into a `Vec`.)
613    #[cfg(feature = "alloc")]
614    fn from_iter(
615        it: impl Iterator<Item = impl Into<alloc::string::String>>,
616    ) -> TimeZoneNameIter<'d> {
617        let names: alloc::vec::Vec<TimeZoneName<'d>> =
618            it.map(|name| TimeZoneName::new(name.into())).collect();
619        TimeZoneNameIter { it: names.into_iter() }
620    }
621}
622
623impl<'d> Iterator for TimeZoneNameIter<'d> {
624    type Item = TimeZoneName<'d>;
625
626    fn next(&mut self) -> Option<TimeZoneName<'d>> {
627        self.it.next()
628    }
629}
630
631/// A name for a time zone yield by the [`TimeZoneNameIter`] iterator.
632///
633/// The iterator is created by [`TimeZoneDatabase::available`].
634///
635/// The lifetime parameter corresponds to the lifetime of the
636/// `TimeZoneDatabase` from which this name was created.
637#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
638pub struct TimeZoneName<'d> {
639    /// The lifetime of the tzdb.
640    ///
641    /// We don't currently use this, but it could be quite useful if we ever
642    /// adopt a "compile time" tzdb like what `chrono-tz` has. Then we could
643    /// return strings directly from the embedded data. Or perhaps a "compile
644    /// time" TZif or some such.
645    lifetime: core::marker::PhantomData<&'d str>,
646    #[cfg(feature = "alloc")]
647    name: alloc::string::String,
648    #[cfg(not(feature = "alloc"))]
649    name: core::convert::Infallible,
650}
651
652impl<'d> TimeZoneName<'d> {
653    /// Returns a new time zone name from the string given.
654    ///
655    /// The lifetime returned is inferred according to the caller's context.
656    #[cfg(feature = "alloc")]
657    fn new(name: alloc::string::String) -> TimeZoneName<'d> {
658        TimeZoneName { lifetime: core::marker::PhantomData, name }
659    }
660
661    /// Returns this time zone name as a borrowed string.
662    ///
663    /// Note that the lifetime of the string returned is tied to `self`,
664    /// which may be shorter than the lifetime `'d` of the originating
665    /// `TimeZoneDatabase`.
666    #[inline]
667    pub fn as_str<'a>(&'a self) -> &'a str {
668        #[cfg(feature = "alloc")]
669        {
670            self.name.as_str()
671        }
672        #[cfg(not(feature = "alloc"))]
673        {
674            // Can never be reached because `TimeZoneName` cannot currently
675            // be constructed in core-only environments.
676            unreachable!()
677        }
678    }
679}
680
681impl<'d> core::fmt::Display for TimeZoneName<'d> {
682    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
683        f.write_str(self.as_str())
684    }
685}
686
687/// Checks if `name` is a "special" time zone and returns one if so.
688///
689/// This is limited to special constants that should have consistent values
690/// across time zone database implementations. For example, `UTC`.
691fn special_time_zone(name: &str) -> Option<TimeZone> {
692    if utf8::cmp_ignore_ascii_case("utc", name).is_eq() {
693        return Some(TimeZone::UTC);
694    }
695    if utf8::cmp_ignore_ascii_case("etc/unknown", name).is_eq() {
696        return Some(TimeZone::unknown());
697    }
698    None
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    /// This tests that the size of a time zone database is kept at a single
706    /// word.
707    ///
708    /// I think it would probably be okay to make this bigger if we had a
709    /// good reason to, but it seems sensible to put a road-block to avoid
710    /// accidentally increasing its size.
711    #[test]
712    fn time_zone_database_size() {
713        #[cfg(feature = "alloc")]
714        {
715            let word = core::mem::size_of::<usize>();
716            assert_eq!(word, core::mem::size_of::<TimeZoneDatabase>());
717        }
718        // A `TimeZoneDatabase` in core-only is vapid.
719        #[cfg(not(feature = "alloc"))]
720        {
721            assert_eq!(1, core::mem::size_of::<TimeZoneDatabase>());
722        }
723    }
724
725    /// Time zone databases should always return `TimeZone::UTC` if the time
726    /// zone is known to be UTC.
727    ///
728    /// Regression test for: https://github.com/BurntSushi/jiff/issues/346
729    #[test]
730    fn bundled_returns_utc_constant() {
731        let db = TimeZoneDatabase::bundled();
732        if db.is_definitively_empty() {
733            return;
734        }
735        assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
736        assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
737        assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
738        assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
739
740        // Also, similarly, for `Etc/Unknown`.
741        assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
742        assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
743    }
744
745    /// Time zone databases should always return `TimeZone::UTC` if the time
746    /// zone is known to be UTC.
747    ///
748    /// Regression test for: https://github.com/BurntSushi/jiff/issues/346
749    #[cfg(all(feature = "std", not(miri)))]
750    #[test]
751    fn zoneinfo_returns_utc_constant() {
752        let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
753            return;
754        };
755        if db.is_definitively_empty() {
756            return;
757        }
758        assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
759        assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
760        assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
761        assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
762
763        // Also, similarly, for `Etc/Unknown`.
764        assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
765        assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
766    }
767
768    /// This checks that our zoneinfo database never returns a time zone
769    /// identifier that isn't presumed to correspond to a real and valid
770    /// TZif file in the tzdb.
771    ///
772    /// This test was added when I optimized the initialized of Jiff's zoneinfo
773    /// database. Originally, it did a directory traversal along with a 4-byte
774    /// read of every file in the directory to check if the file was TZif or
775    /// something else. This turned out to be quite slow on slow file systems.
776    /// I rejiggered it so that the reads of every file were removed. But this
777    /// meant we could have loaded a name from a file that wasn't TZif into
778    /// our in-memory cache.
779    ///
780    /// For doing a single time zone lookup, this isn't a problem, since we
781    /// have to read the TZif data anyway. If it's invalid, then we just
782    /// return `None` and log a warning. No big deal.
783    ///
784    /// But for the `TimeZoneDatabase::available()` API, we were previously
785    /// just returning a list of names under the presumption that every such
786    /// name corresponds to a valid TZif file. This test checks that we don't
787    /// emit junk. (Which was in practice accomplished to moving the 4-byte
788    /// read to when we call `TimeZoneDatabase::available()`.)
789    ///
790    /// Ref: https://github.com/BurntSushi/jiff/issues/366
791    #[cfg(all(feature = "std", not(miri)))]
792    #[test]
793    fn zoneinfo_available_returns_only_tzif() {
794        use alloc::{
795            collections::BTreeSet,
796            string::{String, ToString},
797        };
798
799        let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
800            return;
801        };
802        if db.is_definitively_empty() {
803            return;
804        }
805        let names: BTreeSet<String> =
806            db.available().map(|n| n.as_str().to_string()).collect();
807        // Not all zoneinfo directories are created equal. Some have more or
808        // less junk than others. So just try a few things.
809        let should_be_absent = [
810            "leapseconds",
811            "tzdata.zi",
812            "leap-seconds.list",
813            "SECURITY",
814            "zone1970.tab",
815            "iso3166.tab",
816            "zonenow.tab",
817            "zone.tab",
818        ];
819        for name in should_be_absent {
820            assert!(
821                !names.contains(name),
822                "found `{name}` in time zone list, but it shouldn't be there",
823            );
824        }
825    }
826}