#![forbid(unsafe_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TtInfo {
pub utoff: i32,
pub is_dst: bool,
pub abbr: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeapSecond {
pub occur: i64,
pub corr: i32,
}
#[derive(Debug, Clone)]
pub struct Tzif {
pub version: u8, pub transitions: Vec<i64>,
pub type_indices: Vec<u8>,
pub types: Vec<TtInfo>,
pub leaps: Vec<LeapSecond>,
pub footer: Option<String>,
}
struct Cur<'a> {
b: &'a [u8],
p: usize,
}
impl<'a> Cur<'a> {
fn new(b: &'a [u8]) -> Self {
Cur { b, p: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8], String> {
let end = self.p.checked_add(n).ok_or("offset overflow")?;
if end > self.b.len() {
return Err(format!("truncated: need {n} bytes at {}", self.p));
}
let s = &self.b[self.p..end];
self.p = end;
Ok(s)
}
fn u32(&mut self) -> Result<u32, String> {
let s = self.take(4)?;
Ok(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
}
fn i32(&mut self) -> Result<i32, String> {
Ok(self.u32()? as i32)
}
fn i64(&mut self) -> Result<i64, String> {
let s = self.take(8)?;
let mut a = [0u8; 8];
a.copy_from_slice(s);
Ok(i64::from_be_bytes(a))
}
fn u8(&mut self) -> Result<u8, String> {
Ok(self.take(1)?[0])
}
}
#[derive(Clone, Copy)]
struct Counts {
isutcnt: u32,
isstdcnt: u32,
leapcnt: u32,
timecnt: u32,
typecnt: u32,
charcnt: u32,
}
fn read_header(c: &mut Cur) -> Result<(u8, Counts), String> {
let magic = c.take(4)?;
if magic != b"TZif" {
return Err("bad magic (not TZif)".into());
}
let ver = c.u8()?;
let version = match ver {
0 => 1,
b'2' => 2,
b'3' => 3,
b'4' => 4,
other => return Err(format!("unknown version byte {other:#x}")),
};
c.take(15)?; let counts = Counts {
isutcnt: c.u32()?,
isstdcnt: c.u32()?,
leapcnt: c.u32()?,
timecnt: c.u32()?,
typecnt: c.u32()?,
charcnt: c.u32()?,
};
Ok((version, counts))
}
struct Block {
transitions: Vec<i64>,
type_indices: Vec<u8>,
types: Vec<TtInfo>,
leaps: Vec<LeapSecond>,
}
fn read_block(c: &mut Cur, n: Counts, wide: bool) -> Result<Block, String> {
if n.typecnt == 0 {
return Err("typecnt == 0".into());
}
let mut transitions = Vec::with_capacity(n.timecnt as usize);
for _ in 0..n.timecnt {
transitions.push(if wide { c.i64()? } else { c.i32()? as i64 });
}
let mut type_indices = Vec::with_capacity(n.timecnt as usize);
for _ in 0..n.timecnt {
let ti = c.u8()?;
if ti as u32 >= n.typecnt {
return Err(format!(
"transition type index {ti} >= typecnt {}",
n.typecnt
));
}
type_indices.push(ti);
}
let mut raw_types = Vec::with_capacity(n.typecnt as usize);
for _ in 0..n.typecnt {
let utoff = c.i32()?;
let isdst = c.u8()? != 0;
let desigidx = c.u8()?;
if desigidx as u32 >= n.charcnt {
return Err(format!(
"designation index {desigidx} >= charcnt {}",
n.charcnt
));
}
raw_types.push((utoff, isdst, desigidx));
}
let desig = c.take(n.charcnt as usize)?;
let mut types = Vec::with_capacity(n.typecnt as usize);
for (utoff, isdst, di) in raw_types {
let start = di as usize;
let end = desig[start..]
.iter()
.position(|&b| b == 0)
.map(|z| start + z)
.unwrap_or(desig.len());
let abbr = String::from_utf8_lossy(&desig[start..end]).into_owned();
types.push(TtInfo {
utoff,
is_dst: isdst,
abbr,
});
}
let mut leaps = Vec::with_capacity(n.leapcnt as usize);
for _ in 0..n.leapcnt {
let occur = if wide { c.i64()? } else { c.i32()? as i64 };
let corr = c.i32()?;
leaps.push(LeapSecond { occur, corr });
}
for _ in 0..n.isstdcnt {
c.u8()?;
}
for _ in 0..n.isutcnt {
c.u8()?;
}
Ok(Block {
transitions,
type_indices,
types,
leaps,
})
}
pub fn parse(bytes: &[u8]) -> Result<Tzif, String> {
let mut c = Cur::new(bytes);
let (version, counts) = read_header(&mut c)?;
if version == 1 {
let b = read_block(&mut c, counts, false)?;
return Ok(Tzif {
version,
transitions: b.transitions,
type_indices: b.type_indices,
types: b.types,
leaps: b.leaps,
footer: None,
});
}
let _ = read_block(&mut c, counts, false)?;
let (v2, counts2) = read_header(&mut c)?;
if v2 != version {
return Err(format!("v1/v2 header version mismatch: {version} vs {v2}"));
}
let b = read_block(&mut c, counts2, true)?;
let footer = read_footer(&mut c);
Ok(Tzif {
version,
transitions: b.transitions,
type_indices: b.type_indices,
types: b.types,
leaps: b.leaps,
footer,
})
}
fn read_footer(c: &mut Cur) -> Option<String> {
let rest = &c.b[c.p.min(c.b.len())..];
let s = String::from_utf8_lossy(rest);
let s = s.trim_matches(|ch| ch == '\n' || ch == '\r' || ch == '\0');
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
impl Tzif {
fn pre_first_type(&self) -> usize {
self.types.iter().position(|t| !t.is_dst).unwrap_or(0)
}
pub fn observe(&self, t: i64) -> Observation {
let idx = match self.transitions.binary_search(&t) {
Ok(i) => Some(i), Err(0) => None, Err(i) => Some(i - 1), };
let ti = match idx {
Some(i) => self.type_indices[i] as usize,
None => self.pre_first_type(),
};
let tt = &self.types[ti];
Observation {
utoff: tt.utoff,
is_dst: tt.is_dst,
abbr: tt.abbr.clone(),
}
}
pub fn beyond_explicit(&self, t: i64) -> bool {
match self.transitions.last() {
Some(&last) => t >= last,
None => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Observation {
pub utoff: i32,
pub is_dst: bool,
pub abbr: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn utc_v1() -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(b"TZif");
b.push(0); b.extend_from_slice(&[0u8; 15]);
for v in [0u32, 0, 0, 0, 1, 4] {
b.extend_from_slice(&v.to_be_bytes()); }
b.extend_from_slice(&0i32.to_be_bytes());
b.push(0);
b.push(0);
b.extend_from_slice(b"UTC\0");
b
}
#[test]
fn parses_utc_and_observes() {
let z = parse(&utc_v1()).unwrap();
assert_eq!(z.types.len(), 1);
let o = z.observe(1_767_225_600); assert_eq!(o.utoff, 0);
assert!(!o.is_dst);
assert_eq!(o.abbr, "UTC");
}
#[test]
fn rejects_truncated() {
assert!(parse(b"TZif").is_err());
assert!(parse(b"XXXX").is_err());
}
#[test]
fn rejects_oob_type_index() {
let mut b = Vec::new();
b.extend_from_slice(b"TZif");
b.push(0);
b.extend_from_slice(&[0u8; 15]);
for v in [0u32, 0, 0, 1, 1, 4] {
b.extend_from_slice(&v.to_be_bytes());
}
b.extend_from_slice(&0i32.to_be_bytes()); b.push(5); b.extend_from_slice(&0i32.to_be_bytes());
b.push(0);
b.push(0);
b.extend_from_slice(b"UTC\0");
assert!(parse(&b).is_err());
}
}