Skip to content

Commit

Permalink
Add proper cache refreshing
Browse files Browse the repository at this point in the history
  • Loading branch information
pitdicker committed Mar 19, 2024
1 parent 4a2f99a commit 68553d5
Showing 1 changed file with 119 additions and 9 deletions.
128 changes: 119 additions & 9 deletions src/offset/local/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ struct CachedTzInfo {
zone: Option<TimeZone>,
last_checked: SystemTime,
source: Source,
tz_var: Option<TzEnvVar>,
tz_name: Option<String>,
path: Option<PathBuf>,
tzdb_dir: Option<PathBuf>,
}

impl CachedTzInfo {
Expand All @@ -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:
Expand All @@ -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"))]
Expand All @@ -94,18 +125,21 @@ impl CachedTzInfo {
return;
}
self.zone = None;
self.path = None;
}

/// Read the `TZ` environment variable or the TZif file that it points to.
fn read_from_tz_env(&mut self, tz_var: &TzEnvVar) -> Result<(), ()> {
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"))]
Expand All @@ -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<(), ()> {
Expand All @@ -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<Vec<u8>, ()> {
fn read_tzif_inner(&mut self, tz_id: &str) -> Result<(Vec<u8>, Option<PathBuf>), ()> {
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<Vec<u8>, ()> {
fn read_tzif_inner(&mut self, tz_id: &str) -> Result<(Vec<u8>, Option<PathBuf>), ()> {
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.
Expand All @@ -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<bool, std::io::Error> {
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! {
Expand All @@ -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,
}
) };
}
Expand Down

0 comments on commit 68553d5

Please sign in to comment.