use super::header::{write_header, Counts};
use super::{LeapRecord, LocalTimeType, Transition};
use crate::error::{Error, Result};
fn build_designations(types: &[LocalTimeType]) -> Result<(Vec<u8>, Vec<u8>)> {
let mut table: Vec<u8> = Vec::new();
for t in types {
add_abbr(&mut table, t.abbr.as_bytes());
}
let mut indices = Vec::with_capacity(types.len());
for t in types {
let off = abbr_offset(&table, t.abbr.as_bytes())
.expect("every abbreviation is present after the build pass");
let off = u8::try_from(off).map_err(|_| {
Error::message(
"abbreviation designation table exceeds 255 bytes \
(too many/long timezone abbreviations)",
)
})?;
indices.push(off);
}
Ok((table, indices))
}
fn cstr_len(table: &[u8], i: usize) -> usize {
table[i..]
.iter()
.position(|&b| b == 0)
.expect("designation table entries are NUL-terminated")
}
fn add_abbr(table: &mut Vec<u8>, abbr: &[u8]) {
let alen = abbr.len();
let mut i = 0;
while i < table.len() {
let clen = cstr_len(table, i);
if alen <= clen {
let isuff = i + (clen - alen);
if table[isuff..isuff + alen] == *abbr {
return;
}
} else if table[i..i + clen] == abbr[alen - clen..] {
table.splice(i..i, abbr[..alen - clen].iter().copied());
return;
}
i += clen + 1;
}
table.extend_from_slice(abbr);
table.push(0);
}
fn abbr_offset(table: &[u8], abbr: &[u8]) -> Option<usize> {
let alen = abbr.len();
let mut i = 0;
while i < table.len() {
let clen = cstr_len(table, i);
if alen <= clen {
let isuff = i + (clen - alen);
if table[isuff..isuff + alen] == *abbr {
return Some(isuff);
}
}
i += clen + 1;
}
None
}
pub fn write_block(
out: &mut Vec<u8>,
version: u8,
time_size: usize,
types: &[LocalTimeType],
transitions: &[Transition],
leaps: &[LeapRecord],
) -> Result<()> {
let (designations, desig_idx) = build_designations(types)?;
let counts = Counts {
isutcnt: 0,
isstdcnt: 0,
leapcnt: leaps.len() as u32,
timecnt: transitions.len() as u32,
typecnt: types.len() as u32,
charcnt: designations.len() as u32,
};
write_header(out, version, &counts);
for tr in transitions {
write_time(out, tr.at, time_size);
}
for tr in transitions {
out.push(tr.type_index);
}
for (i, t) in types.iter().enumerate() {
out.extend_from_slice(&t.utoff.to_be_bytes());
out.push(t.is_dst as u8);
out.push(desig_idx[i]);
}
out.extend_from_slice(&designations);
for lp in leaps {
write_time(out, lp.trans, time_size);
out.extend_from_slice(&lp.corr.to_be_bytes());
}
Ok(())
}
fn write_time(out: &mut Vec<u8>, at: i64, time_size: usize) {
match time_size {
4 => {
let v = i32::try_from(at).expect("32-bit transition time out of range");
out.extend_from_slice(&v.to_be_bytes());
}
8 => out.extend_from_slice(&at.to_be_bytes()),
other => unreachable!("unsupported TZif time size {other}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ty(abbr: &str) -> LocalTimeType {
LocalTimeType {
utoff: 0,
is_dst: false,
abbr: abbr.to_string(),
}
}
fn at(table: &[u8], off: u8) -> String {
let s = off as usize;
let end = s + cstr_len(table, s);
String::from_utf8(table[s..end].to_vec()).unwrap()
}
fn assert_indices_resolve(types: &[LocalTimeType]) -> Vec<u8> {
let (table, idx) = build_designations(types).unwrap();
for (t, &off) in types.iter().zip(&idx) {
assert_eq!(
at(&table, off),
t.abbr,
"index for {:?} must resolve",
t.abbr
);
}
table
}
#[test]
fn suffix_is_shared_regardless_of_order() {
let longer_first = assert_indices_resolve(&[ty("AHST"), ty("HST")]);
let shorter_first = assert_indices_resolve(&[ty("HST"), ty("AHST")]);
assert_eq!(longer_first.len(), 5, "AHST\\0 only");
assert_eq!(shorter_first.len(), 5, "splice yields the same size");
assert_eq!(longer_first, b"AHST\0");
assert_eq!(shorter_first, b"AHST\0");
}
#[test]
fn plmt_lmt_share_like_ho_chi_minh() {
let table = assert_indices_resolve(&[ty("PLMT"), ty("LMT"), ty("+07")]);
assert_eq!(table, b"PLMT\0+07\0"); }
#[test]
fn exact_duplicates_dedup() {
let table = assert_indices_resolve(&[ty("EST"), ty("EDT"), ty("EST")]);
assert_eq!(table, b"EST\0EDT\0");
}
#[test]
fn non_suffix_abbrs_are_appended() {
let table = assert_indices_resolve(&[ty("GMT"), ty("BST")]);
assert_eq!(table, b"GMT\0BST\0"); }
#[test]
fn empty_abbr_reuses_a_terminator() {
let table = assert_indices_resolve(&[ty("UTC"), ty("")]);
assert_eq!(table, b"UTC\0");
}
}