chrono/offset/local/
unix.rs

1// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution and at
3// http://rust-lang.org/COPYRIGHT.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime};
12
13use super::tz_info::TimeZone;
14use super::{FixedOffset, NaiveDateTime};
15use crate::{Datelike, MappedLocalTime};
16
17pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
18    offset(utc, false)
19}
20
21pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
22    offset(local, true)
23}
24
25fn offset(d: &NaiveDateTime, local: bool) -> MappedLocalTime<FixedOffset> {
26    TZ_INFO.with(|maybe_cache| {
27        maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local)
28    })
29}
30
31// we have to store the `Cache` in an option as it can't
32// be initialized in a static context.
33thread_local! {
34    static TZ_INFO: RefCell<Option<Cache>> = Default::default();
35}
36
37enum Source {
38    LocalTime { mtime: SystemTime },
39    Environment { hash: u64 },
40}
41
42impl Source {
43    fn new(env_tz: Option<&str>) -> Source {
44        match env_tz {
45            Some(tz) => {
46                let mut hasher = hash_map::DefaultHasher::new();
47                hasher.write(tz.as_bytes());
48                let hash = hasher.finish();
49                Source::Environment { hash }
50            }
51            None => match fs::symlink_metadata("/etc/localtime") {
52                Ok(data) => Source::LocalTime {
53                    // we have to pick a sensible default when the mtime fails
54                    // by picking SystemTime::now() we raise the probability of
55                    // the cache being invalidated if/when the mtime starts working
56                    mtime: data.modified().unwrap_or_else(|_| SystemTime::now()),
57                },
58                Err(_) => {
59                    // as above, now() should be a better default than some constant
60                    // TODO: see if we can improve caching in the case where the fallback is a valid timezone
61                    Source::LocalTime { mtime: SystemTime::now() }
62                }
63            },
64        }
65    }
66}
67
68struct Cache {
69    zone: TimeZone,
70    source: Source,
71    last_checked: SystemTime,
72}
73
74#[cfg(target_os = "aix")]
75const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo";
76
77#[cfg(not(any(target_os = "android", target_os = "aix")))]
78const TZDB_LOCATION: &str = "/usr/share/zoneinfo";
79
80fn fallback_timezone() -> Option<TimeZone> {
81    let tz_name = iana_time_zone::get_timezone().ok()?;
82    #[cfg(not(target_os = "android"))]
83    let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).ok()?;
84    #[cfg(target_os = "android")]
85    let bytes = android_tzdata::find_tz_data(&tz_name).ok()?;
86    TimeZone::from_tz_data(&bytes).ok()
87}
88
89impl Default for Cache {
90    fn default() -> Cache {
91        // default to UTC if no local timezone can be found
92        let env_tz = env::var("TZ").ok();
93        let env_ref = env_tz.as_deref();
94        Cache {
95            last_checked: SystemTime::now(),
96            source: Source::new(env_ref),
97            zone: current_zone(env_ref),
98        }
99    }
100}
101
102fn current_zone(var: Option<&str>) -> TimeZone {
103    TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc)
104}
105
106impl Cache {
107    fn offset(&mut self, d: NaiveDateTime, local: bool) -> MappedLocalTime<FixedOffset> {
108        let now = SystemTime::now();
109
110        match now.duration_since(self.last_checked) {
111            // If the cache has been around for less than a second then we reuse it
112            // unconditionally. This is a reasonable tradeoff because the timezone
113            // generally won't be changing _that_ often, but if the time zone does
114            // change, it will reflect sufficiently quickly from an application
115            // user's perspective.
116            Ok(d) if d.as_secs() < 1 => (),
117            Ok(_) | Err(_) => {
118                let env_tz = env::var("TZ").ok();
119                let env_ref = env_tz.as_deref();
120                let new_source = Source::new(env_ref);
121
122                let out_of_date = match (&self.source, &new_source) {
123                    // change from env to file or file to env, must recreate the zone
124                    (Source::Environment { .. }, Source::LocalTime { .. })
125                    | (Source::LocalTime { .. }, Source::Environment { .. }) => true,
126                    // stay as file, but mtime has changed
127                    (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime })
128                        if old_mtime != mtime =>
129                    {
130                        true
131                    }
132                    // stay as env, but hash of variable has changed
133                    (Source::Environment { hash: old_hash }, Source::Environment { hash })
134                        if old_hash != hash =>
135                    {
136                        true
137                    }
138                    // cache can be reused
139                    _ => false,
140                };
141
142                if out_of_date {
143                    self.zone = current_zone(env_ref);
144                }
145
146                self.last_checked = now;
147                self.source = new_source;
148            }
149        }
150
151        if !local {
152            let offset = self
153                .zone
154                .find_local_time_type(d.and_utc().timestamp())
155                .expect("unable to select local time type")
156                .offset();
157
158            return match FixedOffset::east_opt(offset) {
159                Some(offset) => MappedLocalTime::Single(offset),
160                None => MappedLocalTime::None,
161            };
162        }
163
164        // we pass through the year as the year of a local point in time must either be valid in that locale, or
165        // the entire time was skipped in which case we will return MappedLocalTime::None anyway.
166        self.zone
167            .find_local_time_type_from_local(d.and_utc().timestamp(), d.year())
168            .expect("unable to select local time type")
169            .and_then(|o| FixedOffset::east_opt(o.offset()))
170    }
171}