use super::header::{Counts, MAGIC};
use super::{LeapRecord, LocalTimeType, Transition};
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedTzif {
pub version: u8,
pub types: Vec<LocalTimeType>,
pub transitions: Vec<Transition>,
pub leaps: Vec<LeapRecord>,
pub footer: String,
pub counts: Counts,
pub raw: RawStructural,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RawStructural {
pub isdst: Vec<u8>,
pub desigidx: Vec<u8>,
pub designation: Vec<u8>,
pub std_indicators: Vec<u8>,
pub ut_indicators: Vec<u8>,
}
struct Cursor<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(buf: &'a [u8]) -> Self {
Cursor { buf, pos: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.pos
.checked_add(n)
.ok_or_else(|| Error::message("TZif length overflow"))?;
if end > self.buf.len() {
return Err(Error::message("TZif truncated"));
}
let s = &self.buf[self.pos..end];
self.pos = end;
Ok(s)
}
fn u32(&mut self) -> Result<u32> {
let b = self.take(4)?;
Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn remaining(&self) -> usize {
self.buf.len().saturating_sub(self.pos)
}
fn skip(&mut self, n: usize) -> Result<()> {
let end = self
.pos
.checked_add(n)
.ok_or_else(|| Error::message("TZif length overflow"))?;
if end > self.buf.len() {
return Err(Error::message(
"TZif truncated (a counted block extends past the input)",
));
}
self.pos = end;
Ok(())
}
}
fn read_header(c: &mut Cursor<'_>) -> Result<(u8, Counts)> {
let magic = c.take(4)?;
if magic != MAGIC {
return Err(Error::message("bad TZif magic"));
}
let version = c.take(1)?[0];
let _reserved = 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))
}
fn checked_block_len(counts: &Counts, time_size: usize) -> Result<usize> {
let oflow =
|| Error::message("TZif count arithmetic overflow (declared counts are implausibly large)");
let term = |count: u32, size: usize| count_mul(count, size).ok_or_else(oflow);
let mut total: usize = 0;
for t in [
term(counts.timecnt, time_size)?, term(counts.timecnt, 1)?, term(counts.typecnt, 6)?, term(counts.charcnt, 1)?, term(counts.leapcnt, time_size + 4)?, term(counts.isstdcnt, 1)?, term(counts.isutcnt, 1)?, ] {
total = total.checked_add(t).ok_or_else(oflow)?;
}
Ok(total)
}
fn count_mul(count: u32, size: usize) -> Option<usize> {
(count as usize).checked_mul(size)
}
fn first_oob_type_index(type_indices: &[u8], typecnt: usize) -> Option<usize> {
type_indices.iter().position(|&i| (i as usize) >= typecnt)
}
fn abbr_index_slice_safe(idx: usize, table_len: usize) -> bool {
idx <= table_len
}
pub(crate) fn rfc_designation_index_valid(
idx: usize,
charcnt: usize,
has_nul_at_or_after: bool,
) -> bool {
charcnt != 0 && idx < charcnt && has_nul_at_or_after
}
pub(crate) fn isdst_byte_valid(isdst: u8) -> bool {
isdst <= 1
}
pub(crate) fn indicator_pair_valid(isut: u8, isstd: u8) -> bool {
isut <= 1 && isstd <= 1 && (isut == 0 || isstd == 1)
}
pub(crate) fn utoff_structural_valid(utoff: i32) -> bool {
utoff != i32::MIN
}
#[allow(clippy::type_complexity)]
fn read_block(
c: &mut Cursor<'_>,
counts: &Counts,
time_size: usize,
) -> Result<(
Vec<Transition>,
Vec<LocalTimeType>,
Vec<LeapRecord>,
RawStructural,
)> {
let needed = checked_block_len(counts, time_size)?;
if needed > c.remaining() {
return Err(Error::message(
"TZif truncated: declared counts require more bytes than the input contains",
));
}
let tc = counts.timecnt as usize;
let ty = counts.typecnt as usize;
let mut times = Vec::with_capacity(tc);
for _ in 0..tc {
let b = c.take(time_size)?;
let at = match time_size {
4 => i32::from_be_bytes([b[0], b[1], b[2], b[3]]) as i64,
8 => i64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]),
_ => return Err(Error::message("bad time size")),
};
times.push(at);
}
let idxs = c.take(tc)?.to_vec();
let mut raw_types = Vec::with_capacity(ty);
let mut raw_isdst = Vec::with_capacity(ty);
let mut raw_desigidx = Vec::with_capacity(ty);
for _ in 0..ty {
let b = c.take(6)?;
let utoff = i32::from_be_bytes([b[0], b[1], b[2], b[3]]);
let is_dst = b[4] != 0;
let desigidx = b[5] as usize;
raw_isdst.push(b[4]);
raw_desigidx.push(b[5]);
raw_types.push((utoff, is_dst, desigidx));
}
let table = c.take(counts.charcnt as usize)?;
let designation = table.to_vec();
let mut leaps = Vec::with_capacity(counts.leapcnt as usize);
for _ in 0..counts.leapcnt as usize {
let tb = c.take(time_size)?;
let trans = match time_size {
4 => i32::from_be_bytes([tb[0], tb[1], tb[2], tb[3]]) as i64,
8 => i64::from_be_bytes([tb[0], tb[1], tb[2], tb[3], tb[4], tb[5], tb[6], tb[7]]),
_ => return Err(Error::message("bad time size")),
};
let cb = c.take(4)?;
let corr = i32::from_be_bytes([cb[0], cb[1], cb[2], cb[3]]);
leaps.push(LeapRecord { trans, corr });
}
let std_indicators = c.take(counts.isstdcnt as usize)?.to_vec();
let ut_indicators = c.take(counts.isutcnt as usize)?.to_vec();
let types = raw_types
.into_iter()
.map(|(utoff, is_dst, idx)| {
let abbr = read_cstr(table, idx)?;
Ok(LocalTimeType {
utoff,
is_dst,
abbr,
})
})
.collect::<Result<Vec<_>>>()?;
if let Some(pos) = first_oob_type_index(&idxs, types.len()) {
return Err(Error::message(format!(
"TZif transition type index {} out of range (typecnt {})",
idxs[pos],
types.len()
)));
}
let transitions: Vec<Transition> = times
.into_iter()
.zip(idxs)
.map(|(at, type_index)| Transition { at, type_index })
.collect();
Ok((
transitions,
types,
leaps,
RawStructural {
isdst: raw_isdst,
desigidx: raw_desigidx,
designation,
std_indicators,
ut_indicators,
},
))
}
fn read_cstr(table: &[u8], idx: usize) -> Result<String> {
if !abbr_index_slice_safe(idx, table.len()) {
return Err(Error::message("designation index out of range"));
}
let rest = &table[idx..];
let end = rest.iter().position(|&b| b == 0).unwrap_or(rest.len());
String::from_utf8(rest[..end].to_vec()).map_err(|_| Error::message("non-UTF-8 abbreviation"))
}
pub fn parse(bytes: &[u8]) -> Result<ParsedTzif> {
let mut c = Cursor::new(bytes);
let (version, v1_counts) = read_header(&mut c)?;
if version == 0 {
let (transitions, types, leaps, raw) = read_block(&mut c, &v1_counts, 4)?;
return Ok(ParsedTzif {
version,
types,
transitions,
leaps,
footer: String::new(),
counts: v1_counts,
raw,
});
}
c.skip(checked_block_len(&v1_counts, 4)?)?;
let (v2_version, v2_counts) = read_header(&mut c)?;
let (transitions, types, leaps, raw) = read_block(&mut c, &v2_counts, 8)?;
let tail = &c.buf[c.pos..];
let footer = parse_footer(tail)?;
Ok(ParsedTzif {
version: v2_version,
types,
transitions,
leaps,
footer,
counts: v2_counts,
raw,
})
}
fn parse_footer(tail: &[u8]) -> Result<String> {
if tail.is_empty() {
return Ok(String::new());
}
if tail[0] != b'\n' || tail[tail.len() - 1] != b'\n' {
return Err(Error::message("malformed TZif footer"));
}
let inner = &tail[1..tail.len() - 1];
String::from_utf8(inner.to_vec()).map_err(|_| Error::message("non-UTF-8 footer"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tzif::{write_bytes, TzifData};
#[test]
fn round_trip_fixed_offset() {
let data = TzifData::fixed(-18000, "EST", "EST5");
let bytes = write_bytes(&data).unwrap();
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed.version, b'2');
assert_eq!(parsed.transitions.len(), 0);
assert_eq!(parsed.types.len(), 1);
assert_eq!(parsed.types[0].utoff, -18000);
assert!(!parsed.types[0].is_dst);
assert_eq!(parsed.types[0].abbr, "EST");
assert_eq!(parsed.footer, "EST5");
}
#[test]
fn round_trip_utc() {
let data = TzifData::fixed(0, "UTC", "UTC0");
let parsed = parse(&write_bytes(&data).unwrap()).unwrap();
assert_eq!(parsed.types[0].abbr, "UTC");
assert_eq!(parsed.footer, "UTC0");
}
fn v1_tzif_one_transition(trans_type_index: u8) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&MAGIC); 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(trans_type_index); b.extend_from_slice(&0i32.to_be_bytes()); b.push(0); b.push(0); b.extend_from_slice(b"UTC\0"); b
}
#[test]
fn valid_transition_type_index_parses() {
let parsed = parse(&v1_tzif_one_transition(0)).unwrap();
assert_eq!(parsed.transitions.len(), 1);
assert_eq!(parsed.transitions[0].type_index, 0);
assert_eq!(parsed.types.len(), 1);
}
fn v1_header_with_counts(
isutcnt: u32,
isstdcnt: u32,
leapcnt: u32,
timecnt: u32,
typecnt: u32,
charcnt: u32,
body_len: usize,
) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&MAGIC);
b.push(0); b.extend_from_slice(&[0u8; 15]);
for v in [isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt] {
b.extend_from_slice(&v.to_be_bytes());
}
b.extend(std::iter::repeat(0u8).take(body_len));
b
}
#[test]
fn implausibly_large_declared_count_is_rejected_not_ooming() {
let bytes = v1_header_with_counts(0, 0, 0, 1_000_000_000, 1, 4, 8);
let err = parse(&bytes).unwrap_err();
assert!(
err.to_string()
.contains("more bytes than the input contains")
|| err.to_string().contains("truncated"),
"expected a counts-exceed-input rejection, got: {err}"
);
}
#[test]
fn count_block_len_is_checked_arithmetic() {
let needed = checked_block_len(
&Counts {
isutcnt: 0,
isstdcnt: 0,
leapcnt: 0,
timecnt: u32::MAX,
typecnt: 1,
charcnt: 4,
},
8,
)
.unwrap();
assert!(needed > 30_000_000_000);
}
#[test]
fn out_of_range_transition_type_index_is_rejected_not_panic() {
let err = parse(&v1_tzif_one_transition(1)).unwrap_err();
assert!(
err.to_string().contains("type index 1 out of range"),
"expected an out-of-range rejection, got: {err}"
);
assert!(parse(&v1_tzif_one_transition(255)).is_err());
}
#[test]
fn rfc_designation_index_validity_rules() {
assert!(rfc_designation_index_valid(0, 4, true));
assert!(!rfc_designation_index_valid(4, 4, true)); assert!(!rfc_designation_index_valid(0, 0, true)); assert!(!rfc_designation_index_valid(0, 4, false)); assert!(abbr_index_slice_safe(3, 4) && rfc_designation_index_valid(3, 4, true));
}
#[test]
fn isdst_byte_validity_rules() {
assert!(isdst_byte_valid(0) && isdst_byte_valid(1));
assert!(!isdst_byte_valid(2) && !isdst_byte_valid(255));
}
#[test]
fn indicator_pair_validity_rules() {
assert!(
indicator_pair_valid(0, 0) && indicator_pair_valid(0, 1) && indicator_pair_valid(1, 1)
);
assert!(!indicator_pair_valid(1, 0)); assert!(!indicator_pair_valid(2, 0) && !indicator_pair_valid(0, 2)); }
#[test]
fn utoff_structural_validity_rules() {
assert!(
utoff_structural_valid(0)
&& utoff_structural_valid(-18000)
&& utoff_structural_valid(i32::MAX)
);
assert!(!utoff_structural_valid(i32::MIN));
}
}
#[cfg(kani)]
mod kani_harness {
use super::{
abbr_index_slice_safe, checked_block_len, first_oob_type_index, indicator_pair_valid,
isdst_byte_valid, rfc_designation_index_valid, utoff_structural_valid, Counts, Cursor,
};
#[kani::proof]
fn abbr_index_guard_prevents_oob_slice() {
let table: [u8; 8] = kani::any();
let idx: usize = kani::any();
if abbr_index_slice_safe(idx, table.len()) {
let _slice = &table[idx..]; assert!(idx <= table.len());
} else {
assert!(idx > table.len());
}
}
#[kani::proof]
#[kani::unwind(5)]
fn type_index_guard_is_sound() {
let idxs: [u8; 4] = kani::any();
let typecnt: usize = kani::any();
match first_oob_type_index(&idxs, typecnt) {
None => {
for &i in idxs.iter() {
assert!((i as usize) < typecnt);
}
}
Some(p) => {
assert!(p < idxs.len()); assert!((idxs[p] as usize) >= typecnt); }
}
}
#[kani::proof]
fn take_never_panics_and_preserves_bounds() {
let buf: [u8; 8] = kani::any();
let pos: usize = kani::any();
kani::assume(pos <= buf.len());
let mut c = Cursor { buf: &buf, pos };
let n: usize = kani::any();
match c.take(n) {
Ok(s) => {
assert!(s.len() == n);
assert!(c.pos == pos + n);
assert!(c.pos <= buf.len());
}
Err(_) => assert!(c.pos == pos),
}
}
#[kani::proof]
fn skip_never_panics_and_preserves_bounds() {
let buf: [u8; 8] = kani::any();
let pos: usize = kani::any();
kani::assume(pos <= buf.len());
let mut c = Cursor { buf: &buf, pos };
let n: usize = kani::any();
match c.skip(n) {
Ok(()) => assert!(c.pos == pos + n && c.pos <= buf.len()),
Err(_) => assert!(c.pos == pos),
}
}
#[kani::proof]
fn skip_within_remaining_cannot_truncate() {
let buf: [u8; 8] = kani::any();
let pos: usize = kani::any();
kani::assume(pos <= buf.len());
let mut c = Cursor { buf: &buf, pos };
let r = c.remaining();
assert!(pos + r == buf.len()); let n: usize = kani::any();
kani::assume(n <= r);
assert!(c.skip(n).is_ok()); assert!(c.pos <= buf.len());
}
#[kani::proof]
fn checked_block_len_never_panics() {
let counts = Counts {
isutcnt: kani::any(),
isstdcnt: kani::any(),
leapcnt: kani::any(),
timecnt: kani::any(),
typecnt: kani::any(),
charcnt: kani::any(),
};
let time_size: usize = if kani::any() { 4 } else { 8 };
let _ = checked_block_len(&counts, time_size);
}
#[kani::proof]
fn rfc_designation_index_valid_is_exact_and_implies_slice_safe() {
let idx: usize = kani::any();
let charcnt: usize = kani::any();
let has_nul: bool = kani::any();
let got = rfc_designation_index_valid(idx, charcnt, has_nul);
assert_eq!(got, charcnt != 0 && idx < charcnt && has_nul);
if got {
assert!(abbr_index_slice_safe(idx, charcnt)); }
}
#[kani::proof]
fn isdst_byte_valid_is_exact() {
let b: u8 = kani::any();
let got = isdst_byte_valid(b);
assert_eq!(got, b <= 1);
if got {
assert!(b == 0 || b == 1);
}
}
#[kani::proof]
fn indicator_pair_valid_is_exact() {
let isut: u8 = kani::any();
let isstd: u8 = kani::any();
let got = indicator_pair_valid(isut, isstd);
assert_eq!(got, isut <= 1 && isstd <= 1 && !(isut == 1 && isstd == 0));
if got && isut == 1 {
assert!(isstd == 1);
}
}
#[kani::proof]
fn utoff_structural_valid_excludes_unnegatable_min() {
let u: i32 = kani::any();
let got = utoff_structural_valid(u);
assert_eq!(got, u != i32::MIN);
if got {
assert!(u.checked_neg().is_some()); }
}
}