From 68553d51d4c4ec8b7f4d1969dc3330d0ac672e7a Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Sat, 24 Feb 2024 13:56:32 +0100 Subject: [PATCH] Add proper cache refreshing --- src/offset/local/unix.rs | 128 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 9 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 056381ab3c..2df058fee8 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -44,6 +44,10 @@ struct CachedTzInfo { zone: Option, last_checked: SystemTime, source: Source, + tz_var: Option, + tz_name: Option, + path: Option, + tzdb_dir: Option, } impl CachedTzInfo { @@ -66,10 +70,33 @@ impl CachedTzInfo { } } - self.read_tz_info(); + if self.needs_update() { + self.read_tz_info(); + } self.last_checked = now; } + /// Check if any of the environment variables or files have changed, or any inputs that the + /// `iana_time_zone` crate uses. + fn needs_update(&self) -> bool { + if self.tz_env_var_changed() { + return true; + } + if self.source == Source::TzEnvVar { + return false; // No need for further checks if the cached value came from the `TZ` var. + } + if self.symlink_changed() { + return true; + } + if self.source == Source::LocaltimeSymlink { + return false; // No need for further checks if the cached value came from the symlink. + } + if self.tz_name_changed() { + return true; + } + false + } + /// Try to get the current time zone data. /// /// The following sources are tried in order: @@ -81,9 +108,13 @@ impl CachedTzInfo { /// - the global IANA time zone name in combination with the platform time zone database fn read_tz_info(&mut self) { let tz_var = TzEnvVar::get(); - if let Some(tz_var) = tz_var { - if self.read_from_tz_env(&tz_var).is_ok() { - return; + match tz_var { + None => self.tz_var = None, + Some(tz_var) => { + if self.read_from_tz_env(&tz_var).is_ok() { + self.tz_var = Some(tz_var); + return; + } } } #[cfg(not(target_os = "android"))] @@ -94,6 +125,7 @@ impl CachedTzInfo { return; } self.zone = None; + self.path = None; } /// Read the `TZ` environment variable or the TZif file that it points to. @@ -101,11 +133,13 @@ impl CachedTzInfo { match tz_var { TzEnvVar::TzString(tz_string) => { self.zone = Some(TimeZone::from_tz_string(tz_string).map_err(|_| ())?); + self.path = None; } TzEnvVar::Path(path) => { let path = PathBuf::from(&path[1..]); let tzif = fs::read(&path).map_err(|_| ())?; self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?); + self.path = Some(path); } TzEnvVar::TzName(tz_id) => self.read_tzif(&tz_id[1..])?, #[cfg(not(target_os = "android"))] @@ -115,15 +149,41 @@ impl CachedTzInfo { Ok(()) } + /// Check if the `TZ` environment variable has changed, or the file it points to. + fn tz_env_var_changed(&self) -> bool { + let tz_var = TzEnvVar::get(); + match (&self.tz_var, &tz_var) { + (None, None) => false, + (Some(TzEnvVar::TzString(a)), Some(TzEnvVar::TzString(b))) if a == b => false, + (Some(TzEnvVar::Path(a)), Some(TzEnvVar::Path(b))) if a == b => { + self.mtime_changed(self.path.as_ref().unwrap()) + } + (Some(TzEnvVar::TzName(a)), Some(TzEnvVar::TzName(b))) if a == b => { + self.mtime_changed(self.path.as_ref().unwrap()) || self.tzdb_dir_changed() + } + #[cfg(not(target_os = "android"))] + (Some(TzEnvVar::LocaltimeSymlink), Some(TzEnvVar::LocaltimeSymlink)) => { + self.symlink_changed() + } + _ => true, + } + } + /// Read the Tzif file that `/etc/localtime` is symlinked to. #[cfg(not(target_os = "android"))] fn read_from_symlink(&mut self) -> Result<(), ()> { let tzif = fs::read("/etc/localtime").map_err(|_| ())?; self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?); + self.path = None; self.source = Source::LocaltimeSymlink; Ok(()) } + /// Check if the `/etc/localtime` symlink or its target has changed. + fn symlink_changed(&self) -> bool { + self.mtime_changed(Path::new("/etc/localtime")) + } + /// Get the IANA time zone name of the system by whichever means the `iana_time_zone` crate gets /// it, and try to read the corresponding TZif data. fn read_with_tz_name(&mut self) -> Result<(), ()> { @@ -133,23 +193,31 @@ impl CachedTzInfo { Ok(()) } + /// Check if the IANA time zone name has changed, or the file it points to. + fn tz_name_changed(&self) -> bool { + self.tz_name != iana_time_zone::get_timezone().ok() + || self.tzdb_dir_changed() + || self.mtime_changed(self.path.as_ref().unwrap()) + } + /// Try to read the TZif data for the specified time zone name. fn read_tzif(&mut self, tz_id: &str) -> Result<(), ()> { - let tzif = self.read_tzif_inner(&tz_id[1..])?; + let (tzif, path) = self.read_tzif_inner(&tz_id[1..])?; self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?); + self.path = path; Ok(()) } #[cfg(not(target_os = "android"))] - fn read_tzif_inner(&mut self, tz_id: &str) -> Result, ()> { + fn read_tzif_inner(&mut self, tz_id: &str) -> Result<(Vec, Option), ()> { let path = self.tzdb_dir()?.join(&tz_id[1..]); let tzif = fs::read(&path).map_err(|_| ())?; - Ok(tzif) + Ok((tzif, Some(path))) } #[cfg(target_os = "android")] - fn read_tzif_inner(&mut self, tz_id: &str) -> Result, ()> { + fn read_tzif_inner(&mut self, tz_id: &str) -> Result<(Vec, Option), ()> { let tzif = android_tzdata::find_tz_data(&tz_id).map_err(|_| ())?; - Ok(tzif) + Ok((tzif, None)) } /// Get the location of the time zone database directory with TZif files. @@ -169,14 +237,52 @@ impl CachedTzInfo { } } + // Use the cached value + if let Some(dir) = self.tzdb_dir.as_ref() { + return Ok(PathBuf::from(dir)); + } + + // No cached value yet, try the various possible system timezone directories. for dir in &ZONE_INFO_DIRECTORIES { let path = PathBuf::from(dir); if path.exists() { + self.tzdb_dir = Some(path.clone()); return Ok(path); } } Err(()) } + + /// Check if the location that the `TZDIR` environment variable points to has changed. + fn tzdb_dir_changed(&self) -> bool { + if let Some(tz_dir) = env::var_os("TZDIR") { + if !tz_dir.is_empty() + && Some(tz_dir.as_os_str()) != self.tzdb_dir.as_ref().map(|d| d.as_os_str()) + { + return true; + } + } + false + } + + /// Returns `true` if the modification time of the TZif file or symlink is more recent then + /// `self.last_checked`. + /// + /// Also returns `true` if there was an error getting the modification time. + /// If the file is a symlink this method checks the symlink and the final target. + fn mtime_changed(&self, path: &Path) -> bool { + fn inner(path: &Path, last_checked: SystemTime) -> Result { + let metadata = fs::symlink_metadata(path)?; + if metadata.modified()? > last_checked { + return Ok(true); + } + if metadata.is_symlink() && fs::metadata(path)?.modified()? > last_checked { + return Ok(true); + } + Ok(false) + } + inner(path, self.last_checked).unwrap_or(true) + } } thread_local! { @@ -185,6 +291,10 @@ thread_local! { zone: None, last_checked: SystemTime::UNIX_EPOCH, source: Source::Uninitialized, + tz_var: None, + tz_name: None, + path: None, + tzdb_dir: None, } ) }; }