use alloc::{
string::{String, ToString},
vec,
vec::Vec,
};
use std::{
ffi::OsString,
fs::File,
path::{Path, PathBuf},
sync::{Arc, RwLock},
time::Duration,
};
use crate::{
error::{tz::db::Error as E, Error},
timestamp::Timestamp,
tz::{
concatenated::ConcatenatedTzif, db::special_time_zone, TimeZone,
TimeZoneNameIter,
},
util::{self, array_str::ArrayStr, cache::Expiration, utf8},
};
const DEFAULT_TTL: Duration = Duration::new(5 * 60, 0);
static TZDATA_LOCATIONS: &[TzdataLocation] = &[
TzdataLocation::Env {
name: "ANDROID_ROOT",
default: "/system",
suffix: "usr/share/zoneinfo/tzdata",
},
TzdataLocation::Env {
name: "ANDROID_DATA",
default: "/data/misc",
suffix: "zoneinfo/current/tzdata",
},
];
pub(crate) struct Database {
path: Option<PathBuf>,
names: Option<Names>,
zones: RwLock<CachedZones>,
}
impl Database {
pub(crate) fn from_env() -> Database {
let mut attempted = vec![];
for loc in TZDATA_LOCATIONS {
let path = loc.to_path_buf();
trace!(
"opening concatenated tzdata database at {}",
path.display()
);
match Database::from_path(&path) {
Ok(db) => return db,
Err(_err) => {
trace!("failed opening {}: {_err}", path.display());
}
}
attempted.push(path.to_string_lossy().into_owned());
}
debug!(
"could not find concatenated tzdata database at any of the \
following paths: {}",
attempted.join(", "),
);
Database::none()
}
pub(crate) fn from_path(path: &Path) -> Result<Database, Error> {
let names = Some(Names::new(path)?);
let zones = RwLock::new(CachedZones::new());
Ok(Database { path: Some(path.to_path_buf()), names, zones })
}
pub(crate) fn none() -> Database {
let path = None;
let names = None;
let zones = RwLock::new(CachedZones::new());
Database { path, 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 let Some(tz) = special_time_zone(query) {
return Some(tz);
}
let path = self.path.as_ref()?;
{
let zones = self.zones.read().unwrap();
if let Some(czone) = zones.get(query) {
if !czone.is_expired() {
trace!(
"for time zone query `{query}`, \
found cached zone `{}` \
(expiration={}, last_modified={:?})",
czone.tz.diagnostic_name(),
czone.expiration,
czone.last_modified,
);
return Some(czone.tz.clone());
}
}
}
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(path, ttl) {
return Some(czone.tz.clone());
}
let (scratch1, scratch2) = zones.scratch();
let czone = match CachedTimeZone::new(
path, query, ttl, scratch1, scratch2,
) {
Ok(Some(czone)) => czone,
Ok(None) => return None,
Err(_err) => {
warn!(
"failed to re-cache time zone {query} \
from {path}: {_err}",
path = path.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones[i] = czone;
Some(tz)
}
Err(i) => {
let (scratch1, scratch2) = zones.scratch();
let czone = match CachedTimeZone::new(
path, query, ttl, scratch1, scratch2,
) {
Ok(Some(czone)) => czone,
Ok(None) => return None,
Err(_err) => {
warn!(
"failed to cache time zone {query} \
from {path}: {_err}",
path = path.display(),
);
return None;
}
};
let tz = czone.tz.clone();
zones.zones.insert(i, czone);
Some(tz)
}
}
}
pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
let Some(path) = self.path.as_ref() else {
return TimeZoneNameIter::empty();
};
let Some(names) = self.names.as_ref() else {
return TimeZoneNameIter::empty();
};
TimeZoneNameIter::from_iter(names.available(path).into_iter())
}
pub(crate) fn is_definitively_empty(&self) -> bool {
self.names.is_none()
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("Concatenated(")?;
if let Some(ref path) = self.path {
path.display().fmt(f)?;
} else {
f.write_str("unavailable")?;
}
f.write_str(")")
}
}
#[derive(Debug)]
struct CachedZones {
zones: Vec<CachedTimeZone>,
ttl: Duration,
scratch1: Vec<u8>,
scratch2: Vec<u8>,
}
impl CachedZones {
const DEFAULT_TTL: Duration = DEFAULT_TTL;
fn new() -> CachedZones {
CachedZones {
zones: vec![],
ttl: CachedZones::DEFAULT_TTL,
scratch1: vec![],
scratch2: vec![],
}
}
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| {
utf8::cmp_ignore_ascii_case(zone.name(), query)
})
}
fn reset(&mut self) {
self.zones.clear();
}
fn scratch(&mut self) -> (&mut Vec<u8>, &mut Vec<u8>) {
(&mut self.scratch1, &mut self.scratch2)
}
}
#[derive(Clone, Debug)]
struct CachedTimeZone {
tz: TimeZone,
expiration: Expiration,
last_modified: Option<Timestamp>,
}
impl CachedTimeZone {
fn new(
path: &Path,
query: &str,
ttl: Duration,
scratch1: &mut Vec<u8>,
scratch2: &mut Vec<u8>,
) -> Result<Option<CachedTimeZone>, Error> {
let file = File::open(path).map_err(|e| Error::io(e).path(path))?;
let db = ConcatenatedTzif::open(&file)?;
let Some(tz) = db.get(query, scratch1, scratch2)? else {
return Ok(None);
};
let last_modified = util::fs::last_modified_from_file(path, &file);
let expiration = Expiration::after(ttl);
Ok(Some(CachedTimeZone { tz, expiration, last_modified }))
}
fn is_expired(&self) -> bool {
self.expiration.is_expired()
}
fn name(&self) -> &str {
self.tz.iana_name().unwrap()
}
fn revalidate(&mut self, path: &Path, ttl: Duration) -> bool {
let Some(old_last_modified) = self.last_modified else {
trace!(
"revalidation for {name} in {path} failed because \
old last modified time is unavailable",
name = self.name(),
path = path.display(),
);
return false;
};
let Some(new_last_modified) = util::fs::last_modified_from_path(path)
else {
trace!(
"revalidation for {name} in {path} failed because \
new last modified time is unavailable",
name = self.name(),
path = path.display(),
);
return false;
};
if old_last_modified != new_last_modified {
trace!(
"revalidation for {name} in {path} failed because \
last modified times do not match: old = {old} != {new} = new",
name = self.name(),
path = path.display(),
old = old_last_modified,
new = new_last_modified,
);
return false;
}
trace!(
"revalidation for {name} in {path} succeeded because \
last modified times match: old = {old} == {new} = new",
name = self.name(),
path = path.display(),
old = old_last_modified,
new = new_last_modified,
);
self.expiration = Expiration::after(ttl);
true
}
}
#[derive(Debug)]
struct Names {
inner: RwLock<NamesInner>,
}
#[derive(Debug)]
struct NamesInner {
names: Vec<Arc<str>>,
version: ArrayStr<5>,
scratch: Vec<u8>,
ttl: Duration,
expiration: Expiration,
}
impl Names {
const DEFAULT_TTL: Duration = DEFAULT_TTL;
fn new(path: &Path) -> Result<Names, Error> {
let path = path.to_path_buf();
let mut scratch = vec![];
let (names, version) = read_names_and_version(&path, &mut scratch)?;
trace!(
"found concatenated tzdata at {path} \
with version {version} and {len} \
IANA time zone identifiers",
path = path.display(),
len = names.len(),
);
let ttl = Names::DEFAULT_TTL;
let expiration = Expiration::after(ttl);
let inner = NamesInner { names, version, scratch, ttl, expiration };
Ok(Names { inner: RwLock::new(inner) })
}
fn available(&self, path: &Path) -> Vec<String> {
let mut inner = self.inner.write().unwrap();
inner.attempt_refresh(path);
inner.available()
}
fn reset(&self) {
self.inner.write().unwrap().reset();
}
}
impl NamesInner {
fn available(&self) -> Vec<String> {
self.names.iter().map(|name| name.to_string()).collect()
}
fn attempt_refresh(&mut self, path: &Path) {
if self.expiration.is_expired() {
self.refresh(path);
}
}
fn refresh(&mut self, path: &Path) {
let result = read_names_and_version(path, &mut self.scratch);
self.expiration = Expiration::after(self.ttl);
match result {
Ok((names, version)) => {
trace!(
"refreshed concatenated tzdata at {path} \
with version {version} and {len} \
IANA time zone identifiers",
path = path.display(),
len = names.len(),
);
self.names = names;
self.version = version;
}
Err(_err) => {
warn!(
"failed to refresh concatenated time zone name cache \
for {path}: {_err}",
path = path.display(),
)
}
}
}
fn reset(&mut self) {
self.names.clear();
self.expiration = Expiration::expired();
}
}
#[derive(Debug)]
enum TzdataLocation {
Env { name: &'static str, default: &'static str, suffix: &'static str },
}
impl TzdataLocation {
fn to_path_buf(&self) -> PathBuf {
match *self {
TzdataLocation::Env { name, default, suffix } => {
let var = std::env::var_os(name)
.unwrap_or_else(|| OsString::from(default));
let prefix = PathBuf::from(var);
prefix.join(suffix)
}
}
}
}
fn read_names_and_version(
path: &Path,
scratch: &mut Vec<u8>,
) -> Result<(Vec<Arc<str>>, ArrayStr<5>), Error> {
let file = File::open(path).map_err(|e| Error::io(e).path(path))?;
let db = ConcatenatedTzif::open(file)?;
let names: Vec<Arc<str>> =
db.available(scratch)?.into_iter().map(Arc::from).collect();
if names.is_empty() {
return Err(Error::from(E::ConcatenatedMissingIanaIdentifiers));
}
Ok((names, db.version()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_tzdata_list() -> anyhow::Result<()> {
let _ = crate::logging::Logger::init();
const ENV: &str = "JIFF_DEBUG_CONCATENATED_TZDATA";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let path = PathBuf::from(val);
let db = Database::from_path(&path)?;
for name in db.available() {
std::eprintln!("{name}");
}
Ok(())
}
}