use std::{sync::RwLock, time::Duration};
use alloc::string::ToString;
use crate::{
error::{tz::system::Error as E, Error, ErrorContext},
tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase},
util::cache::Expiration,
};
#[cfg(all(unix, not(any(target_os = "android", target_os = "emscripten"))))]
#[path = "unix.rs"]
mod sys;
#[cfg(all(unix, target_os = "android"))]
#[path = "android.rs"]
mod sys;
#[cfg(windows)]
#[path = "windows/mod.rs"]
mod sys;
#[cfg(all(
feature = "js",
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
))]
#[path = "wasm_js.rs"]
mod sys;
#[cfg(all(target_family = "wasm", target_os = "emscripten"))]
#[path = "wasm_emscripten.rs"]
mod sys;
#[cfg(not(any(
unix,
windows,
all(
feature = "js",
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown"
)
)))]
mod sys {
use crate::tz::{TimeZone, TimeZoneDatabase};
pub(super) fn get(_db: &TimeZoneDatabase) -> Option<TimeZone> {
warn!("getting system time zone on this platform is unsupported");
None
}
pub(super) fn read(
_db: &TimeZoneDatabase,
path: &str,
) -> Option<TimeZone> {
match super::read_unnamed_tzif_file(path) {
Ok(tz) => Some(tz),
Err(_err) => {
debug!("failed to read {path} as unnamed time zone: {_err}");
None
}
}
}
}
static TTL: Duration = Duration::new(5 * 60, 0);
static CACHE: RwLock<Cache> = RwLock::new(Cache::empty());
struct Cache {
tz: Option<TimeZone>,
expiration: Expiration,
}
impl Cache {
const fn empty() -> Cache {
Cache { tz: None, expiration: Expiration::expired() }
}
}
pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
{
let cache = CACHE.read().unwrap();
if let Some(ref tz) = cache.tz {
if !cache.expiration.is_expired() {
return Ok(tz.clone());
}
}
}
let tz = get_force(db)?;
{
let mut cache = CACHE.write().unwrap();
cache.tz = Some(tz.clone());
cache.expiration = Expiration::after(TTL);
}
Ok(tz)
}
pub(crate) fn get_force(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
match get_env_tz(db) {
Ok(Some(tz)) => {
debug!("checked `TZ` environment variable and found {tz:?}");
return Ok(tz);
}
Ok(None) => {
debug!("`TZ` environment variable is not set");
}
Err(err) => {
return Err(err.context(E::FailedEnvTz));
}
}
if let Some(tz) = sys::get(db) {
return Ok(tz);
}
Err(Error::from(E::FailedSystemTimeZone))
}
fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
let Some(tzenv) = std::env::var_os("TZ") else { return Ok(None) };
if tzenv.is_empty() {
debug!(
"`TZ` environment variable set to empty value, \
assuming `TZ=UTC` in order to conform to \
widespread convention among Unix tooling",
);
return Ok(Some(TimeZone::UTC));
}
let tz_name_or_path = match PosixTzEnv::parse_os_str(&tzenv) {
Err(_err) => {
debug!(
"failed to parse {tzenv:?} as POSIX TZ rule \
(attempting to treat it as an IANA time zone): {_err}",
);
tzenv.to_str().ok_or(E::FailedPosixTzAndUtf8)?.to_string()
}
Ok(PosixTzEnv::Implementation(string)) => string.to_string(),
Ok(PosixTzEnv::Rule(tz)) => {
return Ok(Some(TimeZone::from_posix_tz(tz)))
}
};
let needle = "zoneinfo/";
let Some(rpos) = tz_name_or_path.rfind(needle) else {
debug!(
"could not find {needle:?} in `TZ={tz_name_or_path:?}`, \
therefore attempting lookup in `{db:?}`",
);
return match db.get(&tz_name_or_path) {
Ok(tz) => Ok(Some(tz)),
Err(_err) => {
debug!(
"using `TZ={tz_name_or_path:?}` as time zone name failed, \
could not find time zone in zoneinfo database `{db:?}` \
(continuing to try and read `{tz_name_or_path}` as \
a TZif file)",
);
sys::read(db, &tz_name_or_path)
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
.map(Some)
}
};
};
let name = &tz_name_or_path[rpos + needle.len()..];
debug!(
"extracted `{name}` from `TZ={tz_name_or_path}` \
and assuming it is an IANA time zone name",
);
match db.get(&name) {
Ok(tz) => return Ok(Some(tz)),
Err(_err) => {
debug!(
"using `{name}` from `TZ={tz_name_or_path}`, \
could not find time zone in zoneinfo database `{db:?}` \
(continuing to try and use `{tz_name_or_path}`)",
);
}
}
sys::read(db, &tz_name_or_path)
.ok_or_else(|| Error::from(E::FailedEnvTzAsTzif))
.map(Some)
}
fn read_unnamed_tzif_file(path: &str) -> Result<TimeZone, Error> {
let data = std::fs::read(path)
.map_err(Error::io)
.context(E::FailedUnnamedTzifRead)?;
let tz =
TimeZone::tzif_system(&data).context(E::FailedUnnamedTzifInvalid)?;
Ok(tz)
}