use core::cmp::Ordering;
use alloc::{
string::{String, ToString},
vec,
vec::Vec,
};
use std::{
fs::File,
io::Read,
path::{Path, PathBuf},
sync::{Arc, RwLock},
time::Duration,
};
use crate::{
error::{err, Error},
timestamp::Timestamp,
tz::{tzif::is_possibly_tzif, TimeZone},
util::{cache::Expiration, parse},
};
const DEFAULT_TTL: Duration = Duration::new(5 * 60, 0);
static ZONEINFO_DIRECTORIES: &[&str] =
&["/usr/share/zoneinfo", "/etc/zoneinfo"];
pub(crate) struct ZoneInfo {
dir: Option<PathBuf>,
names: Option<ZoneInfoNames>,
zones: RwLock<CachedZones>,
}
impl ZoneInfo {
pub(crate) fn from_env() -> ZoneInfo {
if let Some(tzdir) = std::env::var_os("TZDIR") {
let tzdir = PathBuf::from(tzdir);
debug!("opening zoneinfo database at TZDIR={}", tzdir.display());
match ZoneInfo::from_dir(&tzdir) {
Ok(db) => return db,
Err(_err) => {
warn!("failed opening TZDIR={}: {_err}", tzdir.display());
}
}
}
for dir in ZONEINFO_DIRECTORIES {
let tzdir = Path::new(dir);
debug!("opening zoneinfo database at {}", tzdir.display());
match ZoneInfo::from_dir(&tzdir) {
Ok(db) => return db,
Err(_err) => {
debug!("failed opening {}: {_err}", tzdir.display());
}
}
}
warn!(
"could not find zoneinfo database at any of the following \
paths: {}",
ZONEINFO_DIRECTORIES.join(", "),
);
ZoneInfo::none()
}
pub(crate) fn from_dir(dir: &Path) -> Result<ZoneInfo, Error> {
let names = Some(ZoneInfoNames::new(dir)?);
let zones = RwLock::new(CachedZones::new());
Ok(ZoneInfo { dir: Some(dir.to_path_buf()), names, zones })
}
fn none() -> ZoneInfo {
let dir = None;
let names = None;
let zones = RwLock::new(CachedZones::new());
ZoneInfo { dir, names, zones }
}
pub(crate) fn reset(&self) {
let mut zones = self.zones.write().unwrap();
if let Some(ref names) = self.names {
names.reset();
}
zones.reset();
}
pub(crate) fn get(&self, query: &str) -> Option<TimeZone> {
if query == "UTC" {
return Some(TimeZone::UTC);
}
let names = self.names.as_ref()?;
{
let zones = self.zones.read().unwrap();
if let Some(czone) = zones.get(query) {
if !czone.is_expired() {
return Some(czone.tz.clone());
}
}
}
let info = names.get(query)?;
let mut zones = self.zones.write().unwrap();
let ttl = zones.ttl;
match zones.get_zone_index(query) {
Ok(i) => {
let czone = &mut zones.zones[i];
if czone.revalidate(&info, ttl) {
return Some(czone.tz.clone());
}
let czone = match CachedTimeZone::new(&info, zones.ttl) {
Ok(czone) => czone,
Err(_err) => {
warn!(
"failed to re-cache time zone from file {}: {_err}",
info.inner.full.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones[i] = czone;
Some(tz)
}
Err(i) => {
let czone = match CachedTimeZone::new(&info, ttl) {
Ok(czone) => czone,
Err(_err) => {
warn!(
"failed to cache time zone from file {}: {_err}",
info.inner.full.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones.insert(i, czone);
Some(tz)
}
}
}
pub(crate) fn available(&self) -> Vec<String> {
let Some(names) = self.names.as_ref() else { return vec![] };
names.available()
}
pub(crate) fn is_definitively_empty(&self) -> bool {
self.names.is_none()
}
}
impl core::fmt::Debug for ZoneInfo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ZoneInfo(")?;
if let Some(ref dir) = self.dir {
write!(f, "{}", dir.display())?;
} else {
write!(f, "unavailable")?;
}
write!(f, ")")
}
}
#[derive(Debug)]
struct CachedZones {
zones: Vec<CachedTimeZone>,
ttl: Duration,
}
impl CachedZones {
const DEFAULT_TTL: Duration = DEFAULT_TTL;
fn new() -> CachedZones {
CachedZones { zones: vec![], ttl: CachedZones::DEFAULT_TTL }
}
fn get(&self, query: &str) -> Option<&CachedTimeZone> {
self.get_zone_index(query).ok().map(|i| &self.zones[i])
}
fn get_zone_index(&self, query: &str) -> Result<usize, usize> {
self.zones.binary_search_by(|zone| {
cmp_ignore_ascii_case(zone.tz.diagnostic_name(), query)
})
}
fn reset(&mut self) {
self.zones.clear();
}
}
#[derive(Clone, Debug)]
struct CachedTimeZone {
tz: TimeZone,
expiration: Expiration,
last_modified: Option<Timestamp>,
}
impl CachedTimeZone {
fn new(
info: &ZoneInfoName,
ttl: Duration,
) -> Result<CachedTimeZone, Error> {
let path = &info.inner.full;
let mut file = File::open(path).map_err(|e| Error::fs(path, e))?;
let mut data = vec![];
file.read_to_end(&mut data).map_err(|e| Error::fs(path, e))?;
let tz = TimeZone::tzif(&info.inner.original, &data)
.map_err(|e| e.path(path))?;
let last_modified = last_modified_from_file(path, &file);
let expiration = Expiration::after(ttl);
Ok(CachedTimeZone { tz, expiration, last_modified })
}
fn is_expired(&self) -> bool {
self.expiration.is_expired()
}
fn revalidate(&mut self, info: &ZoneInfoName, ttl: Duration) -> bool {
let Some(old_last_modified) = self.last_modified else {
info!(
"revalidation for {} failed because old last modified time \
is unavailable",
info.inner.full.display(),
);
return false;
};
let Some(new_last_modified) =
last_modified_from_path(&info.inner.full)
else {
info!(
"revalidation for {} failed because new last modified time \
is unavailable",
info.inner.full.display(),
);
return false;
};
if old_last_modified != new_last_modified {
info!(
"revalidation for {} failed because last modified times \
do not match: old = {} != {} = new",
info.inner.full.display(),
old_last_modified,
new_last_modified,
);
return false;
}
trace!(
"revalidation for {} succeeded because last modified times \
match: old = {} == {} = new",
info.inner.full.display(),
old_last_modified,
new_last_modified,
);
self.expiration = Expiration::after(ttl);
true
}
}
#[derive(Debug)]
struct ZoneInfoNames {
inner: RwLock<ZoneInfoNamesInner>,
}
#[derive(Debug)]
struct ZoneInfoNamesInner {
dir: PathBuf,
names: Vec<ZoneInfoName>,
ttl: Duration,
expiration: Expiration,
}
impl ZoneInfoNames {
const DEFAULT_TTL: Duration = DEFAULT_TTL;
fn new(dir: &Path) -> Result<ZoneInfoNames, Error> {
let names = walk(dir)?;
let dir = dir.to_path_buf();
let ttl = ZoneInfoNames::DEFAULT_TTL;
let expiration = Expiration::after(ttl);
let inner = ZoneInfoNamesInner { dir, names, ttl, expiration };
Ok(ZoneInfoNames { inner: RwLock::new(inner) })
}
fn get(&self, query: &str) -> Option<ZoneInfoName> {
{
let inner = self.inner.read().unwrap();
if let Some(zone_info_name) = inner.get(query) {
return Some(zone_info_name);
}
drop(inner); }
let mut inner = self.inner.write().unwrap();
inner.attempt_refresh();
inner.get(query)
}
fn available(&self) -> Vec<String> {
let mut inner = self.inner.write().unwrap();
inner.attempt_refresh();
inner.available()
}
fn reset(&self) {
self.inner.write().unwrap().reset();
}
}
impl ZoneInfoNamesInner {
fn get(&self, query: &str) -> Option<ZoneInfoName> {
self.names
.binary_search_by(|n| cmp_ignore_ascii_case(&n.inner.lower, query))
.ok()
.map(|i| self.names[i].clone())
}
fn available(&self) -> Vec<String> {
self.names.iter().map(|n| n.inner.original.clone()).collect()
}
fn attempt_refresh(&mut self) {
if self.expiration.is_expired() {
self.refresh();
}
}
fn refresh(&mut self) {
let result = walk(&self.dir);
self.expiration = Expiration::after(self.ttl);
match result {
Ok(names) => {
self.names = names;
}
Err(_err) => {
warn!(
"failed to refresh zoneinfo time zone name cache \
for {}: {_err}",
self.dir.display(),
)
}
}
}
fn reset(&mut self) {
self.names.clear();
self.expiration = Expiration::expired();
}
}
#[derive(Clone, Debug)]
struct ZoneInfoName {
inner: Arc<ZoneInfoNameInner>,
}
#[derive(Clone, Debug)]
struct ZoneInfoNameInner {
full: PathBuf,
original: String,
lower: String,
}
impl ZoneInfoName {
fn new(base: &Path, time_zone_name: &Path) -> Result<ZoneInfoName, Error> {
let full = base.join(time_zone_name);
let original = parse::os_str_utf8(time_zone_name.as_os_str())
.map_err(|err| err.path(base))?;
let lower = original.to_ascii_lowercase();
let inner =
ZoneInfoNameInner { full, original: original.to_string(), lower };
Ok(ZoneInfoName { inner: Arc::new(inner) })
}
}
impl Eq for ZoneInfoName {}
impl PartialEq for ZoneInfoName {
fn eq(&self, rhs: &ZoneInfoName) -> bool {
self.inner.lower == rhs.inner.lower
}
}
impl Ord for ZoneInfoName {
fn cmp(&self, rhs: &ZoneInfoName) -> core::cmp::Ordering {
self.inner.lower.cmp(&rhs.inner.lower)
}
}
impl PartialOrd for ZoneInfoName {
fn partial_cmp(&self, rhs: &ZoneInfoName) -> Option<core::cmp::Ordering> {
Some(self.cmp(rhs))
}
}
impl core::hash::Hash for ZoneInfoName {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.inner.lower.hash(state);
}
}
fn last_modified_from_path(path: &Path) -> Option<Timestamp> {
let file = match File::open(path) {
Ok(file) => file,
Err(_err) => {
warn!(
"failed to open file to get last modified time {}: {_err}",
path.display(),
);
return None;
}
};
last_modified_from_file(path, &file)
}
fn last_modified_from_file(_path: &Path, file: &File) -> Option<Timestamp> {
let md = match file.metadata() {
Ok(md) => md,
Err(_err) => {
warn!(
"failed to get metadata (for last modified time) \
for {}: {_err}",
_path.display(),
);
return None;
}
};
let systime = match md.modified() {
Ok(systime) => systime,
Err(_err) => {
warn!(
"failed to get last modified time for {}: {_err}",
_path.display()
);
return None;
}
};
let timestamp = match Timestamp::try_from(systime) {
Ok(timestamp) => timestamp,
Err(_err) => {
warn!(
"system time {systime:?} out of bounds \
for Jiff timestamp for last modified time \
from {}: {_err}",
_path.display(),
);
return None;
}
};
Some(timestamp)
}
fn walk(start: &Path) -> Result<Vec<ZoneInfoName>, Error> {
let mut first_err: Option<Error> = None;
let mut seterr = |path: &Path, err: Error| {
if first_err.is_none() {
first_err = Some(err.path(path));
}
};
let mut names = vec![];
let mut stack = vec![start.to_path_buf()];
while let Some(dir) = stack.pop() {
let readdir = match dir.read_dir() {
Ok(readdir) => readdir,
Err(err) => {
info!(
"error when reading {} as a directory: {err}",
dir.display()
);
seterr(&dir, Error::io(err));
continue;
}
};
for result in readdir {
let dent = match result {
Ok(dent) => dent,
Err(err) => {
info!(
"error when reading directory entry from {}: {err}",
dir.display()
);
seterr(&dir, Error::io(err));
continue;
}
};
let file_type = match dent.file_type() {
Ok(file_type) => file_type,
Err(err) => {
let path = dent.path();
info!(
"error when reading file type from {}: {err}",
path.display()
);
seterr(&path, Error::io(err));
continue;
}
};
let path = dent.path();
if file_type.is_dir() {
stack.push(path);
continue;
}
let mut f = match File::open(&path) {
Ok(f) => f,
Err(err) => {
info!("failed to open {}: {err}", path.display());
seterr(&path, Error::io(err));
continue;
}
};
let mut buf = [0; 4];
if let Err(err) = f.read_exact(&mut buf) {
info!(
"failed to read first 4 bytes of {}: {err}",
path.display()
);
seterr(&path, Error::io(err));
continue;
}
if !is_possibly_tzif(&buf) {
trace!(
"found file {} that isn't TZif since its first \
four bytes are {:?}",
path.display(),
crate::util::escape::Bytes(&buf),
);
continue;
}
let time_zone_name = match path.strip_prefix(start) {
Ok(time_zone_name) => time_zone_name,
Err(err) => {
info!(
"failed to extract time zone name from {} \
using {} as a base: {err}",
path.display(),
start.display(),
);
seterr(&path, Error::adhoc(err));
continue;
}
};
let zone_info_name =
match ZoneInfoName::new(&start, time_zone_name) {
Ok(zone_info_name) => zone_info_name,
Err(err) => {
seterr(&path, err);
continue;
}
};
names.push(zone_info_name);
}
}
if names.is_empty() {
let err = first_err
.take()
.unwrap_or_else(|| err!("{}: no TZif files", start.display()));
Err(err)
} else {
names.sort();
Ok(names)
}
}
fn cmp_ignore_ascii_case(s1: &str, s2: &str) -> Ordering {
let (bytes1, bytes2) = (s1.as_bytes(), s2.as_bytes());
let mut i = 0;
loop {
let b1 = bytes1.get(i).copied().map(|b| b.to_ascii_lowercase());
let b2 = bytes2.get(i).copied().map(|b| b.to_ascii_lowercase());
match (b1, b2) {
(None, None) => return Ordering::Equal,
(Some(_), None) => return Ordering::Greater,
(None, Some(_)) => return Ordering::Less,
(Some(b1), Some(b2)) if b1 == b2 => i += 1,
(Some(b1), Some(b2)) => return b1.cmp(&b2),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_zoneinfo_walk() -> anyhow::Result<()> {
let _ = crate::logging::Logger::init();
const ENV: &str = "JIFF_DEBUG_ZONEINFO_DIR";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let dir = PathBuf::from(val);
let names = walk(&dir)?;
for n in names {
std::eprintln!("{}", n.inner.original);
}
Ok(())
}
}