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 
11 use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime};
12 
13 use super::tz_info::TimeZone;
14 use super::{FixedOffset, NaiveDateTime};
15 use crate::{Datelike, LocalResult};
16 
offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset>17 pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> {
18     offset(utc, false)
19 }
20 
offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset>21 pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> {
22     offset(local, true)
23 }
24 
offset(d: &NaiveDateTime, local: bool) -> LocalResult<FixedOffset>25 fn offset(d: &NaiveDateTime, local: bool) -> LocalResult<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 initalized in a static context.
33 thread_local! {
34     static TZ_INFO: RefCell<Option<Cache>> = Default::default();
35 }
36 
37 enum Source {
38     LocalTime { mtime: SystemTime },
39     Environment { hash: u64 },
40 }
41 
42 impl Source {
new(env_tz: Option<&str>) -> Source43     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 
68 struct Cache {
69     zone: TimeZone,
70     source: Source,
71     last_checked: SystemTime,
72 }
73 
74 #[cfg(target_os = "aix")]
75 const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo";
76 
77 #[cfg(not(any(target_os = "android", target_os = "aix")))]
78 const TZDB_LOCATION: &str = "/usr/share/zoneinfo";
79 
fallback_timezone() -> Option<TimeZone>80 fn 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 
89 impl Default for Cache {
default() -> Cache90     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 
current_zone(var: Option<&str>) -> TimeZone102 fn current_zone(var: Option<&str>) -> TimeZone {
103     TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc)
104 }
105 
106 impl Cache {
offset(&mut self, d: NaiveDateTime, local: bool) -> LocalResult<FixedOffset>107     fn offset(&mut self, d: NaiveDateTime, local: bool) -> LocalResult<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.timestamp())
155                 .expect("unable to select local time type")
156                 .offset();
157 
158             return match FixedOffset::east_opt(offset) {
159                 Some(offset) => LocalResult::Single(offset),
160                 None => LocalResult::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 LocalResult::None anyway.
166         self.zone
167             .find_local_time_type_from_local(d.timestamp(), d.year())
168             .expect("unable to select local time type")
169             .map(|o| FixedOffset::east_opt(o.offset()).unwrap())
170     }
171 }
172