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}