chrono/offset/local/
unix.rs1use 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
31thread_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 mtime: data.modified().unwrap_or_else(|_| SystemTime::now()),
57 },
58 Err(_) => {
59 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 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 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 (Source::Environment { .. }, Source::LocalTime { .. })
125 | (Source::LocalTime { .. }, Source::Environment { .. }) => true,
126 (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime })
128 if old_mtime != mtime =>
129 {
130 true
131 }
132 (Source::Environment { hash: old_hash }, Source::Environment { hash })
134 if old_hash != hash =>
135 {
136 true
137 }
138 _ => 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 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}