Skip to content

Commit b485ac2

Browse files
committed
Add proper cache refreshing
1 parent f99aa51 commit b485ac2

File tree

1 file changed

+128
-50
lines changed

1 file changed

+128
-50
lines changed

src/offset/local/unix.rs

Lines changed: 128 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@
99
// except according to those terms.
1010

1111
use std::cell::RefCell;
12-
use std::collections::hash_map;
1312
use std::env;
1413
use std::fs;
15-
use std::hash::Hasher;
1614
use std::path::{Path, PathBuf};
1715
use std::time::SystemTime;
1816

@@ -51,6 +49,10 @@ struct CachedTzInfo {
5149
zone: Option<TimeZone>,
5250
source: Source,
5351
last_checked: SystemTime,
52+
tz_var: Option<TzEnvVar>,
53+
tz_name: Option<String>,
54+
path: Option<PathBuf>,
55+
tzdb_dir: Option<PathBuf>,
5456
}
5557

5658
impl CachedTzInfo {
@@ -79,26 +81,25 @@ impl CachedTzInfo {
7981
self.last_checked = now;
8082
}
8183

82-
/// Check if any of the `TZ` environment variable or `/etc/localtime` have changed.
84+
/// Check if any of the environment variables or files have changed, or the name of the current
85+
/// time zone as determined by the `iana_time_zone` crate.
8386
fn needs_update(&self) -> bool {
84-
let env_tz = env::var("TZ").ok();
85-
let env_ref = env_tz.as_deref();
86-
let new_source = Source::new(env_ref);
87-
88-
match (&self.source, &new_source) {
89-
(Source::Environment { hash: old_hash }, Source::Environment { hash })
90-
if old_hash == hash =>
91-
{
92-
false
93-
}
94-
(Source::LocalTime, Source::LocalTime) => {
95-
match fs::symlink_metadata("/etc/localtime").and_then(|m| m.modified()) {
96-
Ok(mtime) => mtime > self.last_checked,
97-
Err(_) => false,
98-
}
99-
}
100-
_ => true,
87+
if self.tz_env_var_changed() {
88+
return true;
89+
}
90+
if self.source == Source::TzEnvVar {
91+
return false; // No need for further checks if the cached value came from the `TZ` var.
10192
}
93+
if self.symlink_changed() {
94+
return true;
95+
}
96+
if self.source == Source::Localtime {
97+
return false; // No need for further checks if the cached value came from the symlink.
98+
}
99+
if self.tz_name_changed() {
100+
return true;
101+
}
102+
false
102103
}
103104

104105
/// Try to get the current time zone data.
@@ -112,12 +113,14 @@ impl CachedTzInfo {
112113
/// - the global IANA time zone name in combination with the platform time zone database
113114
/// - fall back to UTC if all else fails
114115
fn read_tz_info(&mut self) {
115-
self.source = Source::new(env::var("TZ").ok().as_deref());
116-
117116
let tz_var = TzEnvVar::get();
118-
if let Some(tz_var) = tz_var {
119-
if self.read_from_tz_env(&tz_var).is_ok() {
120-
return;
117+
match tz_var {
118+
None => self.tz_var = None,
119+
Some(tz_var) => {
120+
if self.read_from_tz_env(&tz_var).is_ok() {
121+
self.tz_var = Some(tz_var);
122+
return;
123+
}
121124
}
122125
}
123126
#[cfg(not(target_os = "android"))]
@@ -128,63 +131,104 @@ impl CachedTzInfo {
128131
return;
129132
}
130133
self.zone = Some(TimeZone::utc());
134+
self.source = Source::Utc;
131135
}
132136

133137
/// Read the `TZ` environment variable or the TZif file that it points to.
134138
fn read_from_tz_env(&mut self, tz_var: &TzEnvVar) -> Result<(), ()> {
135139
match tz_var {
136140
TzEnvVar::TzString(tz_string) => {
137141
self.zone = Some(TimeZone::from_tz_string(tz_string).map_err(|_| ())?);
142+
self.path = None;
138143
}
139144
TzEnvVar::Path(path) => {
140145
let path = PathBuf::from(&path[1..]);
141-
let tzif = fs::read(path).map_err(|_| ())?;
146+
let tzif = fs::read(&path).map_err(|_| ())?;
142147
self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?);
148+
self.path = Some(path);
143149
}
144150
TzEnvVar::TzName(tz_id) => self.read_tzif(&tz_id[1..])?,
145151
#[cfg(not(target_os = "android"))]
146152
TzEnvVar::LocaltimeSymlink => self.read_from_symlink()?,
147153
};
154+
self.source = Source::TzEnvVar;
148155
Ok(())
149156
}
150157

158+
/// Check if the `TZ` environment variable has changed, or the file it points to.
159+
fn tz_env_var_changed(&self) -> bool {
160+
let tz_var = TzEnvVar::get();
161+
match (&self.tz_var, &tz_var) {
162+
(None, None) => false,
163+
(Some(TzEnvVar::TzString(a)), Some(TzEnvVar::TzString(b))) if a == b => false,
164+
(Some(TzEnvVar::Path(a)), Some(TzEnvVar::Path(b))) if a == b => {
165+
self.mtime_changed(self.path.as_deref())
166+
}
167+
(Some(TzEnvVar::TzName(a)), Some(TzEnvVar::TzName(b))) if a == b => {
168+
self.mtime_changed(self.path.as_deref()) || self.tzdb_dir_changed()
169+
}
170+
#[cfg(not(target_os = "android"))]
171+
(Some(TzEnvVar::LocaltimeSymlink), Some(TzEnvVar::LocaltimeSymlink)) => {
172+
self.symlink_changed()
173+
}
174+
_ => true,
175+
}
176+
}
177+
151178
/// Read the Tzif file that `/etc/localtime` is symlinked to.
152179
#[cfg(not(target_os = "android"))]
153180
fn read_from_symlink(&mut self) -> Result<(), ()> {
154181
let tzif = fs::read("/etc/localtime").map_err(|_| ())?;
155182
self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?);
183+
self.source = Source::Localtime;
156184
Ok(())
157185
}
158186

187+
/// Check if the `/etc/localtime` symlink or its target has changed.
188+
fn symlink_changed(&self) -> bool {
189+
self.mtime_changed(Some(Path::new("/etc/localtime")))
190+
}
191+
159192
/// Get the IANA time zone name of the system by whichever means the `iana_time_zone` crate gets
160193
/// it, and try to read the corresponding TZif data.
161194
fn read_with_tz_name(&mut self) -> Result<(), ()> {
162195
let tz_name = iana_time_zone::get_timezone().map_err(|_| ())?;
163-
self.read_tzif(&tz_name)
196+
self.read_tzif(&tz_name)?;
197+
self.tz_name = Some(tz_name);
198+
self.source = Source::TimeZoneName;
199+
Ok(())
200+
}
201+
202+
/// Check if the IANA time zone name has changed, or the file it points to.
203+
fn tz_name_changed(&self) -> bool {
204+
self.tz_name != iana_time_zone::get_timezone().ok()
205+
|| self.tzdb_dir_changed()
206+
|| self.mtime_changed(self.path.as_deref())
164207
}
165208

166209
/// Try to read the TZif data for the specified time zone name.
167210
fn read_tzif(&mut self, tz_name: &str) -> Result<(), ()> {
168-
let tzif = self.read_tzif_inner(tz_name)?;
211+
let (tzif, path) = self.read_tzif_inner(tz_name)?;
169212
self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?);
213+
self.path = path;
170214
Ok(())
171215
}
172216

173217
#[cfg(not(target_os = "android"))]
174-
fn read_tzif_inner(&self, tz_name: &str) -> Result<Vec<u8>, ()> {
218+
fn read_tzif_inner(&mut self, tz_name: &str) -> Result<(Vec<u8>, Option<PathBuf>), ()> {
175219
let path = self.tzdb_dir()?.join(tz_name);
176-
let tzif = fs::read(path).map_err(|_| ())?;
177-
Ok(tzif)
220+
let tzif = fs::read(&path).map_err(|_| ())?;
221+
Ok((tzif, Some(path)))
178222
}
179223
#[cfg(target_os = "android")]
180-
fn read_tzif_inner(&self, tz_name: &str) -> Result<Vec<u8>, ()> {
224+
fn read_tzif_inner(&mut self, tz_name: &str) -> Result<(Vec<u8>, Option<PathBuf>), ()> {
181225
let tzif = android_tzdata::find_tz_data(&tz_name).map_err(|_| ())?;
182-
Ok(tzif)
226+
Ok((tzif, None))
183227
}
184228

185229
/// Get the location of the time zone database directory with TZif files.
186230
#[cfg(not(target_os = "android"))]
187-
fn tzdb_dir(&self) -> Result<PathBuf, ()> {
231+
fn tzdb_dir(&mut self) -> Result<PathBuf, ()> {
188232
// Possible system timezone directories
189233
const ZONE_INFO_DIRECTORIES: [&str; 4] =
190234
["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"];
@@ -199,14 +243,56 @@ impl CachedTzInfo {
199243
}
200244
}
201245

246+
// Use the cached value
247+
if let Some(dir) = self.tzdb_dir.as_ref() {
248+
return Ok(PathBuf::from(dir));
249+
}
250+
251+
// No cached value yet, try the various possible system timezone directories.
202252
for dir in &ZONE_INFO_DIRECTORIES {
203253
let path = PathBuf::from(dir);
204254
if path.exists() {
255+
self.tzdb_dir = Some(path.clone());
205256
return Ok(path);
206257
}
207258
}
208259
Err(())
209260
}
261+
262+
/// Check if the location that the `TZDIR` environment variable points to has changed.
263+
fn tzdb_dir_changed(&self) -> bool {
264+
#[cfg(not(target_os = "android"))]
265+
if let Some(tz_dir) = env::var_os("TZDIR") {
266+
if !tz_dir.is_empty()
267+
&& Some(tz_dir.as_os_str()) != self.tzdb_dir.as_ref().map(|d| d.as_os_str())
268+
{
269+
return true;
270+
}
271+
}
272+
false
273+
}
274+
275+
/// Returns `true` if the modification time of the TZif file or symlink is more recent then
276+
/// `self.last_checked`.
277+
///
278+
/// Also returns `true` if there was an error getting the modification time.
279+
/// If the file is a symlink this method checks the symlink and the final target.
280+
fn mtime_changed(&self, path: Option<&Path>) -> bool {
281+
fn inner(path: &Path, last_checked: SystemTime) -> Result<bool, std::io::Error> {
282+
let metadata = fs::symlink_metadata(path)?;
283+
if metadata.modified()? > last_checked {
284+
return Ok(true);
285+
}
286+
if metadata.is_symlink() && fs::metadata(path)?.modified()? > last_checked {
287+
return Ok(true);
288+
}
289+
Ok(false)
290+
}
291+
match path {
292+
Some(path) => inner(path, self.last_checked).unwrap_or(true),
293+
None => false,
294+
}
295+
}
210296
}
211297

212298
thread_local! {
@@ -215,31 +301,23 @@ thread_local! {
215301
zone: None,
216302
source: Source::Uninitialized,
217303
last_checked: SystemTime::UNIX_EPOCH,
304+
tz_var: None,
305+
tz_name: None,
306+
path: None,
307+
tzdb_dir: None,
218308
}
219309
) };
220310
}
221311

222312
#[derive(PartialEq)]
223313
enum Source {
224-
Environment { hash: u64 },
225-
LocalTime,
314+
TzEnvVar,
315+
Localtime,
316+
TimeZoneName,
317+
Utc,
226318
Uninitialized,
227319
}
228320

229-
impl Source {
230-
fn new(env_tz: Option<&str>) -> Source {
231-
match env_tz {
232-
Some(tz) => {
233-
let mut hasher = hash_map::DefaultHasher::new();
234-
hasher.write(tz.as_bytes());
235-
let hash = hasher.finish();
236-
Source::Environment { hash }
237-
}
238-
None => Source::LocalTime,
239-
}
240-
}
241-
}
242-
243321
/// Type of the `TZ` environment variable.
244322
///
245323
/// Supported formats are:

0 commit comments

Comments
 (0)