Skip to main content

time/
timezone.rs

1//! Time zone rules and named-zone lookup.
2
3use core::fmt;
4
5use crate::{
6    civil::SECS_PER_DAY,
7    error::{self, Error},
8    tzdb::{self, ZoneEntry, ZoneRules},
9    tzif::ZoneInfo,
10};
11
12/// How a civil (wall-clock) time that does not map cleanly to an instant
13/// should be resolved.
14///
15/// This is the [`TimeZone::ambiguity`] companion used by
16/// [`DateTime::from_parts_with`](crate::DateTime::from_parts_with).
17#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
18#[non_exhaustive]
19pub enum Disambiguation {
20    /// The default: in a gap shift the wall time forward, in a fold select
21    /// the earlier of the two instants.
22    Compatible,
23    /// Select the earliest instant. In a gap there is only the shifted
24    /// forward instant, so this is the same as `Compatible` there.
25    Earlier,
26    /// Select the latest instant. In a gap there is only the shifted
27    /// forward instant, so this is the same as `Compatible` there.
28    Later,
29    /// Reject civil times that are ambiguous or that do not exist with
30    /// [`ErrorKind::Ambiguous`](crate::ErrorKind::Ambiguous).
31    Reject,
32}
33
34/// Describes how a civil time maps to UTC offsets in a time zone.
35#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
36pub enum Ambiguity {
37    /// Exactly one offset applies.
38    Exact {
39        /// The offset in effect, in seconds east of Greenwich.
40        offset: i32,
41    },
42    /// A fold: the civil time occurs twice.
43    Fold {
44        /// The offset of the earlier instant.
45        earlier: i32,
46        /// The offset of the later instant.
47        later: i32,
48    },
49    /// A gap: the civil time does not exist.
50    Gap {
51        /// The offset in effect before the transition.
52        before: i32,
53        /// The offset in effect after the transition.
54        after: i32,
55    },
56}
57
58/// The maximum magnitude of a UTC offset, in seconds (25:59:59).
59pub(crate) const OFFSET_LIMIT: i32 = 93_599;
60
61/// Number of seconds used when probing for offset transitions.
62const PROBE: i64 = 2 * SECS_PER_DAY;
63
64/// A time zone: UTC, a fixed offset, or a named IANA zone.
65///
66/// Values are cheap to copy. Named zones are backed by the embedded database
67/// (or, with the `timezone-system` feature, by data read from the system) and
68/// never require allocation to use.
69///
70/// # Invariants
71///
72/// A value can only be constructed through [`TimeZone::UTC`],
73/// [`TimeZone::fixed`], [`TimeZone::named`] or [`TimeZone::system`], so:
74///
75/// * a fixed offset is always validated to lie within
76///   `-25:59:59 ..= 25:59:59`, and
77/// * a named zone always carries rule data that parsed successfully; a
78///   lookup or a read of malformed data fails with
79///   [`ErrorKind::UnknownTimeZone`](crate::ErrorKind::UnknownTimeZone) or
80///   [`ErrorKind::UnknownSystemTimeZone`](crate::ErrorKind::UnknownSystemTimeZone)
81///   instead of inventing offsets.
82#[derive(Clone, Copy)]
83pub struct TimeZone {
84    zone: ZoneKind,
85}
86
87#[derive(Clone, Copy)]
88enum ZoneKind {
89    /// Coordinated Universal Time: [`TimeZone::UTC`], the `"z"` alias, and
90    /// zero offsets without an annotation.
91    Utc,
92    /// A fixed offset from UTC, in seconds east of Greenwich.
93    Fixed {
94        /// Offset in seconds east of UTC.
95        offset: i32,
96    },
97    /// A named IANA zone from the embedded database, or the detected system
98    /// zone (see [`TimeZone::system`]).
99    Named(&'static ZoneEntry),
100}
101
102impl TimeZone {
103    /// The UTC time zone.
104    pub const UTC: TimeZone = TimeZone {
105        zone: ZoneKind::Utc,
106    };
107
108    /// Returns a fixed-offset time zone.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if `offset` is outside `-25:59:59 ..= 25:59:59`.
113    pub const fn fixed(offset: i32) -> Result<TimeZone, Error> {
114        if offset < -OFFSET_LIMIT || offset > OFFSET_LIMIT {
115            return Err(error::out_of_range("UTC offset is outside the supported range"));
116        }
117        Ok(TimeZone {
118            zone: ZoneKind::Fixed {
119                offset,
120            },
121        })
122    }
123
124    /// Looks up an IANA time zone by name.
125    ///
126    /// `"UTC"`, `"GMT"`, `"Etc/UTC"` and the other UTC names in the database
127    /// are returned as ordinary named zones, so their canonical name,
128    /// abbreviation and RFC 9557 annotation are preserved. The bare `"z"`
129    /// convenience alias resolves to [`TimeZone::UTC`]. `"local"` resolves to
130    /// the system time zone when the `timezone-system` feature is enabled.
131    /// Matching is case-insensitive.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`ErrorKind::UnknownTimeZone`](crate::ErrorKind::UnknownTimeZone)
136    /// if `name` is not present in the database.
137    pub fn named(name: &str) -> Result<TimeZone, Error> {
138        if is_utc_convenience(name) {
139            return Ok(TimeZone::UTC);
140        }
141        if name.eq_ignore_ascii_case("local") {
142            return TimeZone::system();
143        }
144        match tzdb::lookup(name) {
145            Some(entry) => Ok(TimeZone {
146                zone: ZoneKind::Named(entry),
147            }),
148            None => Err(error::unknown_time_zone("unknown time zone")),
149        }
150    }
151
152    /// Returns the name used in an RFC 9557 annotation, which only named
153    /// IANA zones have (UTC and fixed offsets are written as offsets).
154    pub(crate) const fn annotation_name(&self) -> Option<&'static str> {
155        match self.zone {
156            ZoneKind::Named(entry) => Some(entry.name),
157            _ => None,
158        }
159    }
160
161    /// Returns the canonical name of this time zone, if it has one.
162    #[must_use]
163    #[inline]
164    pub const fn name(&self) -> Option<&'static str> {
165        match self.zone {
166            ZoneKind::Utc => Some("UTC"),
167            ZoneKind::Fixed {
168                ..
169            } => None,
170            ZoneKind::Named(entry) => Some(entry.name),
171        }
172    }
173
174    /// Returns the UTC offset of this time zone at the given instant, in
175    /// seconds east of Greenwich.
176    pub(crate) fn offset_at(self, secs: i64) -> i32 {
177        match self.info_at(secs) {
178            Some(info) => info.offset,
179            None => 0,
180        }
181    }
182
183    /// Returns the local-time information at the given instant.
184    pub(crate) fn info_at(self, secs: i64) -> Option<ZoneInfo<'static>> {
185        Some(match self.zone {
186            ZoneKind::Utc => ZoneInfo {
187                offset: 0,
188                is_dst: false,
189                abbrev: "UTC",
190            },
191            ZoneKind::Fixed {
192                offset,
193            } => ZoneInfo {
194                offset,
195                is_dst: false,
196                abbrev: "",
197            },
198            ZoneKind::Named(entry) => match &entry.rules {
199                ZoneRules::Embedded(zone) => {
200                    let info = embedded_at(zone, secs);
201                    #[cfg(all(test, feature = "timezone-db"))]
202                    parity_check(entry.name, secs, &info);
203                    info
204                }
205                ZoneRules::Runtime(tzif) => tzif.lookup(secs)?,
206            },
207        })
208    }
209
210    /// Returns `true` if daylight saving time is in effect at `secs`.
211    pub(crate) fn is_dst_at(self, secs: i64) -> bool {
212        self.info_at(secs).is_some_and(|info| info.is_dst)
213    }
214
215    /// Resolves a civil (wall-clock) time expressed as seconds since the Unix
216    /// epoch to the corresponding instant, using the "compatible"
217    /// disambiguation strategy: gaps are shifted forward, folds select the
218    /// earlier instant.
219    pub(crate) fn resolve_compatible(self, local_secs: i64) -> i64 {
220        match self.resolve_with(local_secs, Disambiguation::Compatible) {
221            Ok(instant) => instant,
222            // `Compatible` never rejects a civil time.
223            Err(_) => local_secs,
224        }
225    }
226
227    /// Returns the pair of UTC offsets that can map to the civil time
228    /// `local_secs`.
229    ///
230    /// For a named zone this uses the two offsets around the nearest explicit
231    /// transition when one is close enough, which is exact even when two
232    /// transitions are less than the two-day probe apart. When no explicit
233    /// transition is near (for example the future governed by a POSIX footer
234    /// rule, whose transitions are always months apart), it falls back to
235    /// probing two days either side.
236    fn candidate_offsets(self, local_secs: i64) -> (i32, i32) {
237        if let ZoneKind::Named(entry) = self.zone {
238            let nearby = match &entry.rules {
239                ZoneRules::Embedded(zone) => embedded_nearby(zone, local_secs),
240                ZoneRules::Runtime(tzif) => tzif.nearby_offsets(local_secs),
241            };
242            if let Some(pair) = nearby {
243                return pair;
244            }
245        }
246        (self.offset_at(local_secs - PROBE), self.offset_at(local_secs + PROBE))
247    }
248
249    /// Resolves a civil (wall-clock) time, expressed as seconds since the
250    /// Unix epoch, to the corresponding instant following `disambiguation`.
251    pub(crate) fn resolve_with(self, local_secs: i64, disambiguation: Disambiguation) -> Result<i64, Error> {
252        match self.zone {
253            ZoneKind::Utc => Ok(local_secs),
254            ZoneKind::Fixed {
255                offset,
256            } => Ok(local_secs - i64::from(offset)),
257            ZoneKind::Named(..) => {
258                let (offset_before, offset_after) = self.candidate_offsets(local_secs);
259                if offset_before == offset_after {
260                    return Ok(local_secs - i64::from(offset_before));
261                }
262                let instant_before = local_secs - i64::from(offset_before);
263                let instant_after = local_secs - i64::from(offset_after);
264                let before_ok = self.offset_at(instant_before) == offset_before;
265                let after_ok = self.offset_at(instant_after) == offset_after;
266                match (before_ok, after_ok) {
267                    // A fold: two instants map to the same wall time.
268                    (true, true) => match disambiguation {
269                        Disambiguation::Later => Ok(instant_after),
270                        Disambiguation::Reject => Err(error::ambiguous("civil time occurs twice in this time zone")),
271                        _ => Ok(instant_before),
272                    },
273                    (true, false) => Ok(instant_before),
274                    (false, true) => Ok(instant_after),
275                    // A gap: the wall time does not exist. Shift forward.
276                    (false, false) => match disambiguation {
277                        Disambiguation::Reject => Err(error::ambiguous("civil time does not exist in this time zone")),
278                        _ => Ok(instant_before),
279                    },
280                }
281            }
282        }
283    }
284
285    /// Classifies how the civil time `local_secs` maps to offsets in this
286    /// zone. `local_secs` is measured as seconds since the Unix epoch and is
287    /// interpreted as if it were an instant, which is sound because every
288    /// offset is smaller than the probe window.
289    pub(crate) fn classify(self, local_secs: i64) -> Ambiguity {
290        match self.zone {
291            ZoneKind::Utc => Ambiguity::Exact {
292                offset: 0,
293            },
294            ZoneKind::Fixed {
295                offset,
296            } => Ambiguity::Exact {
297                offset,
298            },
299            ZoneKind::Named(..) => {
300                let (offset_before, offset_after) = self.candidate_offsets(local_secs);
301                if offset_before == offset_after {
302                    return Ambiguity::Exact {
303                        offset: offset_before,
304                    };
305                }
306                let instant_before = local_secs - i64::from(offset_before);
307                let instant_after = local_secs - i64::from(offset_after);
308                let before_ok = self.offset_at(instant_before) == offset_before;
309                let after_ok = self.offset_at(instant_after) == offset_after;
310                match (before_ok, after_ok) {
311                    // A fold: two instants map to the same wall time.
312                    (true, true) => Ambiguity::Fold {
313                        earlier: offset_before,
314                        later: offset_after,
315                    },
316                    // Exactly one candidate is self-consistent (a transition
317                    // inside the probe window): the civil time exists once.
318                    (true, false) => Ambiguity::Exact {
319                        offset: offset_before,
320                    },
321                    (false, true) => Ambiguity::Exact {
322                        offset: offset_after,
323                    },
324                    // A gap: the wall time does not exist.
325                    (false, false) => Ambiguity::Gap {
326                        before: offset_before,
327                        after: offset_after,
328                    },
329                }
330            }
331        }
332    }
333
334    /// Classifies how the given civil time maps to offsets in this zone.
335    ///
336    /// # Errors
337    ///
338    /// Returns an error if a field is out of range.
339    pub fn ambiguity(
340        self,
341        year: i32,
342        month: u8,
343        day: u8,
344        hour: u8,
345        minute: u8,
346        second: u8,
347    ) -> Result<Ambiguity, Error> {
348        let local = crate::datetime::local_seconds_checked(year, month, day, hour, minute, second)?;
349        Ok(self.classify(local))
350    }
351
352    /// Returns the bounds of the constant-offset period that contains `secs`,
353    /// as `(start, end)` instants where an absent bound means unbounded.
354    pub(crate) fn bounds_at(self, secs: i64) -> (Option<i64>, Option<i64>) {
355        match self.zone {
356            ZoneKind::Named(entry) => match &entry.rules {
357                ZoneRules::Embedded(zone) => {
358                    let starts = zone.starts;
359                    if starts.is_empty() {
360                        return (None, None);
361                    }
362                    let past = starts.partition_point(|&start| start <= secs);
363                    if past == 0 {
364                        return (None, Some(starts[0]));
365                    }
366                    if past == starts.len() {
367                        return (Some(starts[starts.len() - 1]), None);
368                    }
369                    (Some(starts[past - 1]), Some(starts[past]))
370                }
371                // Runtime TZif zones do not expose their transition table.
372                ZoneRules::Runtime(_) => (None, None),
373            },
374            _ => (None, None),
375        }
376    }
377
378    /// Determines the system's local time zone.
379    ///
380    /// This reads the `TZ` environment variable, then `/etc/localtime`, then
381    /// the platform zoneinfo directory. It never calls `localtime_r`. The
382    /// time zone data that a detected value uses must parse; otherwise the
383    /// lookup fails. The detected zone is determined once per process and
384    /// cached, so a later change of `TZ` (or of `/etc/localtime`) is not
385    /// observed, and the process leaks one small allocation of detected data
386    /// at most.
387    ///
388    /// # Errors
389    ///
390    /// Returns [`ErrorKind::UnknownSystemTimeZone`](crate::ErrorKind::UnknownSystemTimeZone)
391    /// when no system zone can be determined, or
392    /// [`ErrorKind::UnknownTimeZone`](crate::ErrorKind::UnknownTimeZone) when
393    /// `TZ` names a zone that cannot be found.
394    #[cfg(feature = "timezone-system")]
395    pub fn system() -> Result<TimeZone, Error> {
396        static SYSTEM_ZONE: std::sync::OnceLock<Result<TimeZone, Error>> = std::sync::OnceLock::new();
397        *SYSTEM_ZONE.get_or_init(system::detect)
398    }
399
400    /// Determines the system's local time zone.
401    ///
402    /// Without the `timezone-system` feature this always returns `UTC`.
403    ///
404    /// # Errors
405    ///
406    /// With `timezone-system`, returns an error when the system zone cannot be
407    /// determined.
408    #[cfg(not(feature = "timezone-system"))]
409    pub fn system() -> Result<TimeZone, Error> {
410        Ok(TimeZone::UTC)
411    }
412}
413
414/// Returns `true` for a name that maps directly to [`TimeZone::UTC`] instead of
415/// going through the database.
416///
417/// The bare `"z"` alias always does. Without the `timezone-db` feature the
418/// database is absent, so the canonical UTC names fall back to the constant
419/// too; with it they are ordinary named zones that preserve their abbreviation
420/// and RFC 9557 annotation.
421fn is_utc_convenience(name: &str) -> bool {
422    if name.eq_ignore_ascii_case("z") {
423        return true;
424    }
425    #[cfg(not(feature = "timezone-db"))]
426    {
427        ["utc", "gmt", "zulu", "universal", "uct", "etc/utc", "etc/gmt"]
428            .iter()
429            .any(|candidate| name.eq_ignore_ascii_case(candidate))
430    }
431    #[cfg(feature = "timezone-db")]
432    {
433        false
434    }
435}
436
437/// Evaluates a zone from the embedded database.
438#[cfg(feature = "timezone-db")]
439fn embedded_at(zone: &'static tzdb::ZoneData, secs: i64) -> ZoneInfo<'static> {
440    zone.lookup(secs)
441}
442
443/// Without the embedded database, no `ZoneRules::Embedded` value can be
444/// constructed; this arm exists only to keep the match exhaustive in builds
445/// that exclude the database.
446#[cfg(not(feature = "timezone-db"))]
447fn embedded_at(_zone: &'static tzdb::ZoneData, _secs: i64) -> ZoneInfo<'static> {
448    ZoneInfo {
449        offset: 0,
450        is_dst: false,
451        abbrev: "",
452    }
453}
454
455/// Returns the offsets immediately before and after the explicit transition
456/// nearest to `local` in an embedded zone, or `None` when no transition is
457/// close enough to affect it.
458fn embedded_nearby(zone: &'static tzdb::ZoneData, local: i64) -> Option<(i32, i32)> {
459    let starts = zone.starts;
460    if starts.is_empty() {
461        return None;
462    }
463    let past = starts.partition_point(|&start| start <= local);
464    let mut best_index = 0usize;
465    let mut best_distance = i64::MAX;
466    let mut consider = |index: usize| {
467        if index < starts.len() {
468            let distance = (starts[index] - local).abs();
469            if distance < best_distance {
470                best_distance = distance;
471                best_index = index;
472            }
473        }
474    };
475    if let Some(index) = past.checked_sub(1) {
476        consider(index);
477    }
478    consider(past);
479    if best_distance > 2 * i64::from(OFFSET_LIMIT) {
480        return None;
481    }
482    let after = zone.record_at(best_index).offset;
483    let before = if best_index == 0 {
484        zone.initial.offset
485    } else {
486        zone.record_at(best_index - 1).offset
487    };
488    Some((before, after))
489}
490
491/// Test-only cross-check: the pre-parsed static engine must produce the same
492/// answer as evaluating the original TZif bytes (the legacy path).
493#[cfg(all(test, feature = "timezone-db"))]
494fn parity_check(name: &'static str, secs: i64, evaluated: &ZoneInfo<'static>) {
495    if let Some((_, data)) = tzdb::lookup_legacy(name) {
496        if let Some(expected) = crate::tzif::Tzif::parse(data).and_then(|tz| tz.lookup(secs)) {
497            assert!(
498                expected.offset == evaluated.offset
499                    && expected.is_dst == evaluated.is_dst
500                    && expected.abbrev == evaluated.abbrev,
501                "zone engine mismatch for {name} at {secs}: {expected:?} != {evaluated:?}",
502            );
503        }
504    }
505}
506
507impl fmt::Debug for TimeZone {
508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509        match self.zone {
510            ZoneKind::Utc => f.write_str("Utc"),
511            ZoneKind::Fixed {
512                offset,
513            } => write!(f, "Fixed({offset})"),
514            ZoneKind::Named(entry) => write!(f, "Named({})", entry.name),
515        }
516    }
517}
518
519impl fmt::Display for TimeZone {
520    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521        match self.zone {
522            ZoneKind::Utc => f.write_str("UTC"),
523            ZoneKind::Fixed {
524                offset,
525            } => crate::format::write_offset(f, offset),
526            ZoneKind::Named(entry) => f.write_str(entry.name),
527        }
528    }
529}
530
531impl PartialEq for TimeZone {
532    fn eq(&self, other: &TimeZone) -> bool {
533        match (self.zone, other.zone) {
534            (ZoneKind::Utc, ZoneKind::Utc) => true,
535            (
536                ZoneKind::Fixed {
537                    offset: left,
538                },
539                ZoneKind::Fixed {
540                    offset: right,
541                },
542            ) => left == right,
543            (ZoneKind::Named(left), ZoneKind::Named(right)) => left.name == right.name,
544            _ => false,
545        }
546    }
547}
548
549impl Eq for TimeZone {}
550
551impl core::hash::Hash for TimeZone {
552    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
553        match self.zone {
554            ZoneKind::Utc => 0u8.hash(state),
555            ZoneKind::Fixed {
556                offset,
557            } => {
558                1u8.hash(state);
559                offset.hash(state);
560            }
561            ZoneKind::Named(entry) => {
562                2u8.hash(state);
563                entry.name.hash(state);
564            }
565        }
566    }
567}
568
569#[cfg(feature = "timezone-system")]
570mod system {
571    use alloc::{borrow::ToOwned, boxed::Box, vec::Vec};
572
573    use small_collections::SmallString;
574
575    use super::{TimeZone, ZoneEntry, ZoneKind, ZoneRules};
576    use crate::{
577        error::{self, Error},
578        tzif::Tzif,
579    };
580
581    /// Reads the system TZif data, parses it once, and returns a zone value
582    /// whose name and rules are heap-allocated once and never freed (the
583    /// caller caches the zone, so the process leaks at most one entry).
584    ///
585    /// The data is validated before anything is released, so a malformed image
586    /// does not leak on the error path.
587    fn leak_named(name: &str, data: Vec<u8>) -> Result<TimeZone, Error> {
588        Tzif::parse(&data).ok_or_else(|| error::unknown_system_time_zone("cannot parse system time zone data"))?;
589        let data: &'static [u8] = Box::leak(data.into_boxed_slice());
590        let tzif: Tzif<'static> =
591            Tzif::parse(data).ok_or_else(|| error::unknown_system_time_zone("cannot parse system time zone data"))?;
592        let rules: &'static Tzif<'static> = Box::leak(Box::new(tzif));
593        let entry: &'static ZoneEntry = Box::leak(Box::new(ZoneEntry {
594            name: Box::leak(name.to_owned().into_boxed_str()),
595            rules: ZoneRules::Runtime(rules),
596        }));
597        Ok(TimeZone {
598            zone: ZoneKind::Named(entry),
599        })
600    }
601
602    /// Inline capacity for the path built when probing zone directories.
603    ///
604    /// A directory prefix plus a typical IANA name fits well within 128 bytes;
605    /// longer paths (for example a long `TZDIR`) spill to the heap inside the
606    /// `SmallString` rather than being truncated.
607    const PATH_INLINE_LENGTH: usize = 128;
608
609    /// Reads `dir/value`, the TZif file for `value` under `dir`.
610    fn read_zone_file(dir: &str, value: &str) -> Option<Vec<u8>> {
611        let mut path: SmallString<PATH_INLINE_LENGTH> = SmallString::new();
612        path.push_str(dir);
613        path.push('/');
614        path.push_str(value);
615        std::fs::read(path.as_str()).ok()
616    }
617
618    fn name_from_path(path: &str) -> &str {
619        path.find("/zoneinfo/")
620            .map(|index| &path[index + "/zoneinfo/".len()..])
621            .unwrap_or("localtime")
622    }
623
624    pub(super) fn detect() -> Result<TimeZone, Error> {
625        if let Ok(value) = std::env::var("TZ") {
626            let value = value.strip_prefix(':').unwrap_or(&value);
627            if value.is_empty() {
628                return Ok(TimeZone::UTC);
629            }
630            if value.starts_with('/') || value.starts_with('.') {
631                let data =
632                    std::fs::read(value).map_err(|_| error::unknown_system_time_zone("cannot read TZ time zone"))?;
633                return leak_named(value, data);
634            }
635            if let Ok(zone) = TimeZone::named(value) {
636                return Ok(zone);
637            }
638            let tzdir = std::env::var("TZDIR").ok();
639            for dir in tzdir.as_deref().into_iter().chain([
640                "/usr/share/zoneinfo",
641                "/usr/lib/zoneinfo",
642                "/usr/share/lib/zoneinfo",
643                "/etc/zoneinfo",
644            ]) {
645                if let Some(data) = read_zone_file(dir, value) {
646                    return leak_named(value, data);
647                }
648            }
649            return Err(error::unknown_time_zone("unknown TZ time zone"));
650        }
651
652        let data = std::fs::read("/etc/localtime")
653            .map_err(|_| error::unknown_system_time_zone("cannot read /etc/localtime"))?;
654        let link = std::fs::read_link("/etc/localtime").ok();
655        let name = link
656            .as_deref()
657            .and_then(|path| path.to_str())
658            .map(name_from_path)
659            .unwrap_or("localtime");
660        leak_named(name, data)
661    }
662}