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