use crate::{
error::{tz::db::Error as E, Error},
tz::TimeZone,
util::{sync::Arc, utf8},
};
mod bundled;
mod concatenated;
mod zoneinfo;
pub fn db() -> &'static TimeZoneDatabase {
#[cfg(not(feature = "std"))]
{
static NONE: TimeZoneDatabase = TimeZoneDatabase::none();
&NONE
}
#[cfg(feature = "std")]
{
use std::sync::OnceLock;
static DB: OnceLock<TimeZoneDatabase> = OnceLock::new();
DB.get_or_init(|| {
let db = TimeZoneDatabase::from_env();
debug!("initialized global time zone database: {db:?}");
db
})
}
}
#[derive(Clone)]
pub struct TimeZoneDatabase {
inner: Option<Arc<Kind>>,
}
#[derive(Debug)]
#[cfg_attr(not(feature = "alloc"), derive(Clone))]
enum Kind {
ZoneInfo(zoneinfo::Database),
Concatenated(concatenated::Database),
Bundled(bundled::Database),
}
impl TimeZoneDatabase {
pub const fn none() -> TimeZoneDatabase {
TimeZoneDatabase { inner: None }
}
pub fn from_env() -> TimeZoneDatabase {
#[cfg(not(miri))]
{
if cfg!(target_os = "android") {
let db = concatenated::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::Concatenated(db));
}
let db = zoneinfo::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::ZoneInfo(db));
}
} else {
let db = zoneinfo::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::ZoneInfo(db));
}
let db = concatenated::Database::from_env();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::Concatenated(db));
}
}
}
let db = bundled::Database::new();
if !db.is_definitively_empty() {
return TimeZoneDatabase::new(Kind::Bundled(db));
}
warn!(
"could not find zoneinfo, concatenated tzdata or \
bundled time zone database",
);
TimeZoneDatabase::none()
}
#[cfg(feature = "std")]
pub fn from_dir<P: AsRef<std::path::Path>>(
path: P,
) -> Result<TimeZoneDatabase, Error> {
let path = path.as_ref();
let db = zoneinfo::Database::from_dir(path)?;
if db.is_definitively_empty() {
warn!(
"could not find zoneinfo data at directory {path}",
path = path.display(),
);
}
Ok(TimeZoneDatabase::new(Kind::ZoneInfo(db)))
}
#[cfg(feature = "std")]
pub fn from_concatenated_path<P: AsRef<std::path::Path>>(
path: P,
) -> Result<TimeZoneDatabase, Error> {
let path = path.as_ref();
let db = concatenated::Database::from_path(path)?;
if db.is_definitively_empty() {
warn!(
"could not find concatenated tzdata in file {path}",
path = path.display(),
);
}
Ok(TimeZoneDatabase::new(Kind::Concatenated(db)))
}
pub fn bundled() -> TimeZoneDatabase {
let db = bundled::Database::new();
if db.is_definitively_empty() {
warn!("could not find embedded/bundled zoneinfo");
}
TimeZoneDatabase::new(Kind::Bundled(db))
}
fn new(kind: Kind) -> TimeZoneDatabase {
TimeZoneDatabase { inner: Some(Arc::new(kind)) }
}
pub fn get(&self, name: &str) -> Result<TimeZone, Error> {
let inner = self
.inner
.as_deref()
.ok_or_else(|| E::failed_time_zone_no_database_configured(name))?;
match *inner {
Kind::ZoneInfo(ref db) => {
if let Some(tz) = db.get(name) {
trace!("found time zone `{name}` in {db:?}", db = self);
return Ok(tz);
}
}
Kind::Concatenated(ref db) => {
if let Some(tz) = db.get(name) {
trace!("found time zone `{name}` in {db:?}", db = self);
return Ok(tz);
}
}
Kind::Bundled(ref db) => {
if let Some(tz) = db.get(name) {
trace!("found time zone `{name}` in {db:?}", db = self);
return Ok(tz);
}
}
}
Err(Error::from(E::failed_time_zone(name)))
}
pub fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
let Some(inner) = self.inner.as_deref() else {
return TimeZoneNameIter::empty();
};
match *inner {
Kind::ZoneInfo(ref db) => db.available(),
Kind::Concatenated(ref db) => db.available(),
Kind::Bundled(ref db) => db.available(),
}
}
pub fn reset(&self) {
let Some(inner) = self.inner.as_deref() else { return };
match *inner {
Kind::ZoneInfo(ref db) => db.reset(),
Kind::Concatenated(ref db) => db.reset(),
Kind::Bundled(ref db) => db.reset(),
}
}
pub fn is_definitively_empty(&self) -> bool {
let Some(inner) = self.inner.as_deref() else { return true };
match *inner {
Kind::ZoneInfo(ref db) => db.is_definitively_empty(),
Kind::Concatenated(ref db) => db.is_definitively_empty(),
Kind::Bundled(ref db) => db.is_definitively_empty(),
}
}
}
impl core::fmt::Debug for TimeZoneDatabase {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("TimeZoneDatabase(")?;
let Some(inner) = self.inner.as_deref() else {
return f.write_str("unavailable)");
};
match *inner {
Kind::ZoneInfo(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::Concatenated(ref db) => core::fmt::Debug::fmt(db, f)?,
Kind::Bundled(ref db) => core::fmt::Debug::fmt(db, f)?,
}
f.write_str(")")
}
}
#[derive(Clone, Debug)]
pub struct TimeZoneNameIter<'d> {
#[cfg(feature = "alloc")]
it: alloc::vec::IntoIter<TimeZoneName<'d>>,
#[cfg(not(feature = "alloc"))]
it: core::iter::Empty<TimeZoneName<'d>>,
}
impl<'d> TimeZoneNameIter<'d> {
fn empty() -> TimeZoneNameIter<'d> {
#[cfg(feature = "alloc")]
{
TimeZoneNameIter { it: alloc::vec::Vec::new().into_iter() }
}
#[cfg(not(feature = "alloc"))]
{
TimeZoneNameIter { it: core::iter::empty() }
}
}
#[cfg(feature = "alloc")]
fn from_iter(
it: impl Iterator<Item = impl Into<alloc::string::String>>,
) -> TimeZoneNameIter<'d> {
let names: alloc::vec::Vec<TimeZoneName<'d>> =
it.map(|name| TimeZoneName::new(name.into())).collect();
TimeZoneNameIter { it: names.into_iter() }
}
}
impl<'d> Iterator for TimeZoneNameIter<'d> {
type Item = TimeZoneName<'d>;
fn next(&mut self) -> Option<TimeZoneName<'d>> {
self.it.next()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct TimeZoneName<'d> {
lifetime: core::marker::PhantomData<&'d str>,
#[cfg(feature = "alloc")]
name: alloc::string::String,
#[cfg(not(feature = "alloc"))]
name: core::convert::Infallible,
}
impl<'d> TimeZoneName<'d> {
#[cfg(feature = "alloc")]
fn new(name: alloc::string::String) -> TimeZoneName<'d> {
TimeZoneName { lifetime: core::marker::PhantomData, name }
}
#[inline]
pub fn as_str<'a>(&'a self) -> &'a str {
#[cfg(feature = "alloc")]
{
self.name.as_str()
}
#[cfg(not(feature = "alloc"))]
{
unreachable!()
}
}
}
impl<'d> core::fmt::Display for TimeZoneName<'d> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
fn special_time_zone(name: &str) -> Option<TimeZone> {
if utf8::cmp_ignore_ascii_case("utc", name).is_eq() {
return Some(TimeZone::UTC);
}
if utf8::cmp_ignore_ascii_case("etc/unknown", name).is_eq() {
return Some(TimeZone::unknown());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn time_zone_database_size() {
#[cfg(feature = "alloc")]
{
let word = core::mem::size_of::<usize>();
assert_eq!(word, core::mem::size_of::<TimeZoneDatabase>());
}
#[cfg(not(feature = "alloc"))]
{
assert_eq!(1, core::mem::size_of::<TimeZoneDatabase>());
}
}
#[test]
fn bundled_returns_utc_constant() {
let db = TimeZoneDatabase::bundled();
if db.is_definitively_empty() {
return;
}
assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
}
#[cfg(all(feature = "std", not(miri)))]
#[test]
fn zoneinfo_returns_utc_constant() {
let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
return;
};
if db.is_definitively_empty() {
return;
}
assert_eq!(db.get("UTC").unwrap(), TimeZone::UTC);
assert_eq!(db.get("utc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("uTc").unwrap(), TimeZone::UTC);
assert_eq!(db.get("UtC").unwrap(), TimeZone::UTC);
assert_eq!(db.get("Etc/Unknown").unwrap(), TimeZone::unknown());
assert_eq!(db.get("etc/UNKNOWN").unwrap(), TimeZone::unknown());
}
#[cfg(all(feature = "std", not(miri)))]
#[test]
fn zoneinfo_available_returns_only_tzif() {
use alloc::{
collections::BTreeSet,
string::{String, ToString},
};
let Ok(db) = TimeZoneDatabase::from_dir("/usr/share/zoneinfo") else {
return;
};
if db.is_definitively_empty() {
return;
}
let names: BTreeSet<String> =
db.available().map(|n| n.as_str().to_string()).collect();
let should_be_absent = [
"leapseconds",
"tzdata.zi",
"leap-seconds.list",
"SECURITY",
"zone1970.tab",
"iso3166.tab",
"zonenow.tab",
"zone.tab",
];
for name in should_be_absent {
assert!(
!names.contains(name),
"found `{name}` in time zone list, but it shouldn't be there",
);
}
}
}