use std::{sync::RwLock, time::Duration};
use alloc::string::ToString;
use crate::{
error::{err, Error, ErrorContext},
tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase},
util::cache::Expiration,
};
#[cfg(all(unix, not(target_os = "android")))]
#[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(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(
"TZ environment variable set, but failed to read value",
));
}
}
if let Some(tz) = sys::get(db) {
return Ok(tz);
}
Err(err!("failed to find system time zone"))
}
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_else(|| {
err!(
"failed to parse {tzenv:?} as a POSIX TZ transition \
string, or as valid UTF-8",
)
})?
.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(|| {
err!(
"failed to read TZ={tz_name_or_path:?} \
as a TZif file after attempting a tzdb \
lookup for `{tz_name_or_path}`",
)
})
.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(|| {
err!(
"failed to read TZ={tz_name_or_path:?} \
as a TZif file after attempting a tzdb \
lookup for `{name}`",
)
})
.map(Some)
}
fn read_unnamed_tzif_file(path: &str) -> Result<TimeZone, Error> {
let data = std::fs::read(path)
.map_err(Error::io)
.with_context(|| err!("failed to read {path:?} as TZif file"))?;
let tz = TimeZone::tzif_system(&data)
.with_context(|| err!("found invalid TZif data at {path:?}"))?;
Ok(tz)
}