From 48515249dac1cd4566a9416088a2f62c6617754c Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 13:03:18 +0100 Subject: [PATCH 01/16] Factor out `Cache::refresh_cache` --- src/offset/local/unix.rs | 55 ++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index c1942eba7e..121949916f 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -105,14 +105,39 @@ fn current_zone(var: Option<&str>) -> TimeZone { impl Cache { fn offset(&mut self, d: NaiveDateTime, local: bool) -> MappedLocalTime { + self.refresh_cache(); + + if !local { + let offset = self + .zone + .find_local_time_type(d.and_utc().timestamp()) + .expect("unable to select local time type") + .offset(); + + return match FixedOffset::east_opt(offset) { + Some(offset) => MappedLocalTime::Single(offset), + None => MappedLocalTime::None, + }; + } + + // we pass through the year as the year of a local point in time must either be valid in that locale, or + // the entire time was skipped in which case we will return MappedLocalTime::None anyway. + self.zone + .find_local_time_type_from_local(d.and_utc().timestamp(), d.year()) + .expect("unable to select local time type") + .and_then(|o| FixedOffset::east_opt(o.offset())) + } + + /// Refresh our cached data if necessary. + /// + /// If the cache has been around for less than a second then we reuse it unconditionally. This is + /// a reasonable tradeoff because the timezone generally won't be changing _that_ often, but if + /// the time zone does change, it will reflect sufficiently quickly from an application user's + /// perspective. + fn refresh_cache(&mut self) { let now = SystemTime::now(); match now.duration_since(self.last_checked) { - // If the cache has been around for less than a second then we reuse it - // unconditionally. This is a reasonable tradeoff because the timezone - // generally won't be changing _that_ often, but if the time zone does - // change, it will reflect sufficiently quickly from an application - // user's perspective. Ok(d) if d.as_secs() < 1 => (), Ok(_) | Err(_) => { let env_tz = env::var("TZ").ok(); @@ -147,25 +172,5 @@ impl Cache { self.source = new_source; } } - - if !local { - let offset = self - .zone - .find_local_time_type(d.and_utc().timestamp()) - .expect("unable to select local time type") - .offset(); - - return match FixedOffset::east_opt(offset) { - Some(offset) => MappedLocalTime::Single(offset), - None => MappedLocalTime::None, - }; - } - - // we pass through the year as the year of a local point in time must either be valid in that locale, or - // the entire time was skipped in which case we will return MappedLocalTime::None anyway. - self.zone - .find_local_time_type_from_local(d.and_utc().timestamp(), d.year()) - .expect("unable to select local time type") - .and_then(|o| FixedOffset::east_opt(o.offset())) } } From a64818ae1efe74679eb87c9f9310b2491cc70d00 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 13:18:42 +0100 Subject: [PATCH 02/16] Factor out `Cache::needs_update` --- src/offset/local/unix.rs | 68 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 121949916f..24995f5311 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -136,41 +136,45 @@ impl Cache { /// perspective. fn refresh_cache(&mut self) { let now = SystemTime::now(); + if let Ok(d) = now.duration_since(self.last_checked) { + if d.as_secs() < 1 { + return; + } + } - match now.duration_since(self.last_checked) { - Ok(d) if d.as_secs() < 1 => (), - Ok(_) | Err(_) => { - let env_tz = env::var("TZ").ok(); - let env_ref = env_tz.as_deref(); - let new_source = Source::new(env_ref); - - let out_of_date = match (&self.source, &new_source) { - // change from env to file or file to env, must recreate the zone - (Source::Environment { .. }, Source::LocalTime { .. }) - | (Source::LocalTime { .. }, Source::Environment { .. }) => true, - // stay as file, but mtime has changed - (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime }) - if old_mtime != mtime => - { - true - } - // stay as env, but hash of variable has changed - (Source::Environment { hash: old_hash }, Source::Environment { hash }) - if old_hash != hash => - { - true - } - // cache can be reused - _ => false, - }; - - if out_of_date { - self.zone = current_zone(env_ref); - } + if self.needs_update() { + let env_tz = env::var("TZ").ok(); + let env_ref = env_tz.as_deref(); + self.source = Source::new(env_ref); + self.zone = current_zone(env_ref); + } + self.last_checked = now; + } - self.last_checked = now; - self.source = new_source; + /// Check if any of the `TZ` environment variable or `/etc/localtime` have changed. + 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) { + // change from env to file or file to env, must recreate the zone + (Source::Environment { .. }, Source::LocalTime { .. }) + | (Source::LocalTime { .. }, Source::Environment { .. }) => true, + // stay as file, but mtime has changed + (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime }) + if old_mtime != mtime => + { + true + } + // stay as env, but hash of variable has changed + (Source::Environment { hash: old_hash }, Source::Environment { hash }) + if old_hash != hash => + { + true } + // cache can be reused + _ => false, } } } From 539f9417fe0010c21cbfaa5dff2e21eda9b3e715 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 13:53:59 +0100 Subject: [PATCH 03/16] Compare mtime of `/etc/localtime` against `Cache::last_checked` --- src/offset/local/unix.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 24995f5311..ccee5c398f 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -35,8 +35,8 @@ thread_local! { } enum Source { - LocalTime { mtime: SystemTime }, Environment { hash: u64 }, + LocalTime, } impl Source { @@ -48,19 +48,7 @@ impl Source { let hash = hasher.finish(); Source::Environment { hash } } - None => match fs::symlink_metadata("/etc/localtime") { - Ok(data) => Source::LocalTime { - // we have to pick a sensible default when the mtime fails - // by picking SystemTime::now() we raise the probability of - // the cache being invalidated if/when the mtime starts working - mtime: data.modified().unwrap_or_else(|_| SystemTime::now()), - }, - Err(_) => { - // as above, now() should be a better default than some constant - // TODO: see if we can improve caching in the case where the fallback is a valid timezone - Source::LocalTime { mtime: SystemTime::now() } - } - }, + None => Source::LocalTime, } } } @@ -162,10 +150,11 @@ impl Cache { (Source::Environment { .. }, Source::LocalTime { .. }) | (Source::LocalTime { .. }, Source::Environment { .. }) => true, // stay as file, but mtime has changed - (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime }) - if old_mtime != mtime => - { - true + (Source::LocalTime, Source::LocalTime) => { + match fs::symlink_metadata("/etc/localtime").and_then(|m| m.modified()) { + Ok(mtime) => mtime > self.last_checked, + Err(_) => false, + } } // stay as env, but hash of variable has changed (Source::Environment { hash: old_hash }, Source::Environment { hash }) From dcdee1eb03c73c6f498ad55bf406065ca474ce0f Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 14:03:34 +0100 Subject: [PATCH 04/16] Simplify `Cache::needs_update` --- src/offset/local/unix.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index ccee5c398f..8a9ffeb224 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -146,24 +146,18 @@ impl Cache { let new_source = Source::new(env_ref); match (&self.source, &new_source) { - // change from env to file or file to env, must recreate the zone - (Source::Environment { .. }, Source::LocalTime { .. }) - | (Source::LocalTime { .. }, Source::Environment { .. }) => true, - // stay as file, but mtime has changed + (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, } } - // stay as env, but hash of variable has changed - (Source::Environment { hash: old_hash }, Source::Environment { hash }) - if old_hash != hash => - { - true - } - // cache can be reused - _ => false, + _ => true, } } } From 5c5085d3b0bbc7b31a30e02d8d2d0409a619d681 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 14:13:51 +0100 Subject: [PATCH 05/16] Const initialize `TZ_INFO` --- src/offset/local/unix.rs | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 8a9ffeb224..e79cceefeb 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -24,19 +24,25 @@ pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTi fn offset(d: &NaiveDateTime, local: bool) -> MappedLocalTime { TZ_INFO.with(|maybe_cache| { - maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local) + maybe_cache.borrow_mut().offset(*d, local) }) } -// we have to store the `Cache` in an option as it can't -// be initialized in a static context. thread_local! { - static TZ_INFO: RefCell> = Default::default(); + static TZ_INFO: RefCell = const { RefCell::new( + Cache { + zone: None, + source: Source::Uninitialized, + last_checked: SystemTime::UNIX_EPOCH, + } + ) }; } +#[derive(PartialEq)] enum Source { Environment { hash: u64 }, LocalTime, + Uninitialized, } impl Source { @@ -54,7 +60,7 @@ impl Source { } struct Cache { - zone: TimeZone, + zone: Option, source: Source, last_checked: SystemTime, } @@ -74,19 +80,6 @@ fn fallback_timezone() -> Option { TimeZone::from_tz_data(&bytes).ok() } -impl Default for Cache { - fn default() -> Cache { - // default to UTC if no local timezone can be found - let env_tz = env::var("TZ").ok(); - let env_ref = env_tz.as_deref(); - Cache { - last_checked: SystemTime::now(), - source: Source::new(env_ref), - zone: current_zone(env_ref), - } - } -} - fn current_zone(var: Option<&str>) -> TimeZone { TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc) } @@ -98,6 +91,8 @@ impl Cache { if !local { let offset = self .zone + .as_ref() + .unwrap() .find_local_time_type(d.and_utc().timestamp()) .expect("unable to select local time type") .offset(); @@ -111,6 +106,8 @@ impl Cache { // we pass through the year as the year of a local point in time must either be valid in that locale, or // the entire time was skipped in which case we will return MappedLocalTime::None anyway. self.zone + .as_ref() + .unwrap() .find_local_time_type_from_local(d.and_utc().timestamp(), d.year()) .expect("unable to select local time type") .and_then(|o| FixedOffset::east_opt(o.offset())) @@ -125,7 +122,7 @@ impl Cache { fn refresh_cache(&mut self) { let now = SystemTime::now(); if let Ok(d) = now.duration_since(self.last_checked) { - if d.as_secs() < 1 { + if d.as_secs() < 1 && self.source != Source::Uninitialized { return; } } @@ -134,7 +131,7 @@ impl Cache { let env_tz = env::var("TZ").ok(); let env_ref = env_tz.as_deref(); self.source = Source::new(env_ref); - self.zone = current_zone(env_ref); + self.zone = Some(current_zone(env_ref)); } self.last_checked = now; } From 95be522a9ebd007aa9e771d854b099440eeb425b Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 14:22:05 +0100 Subject: [PATCH 06/16] Refactor `Cache::offset` --- src/offset/local/unix.rs | 54 ++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index e79cceefeb..63df61b5c1 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -15,16 +15,29 @@ use super::{FixedOffset, NaiveDateTime}; use crate::{Datelike, MappedLocalTime}; pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime { - offset(utc, false) -} + TZ_INFO.with(|cache| { + let mut cache_ref = cache.borrow_mut(); + let tz_info = cache_ref.tz_info(); + let offset = tz_info + .find_local_time_type(utc.and_utc().timestamp()) + .expect("unable to select local time type") + .offset(); -pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime { - offset(local, true) + match FixedOffset::east_opt(offset) { + Some(offset) => MappedLocalTime::Single(offset), + None => MappedLocalTime::None, + } + }) } -fn offset(d: &NaiveDateTime, local: bool) -> MappedLocalTime { - TZ_INFO.with(|maybe_cache| { - maybe_cache.borrow_mut().offset(*d, local) +pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime { + TZ_INFO.with(|cache| { + let mut cache_ref = cache.borrow_mut(); + let tz_info = cache_ref.tz_info(); + tz_info + .find_local_time_type_from_local(local.and_utc().timestamp(), local.year()) + .expect("unable to select local time type") + .and_then(|o| FixedOffset::east_opt(o.offset())) }) } @@ -85,32 +98,9 @@ fn current_zone(var: Option<&str>) -> TimeZone { } impl Cache { - fn offset(&mut self, d: NaiveDateTime, local: bool) -> MappedLocalTime { + fn tz_info(&mut self) -> &TimeZone { self.refresh_cache(); - - if !local { - let offset = self - .zone - .as_ref() - .unwrap() - .find_local_time_type(d.and_utc().timestamp()) - .expect("unable to select local time type") - .offset(); - - return match FixedOffset::east_opt(offset) { - Some(offset) => MappedLocalTime::Single(offset), - None => MappedLocalTime::None, - }; - } - - // we pass through the year as the year of a local point in time must either be valid in that locale, or - // the entire time was skipped in which case we will return MappedLocalTime::None anyway. - self.zone - .as_ref() - .unwrap() - .find_local_time_type_from_local(d.and_utc().timestamp(), d.year()) - .expect("unable to select local time type") - .and_then(|o| FixedOffset::east_opt(o.offset())) + self.zone.as_ref().unwrap() } /// Refresh our cached data if necessary. From fecce91016c992061d2dedda7857b27d226a1d31 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 14:24:46 +0100 Subject: [PATCH 07/16] Move `Source` below `Cache` --- src/offset/local/unix.rs | 62 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 63df61b5c1..f074874786 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -41,37 +41,6 @@ pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTi }) } -thread_local! { - static TZ_INFO: RefCell = const { RefCell::new( - Cache { - zone: None, - source: Source::Uninitialized, - last_checked: SystemTime::UNIX_EPOCH, - } - ) }; -} - -#[derive(PartialEq)] -enum Source { - Environment { hash: u64 }, - LocalTime, - 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, - } - } -} - struct Cache { zone: Option, source: Source, @@ -148,3 +117,34 @@ impl Cache { } } } + +thread_local! { + static TZ_INFO: RefCell = const { RefCell::new( + Cache { + zone: None, + source: Source::Uninitialized, + last_checked: SystemTime::UNIX_EPOCH, + } + ) }; +} + +#[derive(PartialEq)] +enum Source { + Environment { hash: u64 }, + LocalTime, + 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, + } + } +} From 75711e7b4eeac90374440d43933c42c55fe6d739 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 14:41:52 +0100 Subject: [PATCH 08/16] Factor out `Cache::read_tz_info` --- src/offset/local/unix.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index f074874786..a0d4377a44 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -62,10 +62,6 @@ fn fallback_timezone() -> Option { TimeZone::from_tz_data(&bytes).ok() } -fn current_zone(var: Option<&str>) -> TimeZone { - TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc) -} - impl Cache { fn tz_info(&mut self) -> &TimeZone { self.refresh_cache(); @@ -87,10 +83,7 @@ impl Cache { } if self.needs_update() { - let env_tz = env::var("TZ").ok(); - let env_ref = env_tz.as_deref(); - self.source = Source::new(env_ref); - self.zone = Some(current_zone(env_ref)); + self.read_tz_info(); } self.last_checked = now; } @@ -116,6 +109,27 @@ impl Cache { _ => true, } } + + /// Try to get the current time zone data. + /// + /// The following sources are tried in order: + /// - `TZ` environment variable, containing: + /// - the POSIX TZ rule + /// - an absolute path + /// - an IANA time zone name in combination with the platform time zone database + /// - the `/etc/localtime` symlink + /// - 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) { + let env_tz = env::var("TZ").ok(); + self.source = Source::new(env_tz.as_deref()); + self.zone = Some( + TimeZone::local(env_tz.as_deref()) + .ok() + .or_else(fallback_timezone) + .unwrap_or_else(TimeZone::utc), + ); + } } thread_local! { From e7b46ef366e526e38026a302e9a3b5f7a4b40606 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 15:29:31 +0100 Subject: [PATCH 09/16] Add `Cache::read_from_tz_env_or_localtime` --- src/offset/local/unix.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index a0d4377a44..33a7689568 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -124,12 +124,18 @@ impl Cache { let env_tz = env::var("TZ").ok(); self.source = Source::new(env_tz.as_deref()); self.zone = Some( - TimeZone::local(env_tz.as_deref()) + self.read_from_tz_env_or_localtime(env_tz.as_deref()) .ok() .or_else(fallback_timezone) .unwrap_or_else(TimeZone::utc), ); } + + /// Read the `TZ` environment variable or the TZif file that it points to. + /// Read from `/etc/localtime` if the variable is not set. + fn read_from_tz_env_or_localtime(&self, env_tz: Option<&str>) -> Result { + TimeZone::local(env_tz).map_err(|_| ()) + } } thread_local! { From 25e5b1a77360ec3e787e3115a20559e38e204426 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 15:34:23 +0100 Subject: [PATCH 10/16] Move `fallback_timezone` to `Cache::read_with_tz_name` --- src/offset/local/unix.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 33a7689568..8cf8f06b58 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -53,15 +53,6 @@ const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo"; #[cfg(not(any(target_os = "android", target_os = "aix")))] const TZDB_LOCATION: &str = "/usr/share/zoneinfo"; -fn fallback_timezone() -> Option { - let tz_name = iana_time_zone::get_timezone().ok()?; - #[cfg(not(target_os = "android"))] - let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).ok()?; - #[cfg(target_os = "android")] - let bytes = android_tzdata::find_tz_data(&tz_name).ok()?; - TimeZone::from_tz_data(&bytes).ok() -} - impl Cache { fn tz_info(&mut self) -> &TimeZone { self.refresh_cache(); @@ -125,9 +116,8 @@ impl Cache { self.source = Source::new(env_tz.as_deref()); self.zone = Some( self.read_from_tz_env_or_localtime(env_tz.as_deref()) - .ok() - .or_else(fallback_timezone) - .unwrap_or_else(TimeZone::utc), + .or_else(|_| self.read_with_tz_name()) + .unwrap_or_else(|_| TimeZone::utc()), ); } @@ -136,6 +126,17 @@ impl Cache { fn read_from_tz_env_or_localtime(&self, env_tz: Option<&str>) -> Result { TimeZone::local(env_tz).map_err(|_| ()) } + + /// 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(&self) -> Result { + let tz_name = iana_time_zone::get_timezone().map_err(|_| ())?; + #[cfg(not(target_os = "android"))] + let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).map_err(|_| ())?; + #[cfg(target_os = "android")] + let bytes = android_tzdata::find_tz_data(&tz_name).ok()?; + TimeZone::from_tz_data(&bytes).map_err(|_| ()) + } } thread_local! { From d572d33719b7b7fc753e7cc2b9d4fbfd878b68c5 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 15:42:51 +0100 Subject: [PATCH 11/16] Factor out `Cache::read_tzif` --- src/offset/local/unix.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 8cf8f06b58..ef82f30bab 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -8,7 +8,13 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime}; +use std::cell::RefCell; +use std::collections::hash_map; +use std::env; +use std::fs; +use std::hash::Hasher; +use std::path::PathBuf; +use std::time::SystemTime; use super::tz_info::TimeZone; use super::{FixedOffset, NaiveDateTime}; @@ -131,11 +137,25 @@ impl Cache { /// it, and try to read the corresponding TZif data. fn read_with_tz_name(&self) -> Result { let tz_name = iana_time_zone::get_timezone().map_err(|_| ())?; - #[cfg(not(target_os = "android"))] - let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).map_err(|_| ())?; - #[cfg(target_os = "android")] - let bytes = android_tzdata::find_tz_data(&tz_name).ok()?; - TimeZone::from_tz_data(&bytes).map_err(|_| ()) + self.read_tzif(&tz_name) + } + + /// Try to read the TZif data for the specified time zone name. + fn read_tzif(&self, tz_name: &str) -> Result { + let tzif = self.read_tzif_inner(tz_name)?; + TimeZone::from_tz_data(&tzif).map_err(|_| ()) + } + + #[cfg(not(target_os = "android"))] + fn read_tzif_inner(&self, tz_name: &str) -> Result, ()> { + let path = PathBuf::from(TZDB_LOCATION).join(tz_name); + let tzif = fs::read(path).map_err(|_| ())?; + Ok(tzif) + } + #[cfg(target_os = "android")] + fn read_tzif_inner(&self, tz_name: &str) -> Result, ()> { + let tzif = android_tzdata::find_tz_data(&tz_name).map_err(|_| ())?; + Ok(tzif) } } From a27807adb98df14fe00e1043499429b82bac33d5 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 15:46:32 +0100 Subject: [PATCH 12/16] Add `Cache::tzdb_dir` using dirs from `TimeZone::find_tz_file` --- src/offset/local/unix.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index ef82f30bab..512e782fe2 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -53,12 +53,6 @@ struct Cache { last_checked: SystemTime, } -#[cfg(target_os = "aix")] -const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo"; - -#[cfg(not(any(target_os = "android", target_os = "aix")))] -const TZDB_LOCATION: &str = "/usr/share/zoneinfo"; - impl Cache { fn tz_info(&mut self) -> &TimeZone { self.refresh_cache(); @@ -148,7 +142,7 @@ impl Cache { #[cfg(not(target_os = "android"))] fn read_tzif_inner(&self, tz_name: &str) -> Result, ()> { - let path = PathBuf::from(TZDB_LOCATION).join(tz_name); + let path = self.tzdb_dir()?.join(tz_name); let tzif = fs::read(path).map_err(|_| ())?; Ok(tzif) } @@ -157,6 +151,22 @@ impl Cache { let tzif = android_tzdata::find_tz_data(&tz_name).map_err(|_| ())?; Ok(tzif) } + + /// Get the location of the time zone database directory with TZif files. + #[cfg(not(target_os = "android"))] + fn tzdb_dir(&self) -> Result { + // Possible system timezone directories + const ZONE_INFO_DIRECTORIES: [&str; 4] = + ["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"]; + + for dir in &ZONE_INFO_DIRECTORIES { + let path = PathBuf::from(dir); + if path.exists() { + return Ok(path); + } + } + Err(()) + } } thread_local! { From 3c70a817616fa1fa264d2d924bd8759fa7f912d4 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 16:22:45 +0100 Subject: [PATCH 13/16] Split `Cache::read_from_tz_env_or_localtime` --- src/offset/local/tz_info/timezone.rs | 2 +- src/offset/local/unix.rs | 39 ++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs index dbb2def931..62fcff5307 100644 --- a/src/offset/local/tz_info/timezone.rs +++ b/src/offset/local/tz_info/timezone.rs @@ -33,7 +33,7 @@ impl TimeZone { } /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). - fn from_posix_tz(tz_string: &str) -> Result { + pub(crate) fn from_posix_tz(tz_string: &str) -> Result { if tz_string.is_empty() { return Err(Error::InvalidTzString("empty TZ string")); } diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 512e782fe2..7c75c744e6 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -114,30 +114,47 @@ impl Cache { fn read_tz_info(&mut self) { let env_tz = env::var("TZ").ok(); self.source = Source::new(env_tz.as_deref()); - self.zone = Some( - self.read_from_tz_env_or_localtime(env_tz.as_deref()) - .or_else(|_| self.read_with_tz_name()) - .unwrap_or_else(|_| TimeZone::utc()), - ); + if let Some(env_tz) = env_tz { + if self.read_from_tz_env(&env_tz).is_ok() { + return; + } + } + #[cfg(not(target_os = "android"))] + if self.read_from_symlink().is_ok() { + return; + } + if self.read_with_tz_name().is_ok() { + return; + } + self.zone = Some(TimeZone::utc()); } /// Read the `TZ` environment variable or the TZif file that it points to. - /// Read from `/etc/localtime` if the variable is not set. - fn read_from_tz_env_or_localtime(&self, env_tz: Option<&str>) -> Result { - TimeZone::local(env_tz).map_err(|_| ()) + fn read_from_tz_env(&mut self, tz_var: &str) -> Result<(), ()> { + self.zone = Some(TimeZone::from_posix_tz(tz_var).map_err(|_| ())?); + Ok(()) + } + + /// 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(|_| ())?); + Ok(()) } /// 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(&self) -> Result { + fn read_with_tz_name(&mut self) -> Result<(), ()> { let tz_name = iana_time_zone::get_timezone().map_err(|_| ())?; self.read_tzif(&tz_name) } /// Try to read the TZif data for the specified time zone name. - fn read_tzif(&self, tz_name: &str) -> Result { + fn read_tzif(&mut self, tz_name: &str) -> Result<(), ()> { let tzif = self.read_tzif_inner(tz_name)?; - TimeZone::from_tz_data(&tzif).map_err(|_| ()) + self.zone = Some(TimeZone::from_tz_data(&tzif).map_err(|_| ())?); + Ok(()) } #[cfg(not(target_os = "android"))] From ecd60ccb74956665cffeb46575c49b04e936008e Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 16:45:33 +0100 Subject: [PATCH 14/16] Move parsing of `TZ` variable to `unix` module --- src/offset/local/tz_info/timezone.rs | 103 +-------------------------- src/offset/local/unix.rs | 59 +++++++++++++-- 2 files changed, 53 insertions(+), 109 deletions(-) diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs index 62fcff5307..5ecbce88f5 100644 --- a/src/offset/local/tz_info/timezone.rs +++ b/src/offset/local/tz_info/timezone.rs @@ -1,8 +1,5 @@ //! Types related to a time zone. -use std::fs::{self, File}; -use std::io::{self, Read}; -use std::path::{Path, PathBuf}; use std::{cmp::Ordering, fmt, str}; use super::rule::{AlternateTime, TransitionRule}; @@ -22,43 +19,8 @@ pub(crate) struct TimeZone { } impl TimeZone { - /// Returns local time zone. - /// - /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. - pub(crate) fn local(env_tz: Option<&str>) -> Result { - match env_tz { - Some(tz) => Self::from_posix_tz(tz), - None => Self::from_posix_tz("localtime"), - } - } - /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). - pub(crate) fn from_posix_tz(tz_string: &str) -> Result { - if tz_string.is_empty() { - return Err(Error::InvalidTzString("empty TZ string")); - } - - if tz_string == "localtime" { - return Self::from_tz_data(&fs::read("/etc/localtime")?); - } - - // attributes are not allowed on if blocks in Rust 1.38 - #[cfg(target_os = "android")] - { - if let Ok(bytes) = android_tzdata::find_tz_data(tz_string) { - return Self::from_tz_data(&bytes); - } - } - - let mut chars = tz_string.chars(); - if chars.next() == Some(':') { - return Self::from_file(&mut find_tz_file(chars.as_str())?); - } - - if let Ok(mut file) = find_tz_file(tz_string) { - return Self::from_file(&mut file); - } - + pub(crate) fn from_tz_string(tz_string: &str) -> Result { // TZ string extensions are not allowed let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace()); let rule = TransitionRule::from_tz_string(tz_string.as_bytes(), false)?; @@ -85,13 +47,6 @@ impl TimeZone { Ok(new) } - /// Construct a time zone from the contents of a time zone file - fn from_file(file: &mut File) -> Result { - let mut bytes = Vec::new(); - file.read_to_end(&mut bytes)?; - Self::from_tz_data(&bytes) - } - /// Construct a time zone from the contents of a time zone file /// /// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536). @@ -606,34 +561,6 @@ impl LocalTimeType { pub(super) const UTC: LocalTimeType = Self { ut_offset: 0, is_dst: false, name: None }; } -/// Open the TZif file corresponding to a TZ string -fn find_tz_file(path: impl AsRef) -> Result { - // Don't check system timezone directories on non-UNIX platforms - #[cfg(not(unix))] - return Ok(File::open(path)?); - - #[cfg(unix)] - { - let path = path.as_ref(); - if path.is_absolute() { - return Ok(File::open(path)?); - } - - for folder in &ZONE_INFO_DIRECTORIES { - if let Ok(file) = File::open(PathBuf::from(folder).join(path)) { - return Ok(file); - } - } - - Err(Error::Io(io::ErrorKind::NotFound.into())) - } -} - -// Possible system timezone directories -#[cfg(unix)] -const ZONE_INFO_DIRECTORIES: [&str; 4] = - ["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"]; - /// Number of seconds in one week pub(crate) const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK; /// Number of seconds in 28 days @@ -844,34 +771,6 @@ mod tests { Ok(()) } - #[test] - fn test_time_zone_from_posix_tz() -> Result<(), Error> { - #[cfg(unix)] - { - // if the TZ var is set, this essentially _overrides_ the - // time set by the localtime symlink - // so just ensure that ::local() acts as expected - // in this case - if let Ok(tz) = std::env::var("TZ") { - let time_zone_local = TimeZone::local(Some(tz.as_str()))?; - let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?; - assert_eq!(time_zone_local, time_zone_local_1); - } - - // `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have - // a time zone database, like for example some docker containers. - // In that case skip the test. - if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC") { - assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0); - } - } - - assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err()); - assert!(TimeZone::from_posix_tz("").is_err()); - - Ok(()) - } - #[test] fn test_leap_seconds() -> Result<(), Error> { let time_zone = TimeZone::new( diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 7c75c744e6..f20dae1bc2 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -13,7 +13,7 @@ use std::collections::hash_map; use std::env; use std::fs; use std::hash::Hasher; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::SystemTime; use super::tz_info::TimeZone; @@ -112,10 +112,11 @@ impl Cache { /// - 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) { - let env_tz = env::var("TZ").ok(); - self.source = Source::new(env_tz.as_deref()); - if let Some(env_tz) = env_tz { - if self.read_from_tz_env(&env_tz).is_ok() { + 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; } } @@ -130,8 +131,20 @@ impl Cache { } /// Read the `TZ` environment variable or the TZif file that it points to. - fn read_from_tz_env(&mut self, tz_var: &str) -> Result<(), ()> { - self.zone = Some(TimeZone::from_posix_tz(tz_var).map_err(|_| ())?); + 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(|_| ())?); + } + 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(|_| ())?); + } + TzEnvVar::TzName(tz_id) => self.read_tzif(&tz_id[1..])?, + #[cfg(not(target_os = "android"))] + TzEnvVar::LocaltimeSymlink => self.read_from_symlink()?, + }; Ok(()) } @@ -216,3 +229,35 @@ impl Source { } } } + +/// Type of the `TZ` environment variable. +/// +/// Supported formats are: +/// - a POSIX TZ string +/// - an absolute path (starting with `:/`, as supported by glibc and others) +/// - a time zone name (starting with `:`, as supported by glibc and others) +/// - "localtime" (supported by Solaris and maybe others) +enum TzEnvVar { + TzString(String), + Path(String), // Value still starts with `:` + TzName(String), // Value still starts with `:` + #[cfg(not(target_os = "android"))] + LocaltimeSymlink, +} + +impl TzEnvVar { + /// Get the current value of the `TZ` environment variable and determine its format. + fn get() -> Option { + match env::var("TZ").ok() { + None => None, + Some(s) if s.is_empty() => None, + #[cfg(not(target_os = "android"))] + Some(s) if s == "localtime" => Some(TzEnvVar::LocaltimeSymlink), + Some(tz_var) => match tz_var.strip_prefix(':') { + Some(path) if Path::new(&path).is_absolute() => Some(TzEnvVar::Path(tz_var)), + Some(_) => Some(TzEnvVar::TzName(tz_var)), + None => Some(TzEnvVar::TzString(tz_var)), + }, + } + } +} From 470b1ced9e35da6df88cebf5ced66621c736d866 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 16:46:48 +0100 Subject: [PATCH 15/16] Rename `Cache` to `CachedTzInfo` --- src/offset/local/unix.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index f20dae1bc2..acf46f353f 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -47,13 +47,13 @@ pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTi }) } -struct Cache { +struct CachedTzInfo { zone: Option, source: Source, last_checked: SystemTime, } -impl Cache { +impl CachedTzInfo { fn tz_info(&mut self) -> &TimeZone { self.refresh_cache(); self.zone.as_ref().unwrap() @@ -200,8 +200,8 @@ impl Cache { } thread_local! { - static TZ_INFO: RefCell = const { RefCell::new( - Cache { + static TZ_INFO: RefCell = const { RefCell::new( + CachedTzInfo { zone: None, source: Source::Uninitialized, last_checked: SystemTime::UNIX_EPOCH, From 9ba3069f50b3845ab49d34500e4667277e69ae31 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Tue, 19 Mar 2024 15:47:10 +0100 Subject: [PATCH 16/16] Check `TZDIR` environment variable --- src/offset/local/unix.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index acf46f353f..4080ba3803 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -189,6 +189,16 @@ impl CachedTzInfo { const ZONE_INFO_DIRECTORIES: [&str; 4] = ["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"]; + // Use the value of the `TZDIR` environment variable if set. + if let Some(tz_dir) = env::var_os("TZDIR") { + if !tz_dir.is_empty() { + let path = PathBuf::from(tz_dir); + if path.exists() { + return Ok(path); + } + } + } + for dir in &ZONE_INFO_DIRECTORIES { let path = PathBuf::from(dir); if path.exists() {