use std::collections::HashMap;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq)]
pub struct LocalType {
pub offset: i64,
pub abbrev: String,
}
#[derive(Debug)]
pub struct Tz {
pub name: String,
transitions: Vec<i64>,
type_of: Vec<usize>,
types: Vec<LocalType>,
rule: Option<PosixRule>,
}
#[derive(Debug)]
struct PosixRule {
std: LocalType,
dst: Option<(LocalType, RuleDate, RuleDate)>,
}
#[derive(Debug, Clone, Copy)]
enum RuleDate {
MonthWeekDay {
month: i64,
week: i64,
day: i64,
time: i64,
},
Julian1 { day: i64, time: i64 },
Julian0 { day: i64, time: i64 },
}
impl Tz {
pub fn at(&self, t: i64) -> LocalType {
match self.transitions.binary_search(&t) {
Ok(i) => self.types[self.type_of[i]].clone(),
Err(i) if i == self.transitions.len() && self.rule.is_some() => {
self.rule.as_ref().map(|r| r.at(t)).expect("checked")
}
Err(0) => self.types[0].clone(),
Err(i) => self.types[self.type_of[i - 1]].clone(),
}
}
}
thread_local! {
static CACHE: std::cell::RefCell<HashMap<String, Option<Rc<Tz>>>> =
std::cell::RefCell::new(HashMap::new());
}
pub fn load(name: &str) -> Option<Rc<Tz>> {
if name.is_empty()
|| name.starts_with('/')
|| name
.split('/')
.any(|p| p.is_empty() || p == "." || p == "..")
|| !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '-' | '+'))
{
return None;
}
if let Some(hit) = CACHE.with(|c| c.borrow().get(name).cloned()) {
return hit;
}
let tz = [
"/usr/share/zoneinfo",
"/usr/lib/zoneinfo",
"/usr/share/lib/zoneinfo",
]
.iter()
.find_map(|dir| std::fs::read(format!("{dir}/{name}")).ok())
.and_then(|bytes| parse(&bytes, name))
.map(Rc::new);
CACHE.with(|c| c.borrow_mut().insert(name.to_string(), tz.clone()));
tz
}
fn be32(b: &[u8], at: usize) -> Option<i64> {
Some(i32::from_be_bytes(b.get(at..at + 4)?.try_into().ok()?) as i64)
}
fn be64(b: &[u8], at: usize) -> Option<i64> {
Some(i64::from_be_bytes(b.get(at..at + 8)?.try_into().ok()?))
}
fn counts(b: &[u8], at: usize) -> Option<[usize; 6]> {
let mut c = [0usize; 6];
for (i, slot) in c.iter_mut().enumerate() {
*slot = be32(b, at + 20 + i * 4)? as usize;
}
Some(c)
}
fn parse(b: &[u8], name: &str) -> Option<Tz> {
if b.get(..4)? != b"TZif" {
return None;
}
let version = *b.get(4)?;
let c1 = counts(b, 0)?;
let v1_len = c1[3] * 5 + c1[4] * 6 + c1[5] + c1[2] * 8 + c1[1] + c1[0];
let (base, c, time_size) = if version >= b'2' {
let at = 44 + v1_len;
(at, counts(b, at)?, 8)
} else {
(0, c1, 4)
};
let [isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt] = c;
let mut p = base + 44;
let mut transitions = Vec::with_capacity(timecnt);
for i in 0..timecnt {
transitions.push(if time_size == 8 {
be64(b, p + i * 8)?
} else {
be32(b, p + i * 4)?
});
}
p += timecnt * time_size;
let type_of: Vec<usize> = b.get(p..p + timecnt)?.iter().map(|&x| x as usize).collect();
p += timecnt;
let mut raw_types = Vec::with_capacity(typecnt);
for i in 0..typecnt {
let at = p + i * 6;
raw_types.push((be32(b, at)?, *b.get(at + 5)? as usize));
}
p += typecnt * 6;
let chars = b.get(p..p + charcnt)?;
let types: Vec<LocalType> = raw_types
.into_iter()
.map(|(offset, idx)| {
let end = chars[idx.min(chars.len())..]
.iter()
.position(|&ch| ch == 0)
.map_or(chars.len(), |e| idx + e);
LocalType {
offset,
abbrev: String::from_utf8_lossy(&chars[idx.min(end)..end]).into_owned(),
}
})
.collect();
if types.is_empty() || type_of.iter().any(|&t| t >= types.len()) {
return None;
}
p += charcnt + leapcnt * (time_size + 4) + isstdcnt + isutcnt;
let rule = if version >= b'2' {
b.get(p..).and_then(|rest| {
let text = std::str::from_utf8(rest).ok()?;
let line = text.strip_prefix('\n')?.split('\n').next()?;
PosixRule::parse(line)
})
} else {
None
};
Some(Tz {
name: name.to_string(),
transitions,
type_of,
types,
rule,
})
}
impl PosixRule {
fn parse(s: &str) -> Option<PosixRule> {
let mut cur = s;
let std_name = take_name(&mut cur)?;
let std_off = -take_offset(&mut cur)?;
let std = LocalType {
offset: std_off,
abbrev: std_name,
};
if cur.is_empty() {
return Some(PosixRule { std, dst: None });
}
let dst_name = take_name(&mut cur)?;
let dst_off = if cur.starts_with(',') {
std_off + 3600
} else {
-take_offset(&mut cur)?
};
let rest = cur.strip_prefix(',')?;
let (start, end) = rest.split_once(',')?;
Some(PosixRule {
std,
dst: Some((
LocalType {
offset: dst_off,
abbrev: dst_name,
},
RuleDate::parse(start)?,
RuleDate::parse(end)?,
)),
})
}
fn at(&self, t: i64) -> LocalType {
let Some((dst, start, end)) = &self.dst else {
return self.std.clone();
};
let year = crate::stdlib::date::civil_from_days((t + self.std.offset).div_euclid(86_400)).0;
let begin = start.local_secs(year) - self.std.offset;
let finish = end.local_secs(year) - dst.offset;
let in_dst = if begin < finish {
t >= begin && t < finish
} else {
!(t >= finish && t < begin)
};
if in_dst {
dst.clone()
} else {
self.std.clone()
}
}
}
fn take_name(cur: &mut &str) -> Option<String> {
if let Some(rest) = cur.strip_prefix('<') {
let end = rest.find('>')?;
let name = rest[..end].to_string();
*cur = &rest[end + 1..];
return Some(name);
}
let end = cur
.find(|c: char| !c.is_ascii_alphabetic())
.unwrap_or(cur.len());
if end < 3 {
return None;
}
let name = cur[..end].to_string();
*cur = &cur[end..];
Some(name)
}
fn take_offset(cur: &mut &str) -> Option<i64> {
let (sign, rest) = match cur.as_bytes().first()? {
b'-' => (-1, &cur[1..]),
b'+' => (1, &cur[1..]),
_ => (1, *cur),
};
let end = rest
.find(|c: char| !(c.is_ascii_digit() || c == ':'))
.unwrap_or(rest.len());
let secs = hms(&rest[..end])?;
*cur = &rest[end..];
Some(sign * secs)
}
fn hms(s: &str) -> Option<i64> {
let mut total = 0;
let mut scale = 3600;
for part in s.split(':') {
if part.is_empty() || scale == 0 {
return None;
}
total += part.parse::<i64>().ok()? * scale;
scale /= 60;
}
Some(total)
}
impl RuleDate {
fn parse(s: &str) -> Option<RuleDate> {
let (date, time) = match s.split_once('/') {
Some((d, t)) => {
let (neg, t) = match t.strip_prefix('-') {
Some(t) => (true, t),
None => (false, t.strip_prefix('+').unwrap_or(t)),
};
let secs = hms(t)?;
(d, if neg { -secs } else { secs })
}
None => (s, 7200),
};
if let Some(m) = date.strip_prefix('M') {
let mut it = m.split('.').map(|x| x.parse::<i64>().ok());
let (month, week, day) = (it.next()??, it.next()??, it.next()??);
return Some(RuleDate::MonthWeekDay {
month,
week,
day,
time,
});
}
if let Some(n) = date.strip_prefix('J') {
return Some(RuleDate::Julian1 {
day: n.parse().ok()?,
time,
});
}
Some(RuleDate::Julian0 {
day: date.parse().ok()?,
time,
})
}
fn local_secs(&self, year: i64) -> i64 {
use crate::stdlib::date::days_from_civil;
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
let (days, time) = match *self {
RuleDate::MonthWeekDay {
month,
week,
day,
time,
} => {
let first = days_from_civil(year, month - 1, 1);
let first_wd = (first + 4).rem_euclid(7);
let mut d = first + (day - first_wd).rem_euclid(7) + (week - 1) * 7;
let next_month = if month == 12 {
days_from_civil(year + 1, 0, 1)
} else {
days_from_civil(year, month, 1)
};
while d >= next_month {
d -= 7;
}
(d, time)
}
RuleDate::Julian1 { day, time } => {
let skip = if leap && day >= 60 { 1 } else { 0 };
(days_from_civil(year, 0, 1) + day - 1 + skip, time)
}
RuleDate::Julian0 { day, time } => (days_from_civil(year, 0, 1) + day, time),
};
days * 86_400 + time
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn posix_rule_new_york() {
let r = PosixRule::parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
assert_eq!(r.at(1_909_137_600).abbrev, "EDT");
assert_eq!(r.at(1_893_499_200).abbrev, "EST");
let r = PosixRule::parse("<+0530>-5:30").unwrap();
assert_eq!(r.at(0).offset, 19_800);
assert_eq!(r.at(0).abbrev, "+0530");
}
}