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 20, 2024
1 parent 541c9ba commit 6508743
Showing 1 changed file with 128 additions and 50 deletions.
178 changes: 128 additions & 50 deletions src/offset/local/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
// except according to those terms.

use std::cell::RefCell;
use std::collections::hash_map;
use std::env;
use std::fs;
use std::hash::Hasher;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

Expand Down Expand Up @@ -51,6 +49,10 @@ struct CachedTzInfo {
zone: Option<TimeZone>,
source: Source,
last_checked: SystemTime,
tz_var: Option<TzEnvVar>,
tz_name: Option<String>,
path: Option<PathBuf>,
tzdb_dir: Option<PathBuf>,
}

impl CachedTzInfo {
Expand Down Expand Up @@ -79,26 +81,25 @@ impl CachedTzInfo {
self.last_checked = now;
}

/// Check if any of the `TZ` environment variable or `/etc/localtime` have changed.
/// Check if any of the environment variables or files have changed, or the name of the current
/// time zone as determined by the `iana_time_zone` crate.
fn needs_update(&self) -> bool {
let env_tz = env::var("TZ").ok();
let env_ref = env_tz.as_deref();
let new_source = Source::new(env_ref);

match (&self.source, &new_source) {
(Source::Environment { hash: old_hash }, Source::Environment { hash })
if old_hash == hash =>
{
false
}
(Source::LocalTime, Source::LocalTime) => {
match fs::symlink_metadata("/etc/localtime").and_then(|m| m.modified()) {
Ok(mtime) => mtime > self.last_checked,
Err(_) => false,
}
}
_ => true,
if self.tz_env_var_changed() {
return true;

Check warning on line 88 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L88

Added line #L88 was not covered by tests
}
if self.source == Source::TzEnvVar {
return false; // No need for further checks if the cached value came from the `TZ` var.

Check warning on line 91 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L91

Added line #L91 was not covered by tests
}
if self.symlink_changed() {
return true;
}
if self.source == Source::Localtime {
return false; // No need for further checks if the cached value came from the symlink.
}
if self.tz_name_changed() {
return true;
}
false

Check warning on line 102 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L98-L102

Added lines #L98 - L102 were not covered by tests
}

/// Try to get the current time zone data.
Expand All @@ -112,12 +113,14 @@ impl CachedTzInfo {
/// - the global IANA time zone name in combination with the platform time zone database
/// - fall back to UTC if all else fails
fn read_tz_info(&mut self) {
self.source = Source::new(env::var("TZ").ok().as_deref());

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;
}

Check warning on line 123 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L119-L123

Added lines #L119 - L123 were not covered by tests
}
}
#[cfg(not(target_os = "android"))]
Expand All @@ -128,63 +131,104 @@ impl CachedTzInfo {
return;
}
self.zone = Some(TimeZone::utc());
self.source = Source::Utc;

Check warning on line 134 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L129-L134

Added lines #L129 - L134 were not covered by tests
}

/// 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;

Check warning on line 142 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L138-L142

Added lines #L138 - L142 were not covered by tests
}
TzEnvVar::Path(path) => {
let path = PathBuf::from(&path[1..]);
let tzif = fs::read(path).map_err(|_| ())?;
let tzif = fs::read(&path).map_err(|_| ())?;
self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?);
self.path = Some(path);

Check warning on line 148 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L144-L148

Added lines #L144 - L148 were not covered by tests
}
TzEnvVar::TzName(tz_id) => self.read_tzif(&tz_id[1..])?,

Check warning on line 150 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L150

Added line #L150 was not covered by tests
#[cfg(not(target_os = "android"))]
TzEnvVar::LocaltimeSymlink => self.read_from_symlink()?,

Check warning on line 152 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L152

Added line #L152 was not covered by tests
};
self.source = Source::TzEnvVar;
Ok(())
}

Check warning on line 156 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L154-L156

Added lines #L154 - L156 were not covered by tests

/// 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_deref())

Check warning on line 165 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L163-L165

Added lines #L163 - L165 were not covered by tests
}
(Some(TzEnvVar::TzName(a)), Some(TzEnvVar::TzName(b))) if a == b => {
self.mtime_changed(self.path.as_deref()) || self.tzdb_dir_changed()

Check warning on line 168 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L167-L168

Added lines #L167 - L168 were not covered by tests
}
#[cfg(not(target_os = "android"))]
(Some(TzEnvVar::LocaltimeSymlink), Some(TzEnvVar::LocaltimeSymlink)) => {
self.symlink_changed()

Check warning on line 172 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L172

Added line #L172 was not covered by tests
}
_ => true,

Check warning on line 174 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L174

Added line #L174 was not covered by tests
}
}

/// 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::Localtime;
Ok(())
}

/// Check if the `/etc/localtime` symlink or its target has changed.
fn symlink_changed(&self) -> bool {
self.mtime_changed(Some(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<(), ()> {
let tz_name = iana_time_zone::get_timezone().map_err(|_| ())?;
self.read_tzif(&tz_name)
self.read_tzif(&tz_name)?;
self.source = Source::TimeZoneName;
Ok(())
}

Check warning on line 200 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L195-L200

Added lines #L195 - L200 were not covered by tests

/// 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_deref())
}

Check warning on line 207 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L203-L207

Added lines #L203 - L207 were not covered by tests

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

Check warning on line 215 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L210-L215

Added lines #L210 - L215 were not covered by tests

#[cfg(not(target_os = "android"))]
fn read_tzif_inner(&self, tz_name: &str) -> Result<Vec<u8>, ()> {
fn read_tzif_inner(&mut self, tz_name: &str) -> Result<(Vec<u8>, Option<PathBuf>), ()> {
let path = self.tzdb_dir()?.join(tz_name);
let tzif = fs::read(path).map_err(|_| ())?;
Ok(tzif)
let tzif = fs::read(&path).map_err(|_| ())?;
Ok((tzif, Some(path)))
}

Check warning on line 222 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L218-L222

Added lines #L218 - L222 were not covered by tests
#[cfg(target_os = "android")]
fn read_tzif_inner(&self, tz_name: &str) -> Result<Vec<u8>, ()> {
fn read_tzif_inner(&mut self, tz_name: &str) -> Result<(Vec<u8>, Option<PathBuf>), ()> {
let tzif = android_tzdata::find_tz_data(&tz_name).map_err(|_| ())?;
Ok(tzif)
Ok((tzif, None))
}

/// Get the location of the time zone database directory with TZif files.
#[cfg(not(target_os = "android"))]
fn tzdb_dir(&self) -> Result<PathBuf, ()> {
fn tzdb_dir(&mut self) -> Result<PathBuf, ()> {

Check warning on line 231 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L231

Added line #L231 was not covered by tests
// Possible system timezone directories
const ZONE_INFO_DIRECTORIES: [&str; 4] =
["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"];
Expand All @@ -199,14 +243,56 @@ impl CachedTzInfo {
}
}

Check warning on line 244 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L243-L244

Added lines #L243 - L244 were not covered by tests

// Use the cached value
if let Some(dir) = self.tzdb_dir.as_ref() {
return Ok(PathBuf::from(dir));
}

Check warning on line 249 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L247-L249

Added lines #L247 - L249 were not covered by tests

// 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);

Check warning on line 256 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L252-L256

Added lines #L252 - L256 were not covered by tests
}
}
Err(())
}

Check warning on line 260 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L259-L260

Added lines #L259 - L260 were not covered by tests

/// Check if the location that the `TZDIR` environment variable points to has changed.
fn tzdb_dir_changed(&self) -> bool {

Check warning on line 263 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L263

Added line #L263 was not covered by tests
#[cfg(not(target_os = "android"))]
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())

Check warning on line 267 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L265-L267

Added lines #L265 - L267 were not covered by tests
{
return true;
}
}
false
}

Check warning on line 273 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L269-L273

Added lines #L269 - L273 were not covered by tests

/// 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: Option<&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)
}
match path {
Some(path) => inner(path, self.last_checked).unwrap_or(true),
None => false,

Check warning on line 293 in src/offset/local/unix.rs

View check run for this annotation

Codecov / codecov/patch

src/offset/local/unix.rs#L293

Added line #L293 was not covered by tests
}
}
}

thread_local! {
Expand All @@ -215,31 +301,23 @@ thread_local! {
zone: None,
source: Source::Uninitialized,
last_checked: SystemTime::UNIX_EPOCH,
tz_var: None,
tz_name: None,
path: None,
tzdb_dir: None,
}
) };
}

#[derive(PartialEq)]
enum Source {
Environment { hash: u64 },
LocalTime,
TzEnvVar,
Localtime,
TimeZoneName,
Utc,
Uninitialized,
}

impl Source {
fn new(env_tz: Option<&str>) -> Source {
match env_tz {
Some(tz) => {
let mut hasher = hash_map::DefaultHasher::new();
hasher.write(tz.as_bytes());
let hash = hasher.finish();
Source::Environment { hash }
}
None => Source::LocalTime,
}
}
}

/// Type of the `TZ` environment variable.
///
/// Supported formats are:
Expand Down

0 comments on commit 6508743

Please sign in to comment.