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