use core::ops::Range;
use alloc::{string::String, vec, vec::Vec};
use crate::{
civil::DateTime,
error::{err, Error, ErrorContext},
timestamp::Timestamp,
tz::{
posix::{IanaTz, ReasonablePosixTimeZone},
AmbiguousOffset, Dst, Offset,
},
util::{
crc32,
escape::{Byte, Bytes},
t::UnixSeconds,
},
};
#[derive(Debug)]
pub(crate) struct Tzif {
name: Option<String>,
#[allow(dead_code)]
version: u8,
checksum: u32,
transitions: Vec<Transition>,
types: Vec<LocalTimeType>,
designations: String,
leap_seconds: Vec<LeapSecond>,
posix_tz: Option<ReasonablePosixTimeZone>,
}
impl Tzif {
pub(crate) fn parse(
name: Option<String>,
bytes: &[u8],
) -> Result<Tzif, Error> {
let original = bytes;
let name = name.into();
let (header32, rest) = Header::parse(4, bytes)
.map_err(|e| e.context("failed to parse 32-bit header"))?;
let (mut tzif, rest) = if header32.version == 0 {
Tzif::parse32(name, header32, rest)?
} else {
Tzif::parse64(name, header32, rest)?
};
let tzif_raw_len = (rest.as_ptr() as usize)
.checked_sub(original.as_ptr() as usize)
.unwrap();
let tzif_raw_bytes = &original[..tzif_raw_len];
tzif.checksum = crc32::sum(tzif_raw_bytes);
Ok(tzif)
}
pub(crate) fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub(crate) fn to_offset(
&self,
timestamp: Timestamp,
) -> (Offset, Dst, &str) {
assert!(!self.transitions.is_empty(), "transitions is non-empty");
let index = if timestamp > self.transitions.last().unwrap().timestamp {
self.transitions.len() - 1
} else {
let search = self
.transitions
.binary_search_by_key(×tamp, |t| t.timestamp);
match search {
Err(0) => {
unreachable!("impossible to come before Timestamp::MIN")
}
Ok(i) => i,
Err(i) => i.checked_sub(1).expect("i is non-zero"),
}
};
assert!(index < self.transitions.len());
let t = if index < self.transitions.len() - 1 {
&self.transitions[index]
} else {
match self.posix_tz.as_ref() {
Some(tz) => return tz.to_offset(timestamp),
None => &self.transitions[index],
}
};
let typ = self.local_time_type(t);
self.local_time_type_to_offset(typ)
}
pub(crate) fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
assert!(!self.transitions.is_empty(), "transitions is non-empty");
let search =
self.transitions.binary_search_by_key(&dt, |t| t.wall.start());
let this_index = match search {
Err(0) => unreachable!("impossible to come before DateTime::MIN"),
Ok(i) => i,
Err(i) => i.checked_sub(1).expect("i is non-zero"),
};
assert!(this_index < self.transitions.len());
let this = &self.transitions[this_index];
let this_offset = self.local_time_type(this).offset;
match this.wall {
TransitionWall::Gap { end, .. } if dt < end => {
let prev_index = this_index.checked_sub(1).unwrap();
let prev = &self.transitions[prev_index];
let prev_offset = self.local_time_type(prev).offset;
return AmbiguousOffset::Gap {
before: prev_offset,
after: this_offset,
};
}
TransitionWall::Fold { end, .. } if dt < end => {
let prev_index = this_index.checked_sub(1).unwrap();
let prev = &self.transitions[prev_index];
let prev_offset = self.local_time_type(prev).offset;
return AmbiguousOffset::Fold {
before: prev_offset,
after: this_offset,
};
}
_ => {}
}
if this_index == self.transitions.len() - 1 {
if let Some(tz) = self.posix_tz.as_ref() {
return tz.to_ambiguous_kind(dt);
}
}
AmbiguousOffset::Unambiguous { offset: this_offset }
}
pub(crate) fn previous_transition(
&self,
ts: Timestamp,
) -> Option<Timestamp> {
assert!(!self.transitions.is_empty(), "transitions is non-empty");
let search =
self.transitions.binary_search_by_key(&ts, |t| t.timestamp);
let index = match search {
Ok(i) | Err(i) => i.checked_sub(1)?,
};
if index == 0 {
None
} else if index == self.transitions.len() - 1 {
let tzif_prev_trans = self.transitions[index].timestamp;
let Some(posix_tz) = self.posix_tz.as_ref() else {
return Some(tzif_prev_trans);
};
posix_tz.previous_transition(ts)
} else {
Some(self.transitions[index].timestamp)
}
}
pub(crate) fn next_transition(&self, ts: Timestamp) -> Option<Timestamp> {
assert!(!self.transitions.is_empty(), "transitions is non-empty");
let search =
self.transitions.binary_search_by_key(&ts, |t| t.timestamp);
let index = match search {
Ok(i) => i.checked_add(1)?,
Err(i) => i,
};
if index == 0 {
None
} else if index >= self.transitions.len() - 1 {
let tzif_next_trans =
self.transitions.last().expect("last transition").timestamp;
let Some(posix_tz) = self.posix_tz.as_ref() else {
return Some(tzif_next_trans);
};
posix_tz.next_transition(ts)
} else {
Some(self.transitions[index].timestamp)
}
}
fn local_time_type_to_offset(
&self,
typ: &LocalTimeType,
) -> (Offset, Dst, &str) {
(typ.offset, typ.is_dst, self.designation(typ))
}
fn designation(&self, typ: &LocalTimeType) -> &str {
&self.designations[typ.designation()]
}
fn local_time_type(&self, transition: &Transition) -> &LocalTimeType {
&self.types[usize::from(transition.type_index)]
}
fn first_transition(&self) -> &Transition {
self.transitions.first().unwrap()
}
fn parse32<'b>(
name: Option<String>,
header32: Header,
bytes: &'b [u8],
) -> Result<(Tzif, &'b [u8]), Error> {
let mut tzif = Tzif {
name,
version: header32.version,
checksum: 0,
transitions: vec![],
types: vec![],
designations: String::new(),
leap_seconds: vec![],
posix_tz: None,
};
let rest = tzif.parse_transitions(&header32, bytes)?;
let rest = tzif.parse_transition_types(&header32, rest)?;
let rest = tzif.parse_local_time_types(&header32, rest)?;
let rest = tzif.parse_time_zone_designations(&header32, rest)?;
let rest = tzif.parse_leap_seconds(&header32, rest)?;
let rest = tzif.parse_indicators(&header32, rest)?;
tzif.set_wall_datetimes();
Ok((tzif, rest))
}
fn parse64<'b>(
name: Option<String>,
header32: Header,
bytes: &'b [u8],
) -> Result<(Tzif, &'b [u8]), Error> {
let (_, rest) = try_split_at(
"V1 TZif data block",
bytes,
header32.data_block_len()?,
)?;
let (header64, rest) = Header::parse(8, rest)
.map_err(|e| e.context("failed to parse 64-bit header"))?;
let mut tzif = Tzif {
name,
version: header64.version,
checksum: 0,
transitions: vec![],
types: vec![],
designations: String::new(),
leap_seconds: vec![],
posix_tz: None,
};
let rest = tzif.parse_transitions(&header64, rest)?;
let rest = tzif.parse_transition_types(&header64, rest)?;
let rest = tzif.parse_local_time_types(&header64, rest)?;
let rest = tzif.parse_time_zone_designations(&header64, rest)?;
let rest = tzif.parse_leap_seconds(&header64, rest)?;
let rest = tzif.parse_indicators(&header64, rest)?;
let rest = tzif.parse_footer(&header64, rest)?;
if tzif.transitions.len() > 1 {
if let Some(ref tz) = tzif.posix_tz {
let last = tzif.transitions.last().expect("last transition");
let typ = tzif.local_time_type(last);
let (offset, dst, abbrev) = tz.to_offset(last.timestamp);
if offset != typ.offset {
return Err(err!(
"expected last transition to have DST offset \
of {}, but got {offset} according to POSIX TZ \
string {}",
typ.offset,
tz.as_str(),
));
}
if dst != typ.is_dst {
return Err(err!(
"expected last transition to have is_dst={}, \
but got is_dst={} according to POSIX TZ \
string {}",
typ.is_dst.is_dst(),
dst.is_dst(),
tz.as_str(),
));
}
if abbrev != tzif.designation(&typ) {
return Err(err!(
"expected last transition to have \
designation={abbrev}, \
but got designation={} according to POSIX TZ \
string {}",
tzif.designation(&typ),
tz.as_str(),
));
}
}
}
tzif.set_wall_datetimes();
Ok((tzif, rest))
}
fn parse_transitions<'b>(
&mut self,
header: &Header,
bytes: &'b [u8],
) -> Result<&'b [u8], Error> {
let (bytes, rest) = try_split_at(
"transition times data block",
bytes,
header.transition_times_len()?,
)?;
let mut it = bytes.chunks_exact(header.time_size);
self.transitions.push(Transition {
timestamp: Timestamp::MIN,
wall: TransitionWall::Unambiguous { start: DateTime::MIN },
type_index: 0,
});
while let Some(chunk) = it.next() {
let seconds = if header.is_32bit() {
i64::from(from_be_bytes_i32(chunk))
} else {
from_be_bytes_i64(chunk)
};
let timestamp =
Timestamp::from_second(seconds).unwrap_or_else(|_| {
let clamped = seconds
.clamp(UnixSeconds::MIN_REPR, UnixSeconds::MAX_REPR);
warn!(
"found Unix timestamp {seconds} that is outside \
Jiff's supported range, clamping to {clamped}",
);
Timestamp::from_second(clamped).unwrap()
});
self.transitions.push(Transition {
timestamp,
wall: TransitionWall::Unambiguous {
start: DateTime::default(),
},
type_index: 0,
});
}
assert!(it.remainder().is_empty());
Ok(rest)
}
fn parse_transition_types<'b>(
&mut self,
header: &Header,
bytes: &'b [u8],
) -> Result<&'b [u8], Error> {
let (bytes, rest) = try_split_at(
"transition types data block",
bytes,
header.transition_types_len()?,
)?;
for (transition_index, &type_index) in (1..).zip(bytes) {
if usize::from(type_index) >= header.tzh_typecnt {
return Err(err!(
"found transition type index {type_index},
but there are only {} local time types",
header.tzh_typecnt,
));
}
self.transitions[transition_index].type_index = type_index;
}
Ok(rest)
}
fn parse_local_time_types<'b>(
&mut self,
header: &Header,
bytes: &'b [u8],
) -> Result<&'b [u8], Error> {
let (bytes, rest) = try_split_at(
"local time types data block",
bytes,
header.local_time_types_len()?,
)?;
let mut it = bytes.chunks_exact(6);
while let Some(chunk) = it.next() {
let offset_seconds = from_be_bytes_i32(&chunk[..4]);
let offset =
Offset::from_seconds(offset_seconds).map_err(|e| {
err!(
"found local time type with out-of-bounds offset: {e}"
)
})?;
let is_dst = Dst::from(chunk[4] == 1);
let designation = chunk[5]..chunk[5];
self.types.push(LocalTimeType {
offset,
is_dst,
designation,
indicator: Indicator::LocalWall,
});
}
assert!(it.remainder().is_empty());
Ok(rest)
}
fn parse_time_zone_designations<'b>(
&mut self,
header: &Header,
bytes: &'b [u8],
) -> Result<&'b [u8], Error> {
let (bytes, rest) = try_split_at(
"time zone designations data block",
bytes,
header.time_zone_designations_len()?,
)?;
self.designations =
String::from_utf8(bytes.to_vec()).map_err(|_| {
err!(
"time zone designations are not valid UTF-8: {:?}",
Bytes(bytes),
)
})?;
for (i, typ) in self.types.iter_mut().enumerate() {
let start = usize::from(typ.designation.start);
let Some(suffix) = self.designations.get(start..) else {
return Err(err!(
"local time type {i} has designation index of {start}, \
but cannot be more than {}",
self.designations.len(),
));
};
let Some(len) = suffix.find('\x00') else {
return Err(err!(
"local time type {i} has designation index of {start}, \
but could not find NUL terminator after it in \
designations: {:?}",
self.designations,
));
};
let Some(end) = start.checked_add(len) else {
return Err(err!(
"local time type {i} has designation index of {start}, \
but its length {len} is too big",
));
};
typ.designation.end = u8::try_from(end).map_err(|_| {
err!(
"local time type {i} has designation range of \
{start}..{end}, but end is too big",
)
})?;
}
Ok(rest)
}
fn parse_leap_seconds<'b>(
&mut self,
header: &Header,
bytes: &'b [u8],
) -> Result<&'b [u8], Error> {
let (bytes, rest) = try_split_at(
"leap seconds data block",
bytes,
header.leap_second_len()?,
)?;
let chunk_len = header
.time_size
.checked_add(4)
.expect("time_size plus 4 fits in usize");
let mut it = bytes.chunks_exact(chunk_len);
while let Some(chunk) = it.next() {
let (occur_bytes, corr_bytes) = chunk.split_at(header.time_size);
let occur_seconds = if header.is_32bit() {
i64::from(from_be_bytes_i32(occur_bytes))
} else {
from_be_bytes_i64(occur_bytes)
};
let occurrence =
Timestamp::from_second(occur_seconds).map_err(|e| {
err!(
"leap second occurrence {occur_seconds} \
is out of range: {e}"
)
})?;
let correction = from_be_bytes_i32(corr_bytes);
self.leap_seconds.push(LeapSecond { occurrence, correction });
}
assert!(it.remainder().is_empty());
Ok(rest)
}
fn parse_indicators<'b>(
&mut self,
header: &Header,
bytes: &'b [u8],
) -> Result<&'b [u8], Error> {
let (std_wall_bytes, rest) = try_split_at(
"standard/wall indicators data block",
bytes,
header.standard_wall_len()?,
)?;
let (ut_local_bytes, rest) = try_split_at(
"UT/local indicators data block",
rest,
header.ut_local_len()?,
)?;
if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
for (i, &byte) in ut_local_bytes.iter().enumerate() {
if byte != 0 {
return Err(err!(
"found UT/local indicator '{byte}' for local time \
type {i}, but it must be 0 since all std/wall \
indicators are 0",
));
}
}
} else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() {
for (i, &byte) in std_wall_bytes.iter().enumerate() {
self.types[i].indicator = if byte == 0 {
Indicator::LocalWall
} else if byte == 1 {
Indicator::LocalStandard
} else {
return Err(err!(
"found invalid std/wall indicator '{byte}' for \
local time type {i}, it must be 0 or 1",
));
};
}
} else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
assert_eq!(std_wall_bytes.len(), ut_local_bytes.len());
let it = std_wall_bytes.iter().zip(ut_local_bytes);
for (i, (&stdwall, &utlocal)) in it.enumerate() {
self.types[i].indicator = match (stdwall, utlocal) {
(0, 0) => Indicator::LocalWall,
(1, 0) => Indicator::LocalStandard,
(1, 1) => Indicator::UTStandard,
(0, 1) => {
return Err(err!(
"found illegal ut-wall combination for \
local time type {i}, only local-wall, local-standard \
and ut-standard are allowed",
))
}
_ => {
return Err(err!(
"found illegal std/wall or ut/local value for \
local time type {i}, each must be 0 or 1",
))
}
};
}
} else {
debug_assert!(std_wall_bytes.is_empty());
debug_assert!(ut_local_bytes.is_empty());
}
Ok(rest)
}
fn parse_footer<'b>(
&mut self,
_header: &Header,
bytes: &'b [u8],
) -> Result<&'b [u8], Error> {
if bytes.is_empty() {
return Err(err!(
"invalid V2+ TZif footer, expected \\n, \
but found unexpected end of data",
));
}
if bytes[0] != b'\n' {
return Err(err!(
"invalid V2+ TZif footer, expected {:?}, but found {:?}",
Byte(b'\n'),
Byte(bytes[0]),
));
}
let bytes = &bytes[1..];
let toscan = &bytes[..bytes.len().min(1024)];
let Some(nlat) = toscan.iter().position(|&b| b == b'\n') else {
return Err(err!(
"invalid V2 TZif footer, could not find {:?} \
terminator in: {:?}",
Byte(b'\n'),
Bytes(toscan),
));
};
let (bytes, rest) = bytes.split_at(nlat);
if !bytes.is_empty() {
let iana_tz = IanaTz::parse_v3plus(bytes)?;
self.posix_tz = Some(iana_tz.into_tz());
}
Ok(&rest[1..])
}
fn set_wall_datetimes(&mut self) {
let mut prev = self.local_time_type(self.first_transition()).offset;
for i in 0..self.transitions.len() {
let this = self.local_time_type(&self.transitions[i]).offset;
let t = &mut self.transitions[i];
t.wall = if prev == this {
let start = prev.to_datetime(t.timestamp);
TransitionWall::Unambiguous { start }
} else if prev < this {
let start = prev.to_datetime(t.timestamp);
let end = this.to_datetime(t.timestamp);
TransitionWall::Gap { start, end }
} else {
assert!(prev > this);
let start = this.to_datetime(t.timestamp);
let end = prev.to_datetime(t.timestamp);
TransitionWall::Fold { start, end }
};
prev = this;
}
}
}
impl Eq for Tzif {}
impl PartialEq for Tzif {
fn eq(&self, rhs: &Tzif) -> bool {
self.name == rhs.name && self.checksum == rhs.checksum
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Transition {
timestamp: Timestamp,
wall: TransitionWall,
type_index: u8,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum TransitionWall {
Unambiguous {
start: DateTime,
},
Gap {
start: DateTime,
end: DateTime,
},
Fold {
start: DateTime,
end: DateTime,
},
}
impl TransitionWall {
fn start(&self) -> DateTime {
match *self {
TransitionWall::Unambiguous { start } => start,
TransitionWall::Gap { start, .. } => start,
TransitionWall::Fold { start, .. } => start,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct LocalTimeType {
offset: Offset,
is_dst: Dst,
designation: Range<u8>,
indicator: Indicator,
}
impl LocalTimeType {
fn designation(&self) -> Range<usize> {
usize::from(self.designation.start)..usize::from(self.designation.end)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Indicator {
LocalWall,
LocalStandard,
UTStandard,
}
impl core::fmt::Display for Indicator {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
Indicator::LocalWall => write!(f, "local/wall"),
Indicator::LocalStandard => write!(f, "local/std"),
Indicator::UTStandard => write!(f, "ut/std"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct LeapSecond {
occurrence: Timestamp,
correction: i32,
}
#[derive(Debug)]
struct Header {
time_size: usize,
version: u8,
tzh_ttisutcnt: usize,
tzh_ttisstdcnt: usize,
tzh_leapcnt: usize,
tzh_timecnt: usize,
tzh_typecnt: usize,
tzh_charcnt: usize,
}
impl Header {
fn parse(
time_size: usize,
bytes: &[u8],
) -> Result<(Header, &[u8]), Error> {
assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8");
if bytes.len() < 44 {
return Err(err!("invalid header: too short"));
}
let (magic, rest) = bytes.split_at(4);
if magic != b"TZif" {
return Err(err!("invalid header: magic bytes mismatch"));
}
let (version, rest) = rest.split_at(1);
let (_reserved, rest) = rest.split_at(15);
let (tzh_ttisutcnt_bytes, rest) = rest.split_at(4);
let (tzh_ttisstdcnt_bytes, rest) = rest.split_at(4);
let (tzh_leapcnt_bytes, rest) = rest.split_at(4);
let (tzh_timecnt_bytes, rest) = rest.split_at(4);
let (tzh_typecnt_bytes, rest) = rest.split_at(4);
let (tzh_charcnt_bytes, rest) = rest.split_at(4);
let tzh_ttisutcnt = from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes)
.map_err(|e| e.context("failed to parse tzh_ttisutcnt"))?;
let tzh_ttisstdcnt = from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes)
.map_err(|e| e.context("failed to parse tzh_ttisstdcnt"))?;
let tzh_leapcnt = from_be_bytes_u32_to_usize(tzh_leapcnt_bytes)
.map_err(|e| e.context("failed to parse tzh_leapcnt"))?;
let tzh_timecnt = from_be_bytes_u32_to_usize(tzh_timecnt_bytes)
.map_err(|e| e.context("failed to parse tzh_timecnt"))?;
let tzh_typecnt = from_be_bytes_u32_to_usize(tzh_typecnt_bytes)
.map_err(|e| e.context("failed to parse tzh_typecnt"))?;
let tzh_charcnt = from_be_bytes_u32_to_usize(tzh_charcnt_bytes)
.map_err(|e| e.context("failed to parse tzh_charcnt"))?;
if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt {
return Err(err!(
"expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \
or equal to tzh_typecnt={tzh_typecnt}",
));
}
if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt {
return Err(err!(
"expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \
or equal to tzh_typecnt={tzh_typecnt}",
));
}
if tzh_typecnt < 1 {
return Err(err!(
"expected tzh_typecnt={tzh_typecnt} to be at least 1",
));
}
if tzh_charcnt < 1 {
return Err(err!(
"expected tzh_charcnt={tzh_charcnt} to be at least 1",
));
}
let header = Header {
time_size,
version: version[0],
tzh_ttisutcnt,
tzh_ttisstdcnt,
tzh_leapcnt,
tzh_timecnt,
tzh_typecnt,
tzh_charcnt,
};
Ok((header, rest))
}
fn is_32bit(&self) -> bool {
self.time_size == 4
}
fn data_block_len(&self) -> Result<usize, Error> {
let a = self.transition_times_len()?;
let b = self.transition_types_len()?;
let c = self.local_time_types_len()?;
let d = self.time_zone_designations_len()?;
let e = self.leap_second_len()?;
let f = self.standard_wall_len()?;
let g = self.ut_local_len()?;
a.checked_add(b)
.and_then(|z| z.checked_add(c))
.and_then(|z| z.checked_add(d))
.and_then(|z| z.checked_add(e))
.and_then(|z| z.checked_add(f))
.and_then(|z| z.checked_add(g))
.ok_or_else(|| {
err!(
"length of data block in V{} tzfile is too big",
self.version
)
})
}
fn transition_times_len(&self) -> Result<usize, Error> {
self.tzh_timecnt.checked_mul(self.time_size).ok_or_else(|| {
err!("tzh_timecnt value {} is too big", self.tzh_timecnt)
})
}
fn transition_types_len(&self) -> Result<usize, Error> {
Ok(self.tzh_timecnt)
}
fn local_time_types_len(&self) -> Result<usize, Error> {
self.tzh_typecnt.checked_mul(6).ok_or_else(|| {
err!("tzh_typecnt value {} is too big", self.tzh_typecnt)
})
}
fn time_zone_designations_len(&self) -> Result<usize, Error> {
Ok(self.tzh_charcnt)
}
fn leap_second_len(&self) -> Result<usize, Error> {
let record_len = self
.time_size
.checked_add(4)
.expect("4-or-8 plus 4 always fits in usize");
self.tzh_leapcnt.checked_mul(record_len).ok_or_else(|| {
err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt)
})
}
fn standard_wall_len(&self) -> Result<usize, Error> {
Ok(self.tzh_ttisstdcnt)
}
fn ut_local_len(&self) -> Result<usize, Error> {
Ok(self.tzh_ttisutcnt)
}
}
pub(crate) fn is_possibly_tzif(data: &[u8]) -> bool {
data.starts_with(b"TZif")
}
fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result<usize, Error> {
let n = from_be_bytes_u32(bytes);
usize::try_from(n).map_err(|_| {
err!(
"failed to parse integer {n} (too big, max allowed is {}",
usize::MAX
)
})
}
fn from_be_bytes_u32(bytes: &[u8]) -> u32 {
u32::from_be_bytes(bytes.try_into().unwrap())
}
fn from_be_bytes_i32(bytes: &[u8]) -> i32 {
i32::from_be_bytes(bytes.try_into().unwrap())
}
fn from_be_bytes_i64(bytes: &[u8]) -> i64 {
i64::from_be_bytes(bytes.try_into().unwrap())
}
fn try_split_at<'b>(
what: &'static str,
bytes: &'b [u8],
at: usize,
) -> Result<(&'b [u8], &'b [u8]), Error> {
if at > bytes.len() {
Err(err!(
"expected at least {at} bytes for {what}, \
but found only {} bytes",
bytes.len(),
))
} else {
Ok(bytes.split_at(at))
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use crate::tz::testdata::TZIF_TEST_FILES;
use super::*;
fn tzif_to_human_readable(tzif: &Tzif) -> String {
use std::io::Write;
let mut out = tabwriter::TabWriter::new(vec![])
.alignment(tabwriter::Alignment::Left);
writeln!(out, "TIME ZONE NAME").unwrap();
writeln!(out, " {}", tzif.name().unwrap_or("UNNAMED")).unwrap();
writeln!(out, "TIME ZONE VERSION").unwrap();
writeln!(out, " {}", char::try_from(tzif.version).unwrap()).unwrap();
writeln!(out, "LOCAL TIME TYPES").unwrap();
for (i, typ) in tzif.types.iter().enumerate() {
writeln!(
out,
" {i:03}:\toffset={off}\t\
designation={desig}\t{dst}\tindicator={ind}",
off = typ.offset,
desig = tzif.designation(&typ),
dst = if typ.is_dst.is_dst() { "dst" } else { "" },
ind = typ.indicator,
)
.unwrap();
}
if !tzif.transitions.is_empty() {
writeln!(out, "TRANSITIONS").unwrap();
for (i, t) in tzif.transitions.iter().enumerate() {
let dt = Offset::UTC.to_datetime(t.timestamp);
let typ = &tzif.types[usize::from(t.type_index)];
let wall = alloc::format!("{:?}", t.wall.start());
let ambiguous = match t.wall {
TransitionWall::Unambiguous { .. } => {
"unambiguous".to_string()
}
TransitionWall::Gap { end, .. } => {
alloc::format!(" gap-until({end:?})")
}
TransitionWall::Fold { end, .. } => {
alloc::format!("fold-until({end:?})")
}
};
writeln!(
out,
" {i:04}:\t{dt:?}Z\tunix={ts}\twall={wall}\t\
{ambiguous}\t\
type={type_index}\t{off}\t\
{desig}\t{dst}",
ts = t.timestamp.as_second(),
type_index = t.type_index,
off = typ.offset,
desig = tzif.designation(typ),
dst = if typ.is_dst.is_dst() { "dst" } else { "" },
)
.unwrap();
}
}
if !tzif.leap_seconds.is_empty() {
writeln!(out, "LEAP SECONDS").unwrap();
for ls in tzif.leap_seconds.iter() {
let dt = Offset::UTC.to_datetime(ls.occurrence);
let c = ls.correction;
writeln!(out, " {dt:?}\tcorrection={c}").unwrap();
}
}
if let Some(ref posix_tz) = tzif.posix_tz {
writeln!(out, "POSIX TIME ZONE STRING").unwrap();
writeln!(out, " {}", posix_tz.as_str()).unwrap();
}
String::from_utf8(out.into_inner().unwrap()).unwrap()
}
#[cfg(feature = "std")]
#[test]
fn debug_tzif() -> anyhow::Result<()> {
use anyhow::Context;
let _ = crate::logging::Logger::init();
const ENV: &str = "JIFF_DEBUG_TZIF_PATH";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let Ok(val) = val.into_string() else {
anyhow::bail!("{ENV} has invalid UTF-8")
};
let bytes =
std::fs::read(&val).with_context(|| alloc::format!("{val:?}"))?;
let tzif = Tzif::parse(Some(val.to_string()), &bytes)?;
std::eprint!("{}", tzif_to_human_readable(&tzif));
Ok(())
}
#[test]
fn tzif_parse_v2plus() {
for tzif_test in TZIF_TEST_FILES {
insta::assert_snapshot!(
alloc::format!("{}_v2+", tzif_test.name),
tzif_to_human_readable(&tzif_test.parse())
);
}
}
#[test]
fn tzif_parse_v1() {
for tzif_test in TZIF_TEST_FILES {
insta::assert_snapshot!(
alloc::format!("{}_v1", tzif_test.name),
tzif_to_human_readable(&tzif_test.parse_v1())
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn zoneinfo() {
const TZDIR: &str = "/usr/share/zoneinfo";
for result in walkdir::WalkDir::new(TZDIR) {
let Ok(dent) = result else { continue };
let Some(name) = dent.path().to_str() else { continue };
if name.contains("right/") || name.contains("posix/") {
continue;
}
let Ok(bytes) = std::fs::read(dent.path()) else { continue };
if !is_possibly_tzif(&bytes) {
continue;
}
let tzname = dent
.path()
.strip_prefix(TZDIR)
.unwrap_or_else(|_| {
panic!("all paths in TZDIR have {TZDIR:?} prefix")
})
.to_str()
.expect("all paths to be valid UTF-8")
.to_string();
if let Err(err) = Tzif::parse(Some(tzname), &bytes) {
panic!("failed to parse TZif file {:?}: {err}", dent.path());
}
}
}
}