jiff/tz/db/zoneinfo/enabled.rs
1use alloc::{
2 string::{String, ToString},
3 vec,
4 vec::Vec,
5};
6
7use std::{
8 ffi::OsStr,
9 fs::File,
10 io::Read,
11 path::{Path, PathBuf},
12 sync::{
13 atomic::{AtomicUsize, Ordering},
14 Arc, RwLock,
15 },
16 time::Duration,
17};
18
19use crate::{
20 error::{err, Error},
21 timestamp::Timestamp,
22 tz::{
23 db::special_time_zone, tzif::is_possibly_tzif, TimeZone,
24 TimeZoneNameIter,
25 },
26 util::{self, cache::Expiration, parse, utf8},
27};
28
29const DEFAULT_TTL: Duration = Duration::new(5 * 60, 0);
30
31#[cfg(unix)]
32static ZONEINFO_DIRECTORIES: &[&str] =
33 &["/usr/share/zoneinfo", "/usr/share/lib/zoneinfo", "/etc/zoneinfo"];
34
35// In non-Unix environments, there is (as of 2025-05-17) no standard location
36// for the zoneinfo database. And we specifically do not search the Unix-style
37// directories because this can have weird and undesirable effects on Windows.
38//
39// Ref https://github.com/BurntSushi/jiff/issues/376
40#[cfg(not(unix))]
41static ZONEINFO_DIRECTORIES: &[&str] = &[];
42
43pub(crate) struct Database {
44 dir: Option<PathBuf>,
45 names: Option<ZoneInfoNames>,
46 zones: RwLock<CachedZones>,
47}
48
49impl Database {
50 pub(crate) fn from_env() -> Database {
51 if let Some(tzdir) = std::env::var_os("TZDIR") {
52 let tzdir = PathBuf::from(tzdir);
53 trace!("opening zoneinfo database at TZDIR={}", tzdir.display());
54 match Database::from_dir(&tzdir) {
55 Ok(db) => return db,
56 Err(_err) => {
57 // This is a WARN because it represents a failure to
58 // satisfy a more direct request, which should be louder
59 // than failures related to auto-detection.
60 warn!("failed opening TZDIR={}: {_err}", tzdir.display());
61 // fall through to attempt default directories
62 }
63 }
64 }
65 for dir in ZONEINFO_DIRECTORIES {
66 let tzdir = Path::new(dir);
67 trace!("opening zoneinfo database at {}", tzdir.display());
68 match Database::from_dir(&tzdir) {
69 Ok(db) => return db,
70 Err(_err) => {
71 trace!("failed opening {}: {_err}", tzdir.display());
72 }
73 }
74 }
75 debug!(
76 "could not find zoneinfo database at any of the following \
77 paths: {}",
78 ZONEINFO_DIRECTORIES.join(", "),
79 );
80 Database::none()
81 }
82
83 pub(crate) fn from_dir(dir: &Path) -> Result<Database, Error> {
84 let names = Some(ZoneInfoNames::new(dir)?);
85 let zones = RwLock::new(CachedZones::new());
86 Ok(Database { dir: Some(dir.to_path_buf()), names, zones })
87 }
88
89 /// Creates a "dummy" zoneinfo database in which all lookups fail.
90 pub(crate) fn none() -> Database {
91 let dir = None;
92 let names = None;
93 let zones = RwLock::new(CachedZones::new());
94 Database { dir, names, zones }
95 }
96
97 pub(crate) fn reset(&self) {
98 let mut zones = self.zones.write().unwrap();
99 if let Some(ref names) = self.names {
100 names.reset();
101 }
102 zones.reset();
103 }
104
105 pub(crate) fn get(&self, query: &str) -> Option<TimeZone> {
106 if let Some(tz) = special_time_zone(query) {
107 return Some(tz);
108 }
109 // If we couldn't build any time zone names, then every lookup will
110 // fail. So just bail now.
111 let names = self.names.as_ref()?;
112 // The fast path is when the query matches a pre-existing unexpired
113 // time zone.
114 {
115 let zones = self.zones.read().unwrap();
116 if let Some(czone) = zones.get(query) {
117 if !czone.is_expired() {
118 trace!(
119 "for time zone query `{query}`, \
120 found cached zone `{}` \
121 (expiration={}, last_modified={:?})",
122 czone.tz.diagnostic_name(),
123 czone.expiration,
124 czone.last_modified,
125 );
126 return Some(czone.tz.clone());
127 }
128 }
129 }
130 // At this point, one of three possible cases is true:
131 //
132 // 1. The given query does not match any time zone in this database.
133 // 2. A time zone exists, but isn't cached.
134 // 3. A zime exists and is cached, but needs to be revalidated.
135 //
136 // While (3) is probably the common case since our TTLs are pretty
137 // short, both (2) and (3) require write access. Thus we rule out (1)
138 // before acquiring a write lock on the entire database. Plus, we'll
139 // need the zone info for case (2) and possibly for (3) if cache
140 // revalidation fails.
141 //
142 // I feel kind of bad about all this because it seems to me like there
143 // is too much work being done while holding on to the write lock.
144 // In particular, it seems like bad juju to do any I/O of any kind
145 // while holding any lock at all. I think I could design something
146 // that avoids doing I/O while holding a lock, but it seems a lot more
147 // complicated. (And what happens if the I/O becomes outdated by the
148 // time you acquire the lock?)
149 let info = names.get(query)?;
150 let mut zones = self.zones.write().unwrap();
151 let ttl = zones.ttl;
152 match zones.get_zone_index(query) {
153 Ok(i) => {
154 let czone = &mut zones.zones[i];
155 if czone.revalidate(&info, ttl) {
156 // Metadata on the file didn't change, so we assume the
157 // file hasn't either.
158 return Some(czone.tz.clone());
159 }
160 // Revalidation failed. Re-read the TZif data.
161 let czone = match CachedTimeZone::new(&info, zones.ttl) {
162 Ok(czone) => czone,
163 Err(_err) => {
164 warn!(
165 "failed to re-cache time zone from file {}: {_err}",
166 info.inner.full.display(),
167 );
168 return None;
169 }
170 };
171 let tz = czone.tz.clone();
172 zones.zones[i] = czone;
173 Some(tz)
174 }
175 Err(i) => {
176 let czone = match CachedTimeZone::new(&info, ttl) {
177 Ok(czone) => czone,
178 Err(_err) => {
179 warn!(
180 "failed to cache time zone from file {}: {_err}",
181 info.inner.full.display(),
182 );
183 return None;
184 }
185 };
186 let tz = czone.tz.clone();
187 zones.zones.insert(i, czone);
188 Some(tz)
189 }
190 }
191 }
192
193 pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
194 let Some(names) = self.names.as_ref() else {
195 return TimeZoneNameIter::empty();
196 };
197 TimeZoneNameIter::from_iter(names.available().into_iter())
198 }
199
200 pub(crate) fn is_definitively_empty(&self) -> bool {
201 self.names.is_none()
202 }
203}
204
205impl core::fmt::Debug for Database {
206 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
207 write!(f, "ZoneInfo(")?;
208 if let Some(ref dir) = self.dir {
209 write!(f, "{}", dir.display())?;
210 } else {
211 write!(f, "unavailable")?;
212 }
213 write!(f, ")")
214 }
215}
216
217#[derive(Debug)]
218struct CachedZones {
219 zones: Vec<CachedTimeZone>,
220 ttl: Duration,
221}
222
223impl CachedZones {
224 const DEFAULT_TTL: Duration = DEFAULT_TTL;
225
226 fn new() -> CachedZones {
227 CachedZones { zones: vec![], ttl: CachedZones::DEFAULT_TTL }
228 }
229
230 fn get(&self, query: &str) -> Option<&CachedTimeZone> {
231 self.get_zone_index(query).ok().map(|i| &self.zones[i])
232 }
233
234 fn get_zone_index(&self, query: &str) -> Result<usize, usize> {
235 // The common case is that our query matches the time zone name case
236 // sensitively, so check for that first. It's a bit cheaper than doing
237 // a case insensitive search.
238 if let Ok(i) = self
239 .zones
240 .binary_search_by(|zone| zone.name.original().cmp(&query))
241 {
242 return Ok(i);
243 }
244 self.zones.binary_search_by(|zone| {
245 utf8::cmp_ignore_ascii_case(zone.name.lower(), query)
246 })
247 }
248
249 fn reset(&mut self) {
250 self.zones.clear();
251 }
252}
253
254#[derive(Clone, Debug)]
255struct CachedTimeZone {
256 tz: TimeZone,
257 name: ZoneInfoName,
258 expiration: Expiration,
259 last_modified: Option<Timestamp>,
260}
261
262impl CachedTimeZone {
263 /// Create a new cached time zone.
264 ///
265 /// The `info` says which time zone to create and where to find it. The
266 /// `ttl` says how long the cached time zone should minimally remain fresh
267 /// for.
268 fn new(
269 info: &ZoneInfoName,
270 ttl: Duration,
271 ) -> Result<CachedTimeZone, Error> {
272 fn imp(
273 info: &ZoneInfoName,
274 ttl: Duration,
275 ) -> Result<CachedTimeZone, Error> {
276 let path = info.path();
277 let mut file =
278 File::open(path).map_err(|e| Error::io(e).path(path))?;
279 let mut data = vec![];
280 file.read_to_end(&mut data)
281 .map_err(|e| Error::io(e).path(path))?;
282 let tz = TimeZone::tzif(&info.inner.original, &data)
283 .map_err(|e| e.path(path))?;
284 let name = info.clone();
285 let last_modified = util::fs::last_modified_from_file(path, &file);
286 let expiration = Expiration::after(ttl);
287 Ok(CachedTimeZone { tz, name, expiration, last_modified })
288 }
289
290 let result = imp(info, ttl);
291 info.set_validity(result.is_ok());
292 result
293 }
294
295 /// Returns true if this time zone has gone stale and should, at minimum,
296 /// be revalidated.
297 fn is_expired(&self) -> bool {
298 self.expiration.is_expired()
299 }
300
301 /// Attempts to revalidate this cached time zone.
302 ///
303 /// Upon successful revalidation (that is, the cached time zone is still
304 /// fresh and okay to use), this returns true. Otherwise, the cached time
305 /// zone should be considered stale and must be re-created.
306 ///
307 /// Note that technically another layer of revalidation could be done.
308 /// For example, we could keep a checksum of the TZif data, and only
309 /// consider rebuilding the time zone when the checksum changes. But I
310 /// think the last modified metadata will in practice be good enough, and
311 /// parsing a TZif file should be quite fast.
312 fn revalidate(&mut self, info: &ZoneInfoName, ttl: Duration) -> bool {
313 // If we started with no last modified timestamp, then I guess we
314 // should always fail revalidation? I suppose a case could be made to
315 // do the opposite: always pass revalidation.
316 let Some(old_last_modified) = self.last_modified else {
317 trace!(
318 "revalidation for {} failed because old last modified time \
319 is unavailable",
320 info.inner.full.display(),
321 );
322 return false;
323 };
324 let Some(new_last_modified) =
325 util::fs::last_modified_from_path(info.path())
326 else {
327 trace!(
328 "revalidation for {} failed because new last modified time \
329 is unavailable",
330 info.inner.full.display(),
331 );
332 return false;
333 };
334 // We consider any change to invalidate cache.
335 if old_last_modified != new_last_modified {
336 trace!(
337 "revalidation for {} failed because last modified times \
338 do not match: old = {} != {} = new",
339 info.inner.full.display(),
340 old_last_modified,
341 new_last_modified,
342 );
343 return false;
344 }
345 trace!(
346 "revalidation for {} succeeded because last modified times \
347 match: old = {} == {} = new",
348 info.inner.full.display(),
349 old_last_modified,
350 new_last_modified,
351 );
352 self.expiration = Expiration::after(ttl);
353 true
354 }
355}
356
357/// A collection of time zone names extracted from a zoneinfo directory.
358///
359/// Each time zone name maps to a full path on the file system corresponding
360/// to the TZif formatted data file for that time zone.
361///
362/// This type is responsible not just for providing the names, but also for
363/// updating them periodically.
364#[derive(Debug)]
365struct ZoneInfoNames {
366 inner: RwLock<ZoneInfoNamesInner>,
367}
368
369#[derive(Debug)]
370struct ZoneInfoNamesInner {
371 /// The directory from which we collected time zone names.
372 dir: PathBuf,
373 /// All available names from the `zoneinfo` directory.
374 ///
375 /// Each name corresponds to the suffix of a file path
376 /// starting with `dir`. For example, `America/New_York` in
377 /// `/usr/share/zoneinfo/America/New_York`. Each name also has a normalized
378 /// lowercase version of the name for easy case insensitive lookup.
379 names: Vec<ZoneInfoName>,
380 /// The expiration time of this cached value.
381 ///
382 /// Note that this is a necessary but not sufficient criterion for
383 /// invalidating the cached value.
384 ttl: Duration,
385 /// The time at which the data in `names` becomes stale.
386 expiration: Expiration,
387}
388
389impl ZoneInfoNames {
390 /// The default amount of time to wait before checking for added/removed
391 /// time zones.
392 ///
393 /// Note that this TTL is a necessary but not sufficient criterion to
394 /// provoke cache invalidation. Namely, since we don't expect the set of
395 /// possible time zone names to change often, we only invalidate the cache
396 /// under these circumstances:
397 ///
398 /// 1. The TTL or more has passed since the last time the names were
399 /// attempted to be refreshed (even if it wasn't successful).
400 /// 2. A name lookup is attempted and it isn't found. This is required
401 /// because otherwise there isn't much point in refreshing the names.
402 ///
403 /// This logic does not deal as well with removals from the underlying time
404 /// zone database. That in turn is covered by the TTL on constructing the
405 /// `TimeZone` values themselves.
406 ///
407 /// We could just use the second criterion on its own, but we require the
408 /// TTL to expire out of "good sense." Namely, if there is something borked
409 /// in the environment, the TTL will prevent doing a full scan of the
410 /// zoneinfo directory for every missed time zone lookup.
411 const DEFAULT_TTL: Duration = DEFAULT_TTL;
412
413 /// Create a new collection of names from the zoneinfo database directory
414 /// given.
415 ///
416 /// If no names of time zones with corresponding TZif data files could be
417 /// found in the given directory, then an error is returned.
418 fn new(dir: &Path) -> Result<ZoneInfoNames, Error> {
419 let names = walk(dir)?;
420 let dir = dir.to_path_buf();
421 let ttl = ZoneInfoNames::DEFAULT_TTL;
422 let expiration = Expiration::after(ttl);
423 let inner = ZoneInfoNamesInner { dir, names, ttl, expiration };
424 Ok(ZoneInfoNames { inner: RwLock::new(inner) })
425 }
426
427 /// Attempts to find the name entry for the given query using a case
428 /// insensitive search.
429 ///
430 /// If no match is found and the data is stale, then the time zone names
431 /// are refreshed from the file system before doing another check.
432 fn get(&self, query: &str) -> Option<ZoneInfoName> {
433 {
434 let inner = self.inner.read().unwrap();
435 if let Some(zone_info_name) = inner.get(query) {
436 return Some(zone_info_name);
437 }
438 drop(inner); // unlock
439 }
440 let mut inner = self.inner.write().unwrap();
441 inner.attempt_refresh();
442 inner.get(query)
443 }
444
445 /// Returns all available time zone names after attempting a refresh of
446 /// the underlying data if it's stale.
447 fn available(&self) -> Vec<String> {
448 let mut inner = self.inner.write().unwrap();
449 inner.attempt_refresh();
450 inner.available()
451 }
452
453 fn reset(&self) {
454 self.inner.write().unwrap().reset();
455 }
456}
457
458impl ZoneInfoNamesInner {
459 /// Attempts to find the name entry for the given query using a case
460 /// insensitive search.
461 ///
462 /// `None` is returned if one isn't found.
463 fn get(&self, query: &str) -> Option<ZoneInfoName> {
464 self.names
465 .binary_search_by(|n| {
466 utf8::cmp_ignore_ascii_case(&n.inner.lower, query)
467 })
468 .ok()
469 .map(|i| self.names[i].clone())
470 }
471
472 /// Returns all available time zone names.
473 fn available(&self) -> Vec<String> {
474 self.names
475 .iter()
476 .filter(|n| n.is_valid())
477 .map(|n| n.inner.original.clone())
478 .collect()
479 }
480
481 /// Attempts a refresh, but only follows through if the TTL has been
482 /// exceeded.
483 ///
484 /// The caller must ensure that the other cache invalidation criteria
485 /// have been upheld. For example, this should only be called for a missed
486 /// zone name lookup.
487 fn attempt_refresh(&mut self) {
488 if self.expiration.is_expired() {
489 self.refresh();
490 }
491 }
492
493 /// Forcefully refreshes the cached names with possibly new data from disk.
494 /// If an error occurs when fetching the names, then no names are updated
495 /// (but the `expires_at` is updated). This will also emit a warning log on
496 /// failure.
497 fn refresh(&mut self) {
498 // PERF: Should we try to move this `walk` call to run outside of a
499 // lock? It probably happens pretty rarely, so it might not matter.
500 let result = walk(&self.dir);
501 self.expiration = Expiration::after(self.ttl);
502 match result {
503 Ok(names) => {
504 self.names = names;
505 }
506 Err(_err) => {
507 warn!(
508 "failed to refresh zoneinfo time zone name cache \
509 for {}: {_err}",
510 self.dir.display(),
511 )
512 }
513 }
514 }
515
516 /// Resets the state such that the next lookup is guaranteed to force a
517 /// cache refresh, and that it is impossible for any data to be stale.
518 fn reset(&mut self) {
519 // This will force the next lookup to fail.
520 self.names.clear();
521 // And this will force the next failed lookup to result in a refresh.
522 self.expiration = Expiration::expired();
523 }
524}
525
526/// A single TZif entry in a zoneinfo database directory.
527#[derive(Clone, Debug)]
528struct ZoneInfoName {
529 inner: Arc<ZoneInfoNameInner>,
530}
531
532#[derive(Debug)]
533struct ZoneInfoNameInner {
534 /// A file path resolvable to the corresponding file relative to the
535 /// working directory of this program.
536 ///
537 /// Should we canonicalize this to a absolute path? I guess in practice it
538 /// is an absolute path in most cases.
539 full: PathBuf,
540 /// The original name of this time zone taken from the file path with
541 /// no additional changes.
542 original: String,
543 /// The lowercase version of `original`. This is how we determine name
544 /// equality.
545 lower: String,
546 /// The known validity state of this time zone name. `0` means unknown (and
547 /// thus we need to check), `1` means "presumably valid" and `2` means
548 /// "known invalid." The "presumably valid" means that the file has a
549 /// 4-byte TZif header and the odds of a false positive a low enough that
550 /// we can and should behave as if that file is actually TZif and thus a
551 /// valid IANA time zone identifier.
552 validity: AtomicUsize,
553}
554
555impl ZoneInfoName {
556 /// Create a new time zone info name.
557 ///
558 /// `base` should corresponding to the zoneinfo directory from which the
559 /// suffix `time_zone_name` path was returned.
560 fn new(base: &Path, time_zone_name: &Path) -> Result<ZoneInfoName, Error> {
561 let full = base.join(time_zone_name);
562 let original = parse::os_str_utf8(time_zone_name.as_os_str())
563 .map_err(|err| err.path(base))?;
564 let lower = original.to_ascii_lowercase();
565 let inner = ZoneInfoNameInner {
566 full,
567 original: original.to_string(),
568 lower,
569 validity: AtomicUsize::new(ZONE_INFO_NAME_UNKNOWN),
570 };
571 Ok(ZoneInfoName { inner: Arc::new(inner) })
572 }
573
574 /// Returns the path to the corresponding (presumed) TZif file.
575 fn path(&self) -> &Path {
576 &self.inner.full
577 }
578
579 /// Returns the original name of this time zone.
580 fn original(&self) -> &str {
581 &self.inner.original
582 }
583
584 /// Returns the lowercase name of this time zone.
585 fn lower(&self) -> &str {
586 &self.inner.lower
587 }
588
589 /// Returns true if it is presumed that this points to a valid TZif file.
590 ///
591 /// The result of this function may use a cached value.
592 fn is_valid(&self) -> bool {
593 let validity = self.inner.validity.load(Ordering::Relaxed);
594 if validity == ZONE_INFO_NAME_VALID {
595 return true;
596 } else if validity == ZONE_INFO_NAME_INVALID {
597 return false;
598 }
599 if self.is_valid_impl() {
600 self.inner.validity.store(ZONE_INFO_NAME_VALID, Ordering::Relaxed);
601 true
602 } else {
603 self.inner
604 .validity
605 .store(ZONE_INFO_NAME_INVALID, Ordering::Relaxed);
606 false
607 }
608 }
609
610 /// Marks this zone info name as known to be valid or invalid.
611 ///
612 /// e.g., After doing a successful time zone lookup.
613 fn set_validity(&self, is_valid: bool) {
614 let validity = if is_valid {
615 ZONE_INFO_NAME_VALID
616 } else {
617 ZONE_INFO_NAME_INVALID
618 };
619 self.inner.validity.store(validity, Ordering::Relaxed);
620 }
621
622 /// Performs the actual validity check.
623 ///
624 /// This is generally only needed for APIs like
625 /// `TimeZoneDatabase::available()`, where we don't want to load every time
626 /// zone into memory but we also don't want to return IANA time zone ids
627 /// like `zone.tab`. (Because a `/usr/share/zoneinfo` directory might have
628 /// random junk in it.)
629 fn is_valid_impl(&self) -> bool {
630 let path = self.path();
631 let mut f = match File::open(path) {
632 Ok(f) => f,
633 Err(_err) => {
634 trace!("failed to open {}: {_err}", path.display());
635 return false;
636 }
637 };
638 let mut buf = [0; 4];
639 if let Err(_err) = f.read_exact(&mut buf) {
640 trace!(
641 "failed to read first 4 bytes of {}: {_err}",
642 path.display()
643 );
644 return false;
645 }
646 if !is_possibly_tzif(&buf) {
647 // This is a trace because it's perfectly normal for a
648 // non-TZif file to be in a zoneinfo directory. But it could
649 // still be potentially useful debugging info.
650 trace!(
651 "found file {} that isn't TZif since its first \
652 four bytes are {:?}",
653 path.display(),
654 crate::util::escape::Bytes(&buf),
655 );
656 return false;
657 }
658 true
659 }
660}
661
662impl Eq for ZoneInfoName {}
663
664impl PartialEq for ZoneInfoName {
665 fn eq(&self, rhs: &ZoneInfoName) -> bool {
666 self.inner.lower == rhs.inner.lower
667 }
668}
669
670impl Ord for ZoneInfoName {
671 fn cmp(&self, rhs: &ZoneInfoName) -> core::cmp::Ordering {
672 self.inner.lower.cmp(&rhs.inner.lower)
673 }
674}
675
676impl PartialOrd for ZoneInfoName {
677 fn partial_cmp(&self, rhs: &ZoneInfoName) -> Option<core::cmp::Ordering> {
678 Some(self.cmp(rhs))
679 }
680}
681
682impl core::hash::Hash for ZoneInfoName {
683 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
684 self.inner.lower.hash(state);
685 }
686}
687
688static ZONE_INFO_NAME_UNKNOWN: usize = 0;
689static ZONE_INFO_NAME_VALID: usize = 1;
690static ZONE_INFO_NAME_INVALID: usize = 2;
691
692/// Recursively walks the given directory and returns the names of all time
693/// zones found.
694///
695/// This is guaranteed to return either one or more time zone names OR an
696/// error. That is, `Ok(vec![])` is an impossible result.
697///
698/// This will attempt to collect as many names as possible, even if some I/O
699/// operations fail.
700///
701/// The names returned are sorted in lexicographic order according to the
702/// lowercase form of each name.
703///
704/// # Performance
705///
706/// Note that this routine is written in a way that, at least on Unix, we
707/// should not be doing a syscall for every file. We need to do one for every
708/// directory, but that should be comparatively rare. It's done this way to
709/// avoid long initialization times when `/usr/share/zoneinfo` is on a slow
710/// file system.
711///
712/// See: https://github.com/BurntSushi/jiff/issues/366
713fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
714 struct StackEntry {
715 dir: PathBuf,
716 depth: usize,
717 }
718
719 let mut first_err: Option<Error> = None;
720 let mut seterr = |path: &Path, err: Error| {
721 if first_err.is_none() {
722 first_err = Some(err.path(path));
723 }
724 };
725
726 let mut names = vec![];
727 let mut stack = vec![StackEntry { dir: start.to_path_buf(), depth: 0 }];
728 while let Some(StackEntry { dir, depth }) = stack.pop() {
729 let readdir = match dir.read_dir() {
730 Ok(readdir) => readdir,
731 Err(err) => {
732 info!(
733 "error when reading {} as a directory: {err}",
734 dir.display()
735 );
736 seterr(&dir, Error::io(err));
737 continue;
738 }
739 };
740 for result in readdir {
741 let dent = match result {
742 Ok(dent) => dent,
743 Err(err) => {
744 info!(
745 "error when reading directory entry from {}: {err}",
746 dir.display()
747 );
748 seterr(&dir, Error::io(err));
749 continue;
750 }
751 };
752 let file_type = match dent.file_type() {
753 Ok(file_type) => file_type,
754 Err(err) => {
755 let path = dent.path();
756 info!(
757 "error when reading file type from {}: {err}",
758 path.display()
759 );
760 seterr(&path, Error::io(err));
761 continue;
762 }
763 };
764 let path = dent.path();
765 if file_type.is_dir() {
766 // We ignore the `posix` and `right` directories because Jiff
767 // doesn't care about them. They tend to bloat the output of
768 // `TimeZoneDatabase::available()` for no appreciable reason.
769 // If callers want to use them, they can do, e.g.,
770 // `TZDIR=/usr/share/zoneinfo/posix`. Moreover, they slow down
771 // initialization time in environments with very slow file
772 // systems.
773 if depth == 0
774 && (dent.file_name() == OsStr::new("posix")
775 || dent.file_name() == OsStr::new("right"))
776 {
777 continue;
778 }
779 stack.push(StackEntry {
780 dir: path,
781 depth: depth.saturating_add(1),
782 });
783 continue;
784 }
785 trace!(
786 "zoneinfo database initialization visiting {path}",
787 path = path.display(),
788 );
789 // We assume symlinks are files, although this may not be
790 // appropriate. If we need to also handle the case when they're
791 // directories, then we'll need to add symlink loop detection.
792
793 let time_zone_name = match path.strip_prefix(start) {
794 Ok(time_zone_name) => time_zone_name,
795 Err(err) => {
796 trace!(
797 "failed to extract time zone name from {} \
798 using {} as a base: {err}",
799 path.display(),
800 start.display(),
801 );
802 seterr(&path, Error::adhoc(err));
803 continue;
804 }
805 };
806 let zone_info_name =
807 match ZoneInfoName::new(&start, time_zone_name) {
808 Ok(zone_info_name) => zone_info_name,
809 Err(err) => {
810 seterr(&path, err);
811 continue;
812 }
813 };
814 names.push(zone_info_name);
815 }
816 }
817 if names.is_empty() {
818 let err = first_err
819 .take()
820 .unwrap_or_else(|| err!("{}: no TZif files", start.display()));
821 Err(err)
822 } else {
823 // If we found at least one valid name, then we declare success and
824 // drop any error we might have found. They do all get logged above
825 // though.
826 names.sort();
827 Ok(names)
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 /// DEBUG COMMAND
836 ///
837 /// Takes environment variable `JIFF_DEBUG_ZONEINFO_DIR` as input and
838 /// prints a list of all time zone names in the directory (one per line).
839 ///
840 /// Callers may also set `RUST_LOG` to get extra debugging output.
841 #[test]
842 fn debug_zoneinfo_walk() -> anyhow::Result<()> {
843 let _ = crate::logging::Logger::init();
844
845 const ENV: &str = "JIFF_DEBUG_ZONEINFO_DIR";
846 let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
847 let dir = PathBuf::from(val);
848 let names = walk(&dir)?;
849 for n in names {
850 std::eprintln!("{}", n.inner.original);
851 }
852 Ok(())
853 }
854}