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, LocalResult};
16
17pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> {
18 offset(d:utc, local:false)
19}
20
21pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> {
22 offset(d:local, local:true)
23}
24
25fn offset(d: &NaiveDateTime, local: bool) -> LocalResult<FixedOffset> {
26 TZ_INFO.with(|maybe_cache: &RefCell>| {
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.
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: String = iana_time_zone::get_timezone().ok()?;
82 #[cfg(not(target_os = "android"))]
83 let bytes: Vec = fs::read(path: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: Option = env::var(key:"TZ").ok();
93 let env_ref: Option<&str> = env_tz.as_deref();
94 Cache {
95 last_checked: SystemTime::now(),
96 source: Source::new(env_tz:env_ref),
97 zone: current_zone(var:env_ref),
98 }
99 }
100}
101
102fn current_zone(var: Option<&str>) -> TimeZone {
103 TimeZone::local(env_tz:var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc)
104}
105
106impl Cache {
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