tzif-codec 0.1.1

Codec, validator, and builder for RFC 9636 TZif files
Documentation
use crate::Version;
use thiserror::Error;

#[derive(Debug, Error, PartialEq, Eq)]
pub enum TzifBuildError {
    #[error("time zone designation must not be empty")]
    EmptyDesignation,
    #[error("time zone designation {designation:?} must be ASCII")]
    NonAsciiDesignation { designation: String },
    #[error("time zone designation {designation:?} contains unsupported character {character:?}")]
    UnsupportedDesignationCharacter {
        designation: String,
        character: char,
    },
    #[error("time zone designation {designation:?} must be at least 3 characters")]
    DesignationTooShort { designation: String },
    #[error("time zone designation {designation:?} must be at most 6 characters")]
    DesignationTooLong { designation: String },
    #[error("duplicate time zone designation {0:?}")]
    DuplicateDesignation(String),
    #[error("unknown time zone designation {0:?}")]
    UnknownDesignation(String),
    #[error("explicit transition times must be sorted in ascending order")]
    UnsortedTransitions,
    #[error("UTC offset -2^31 is not valid in TZif local time types")]
    InvalidUtcOffset,
    #[error("UTC offset {seconds} cannot be represented in a POSIX TZ footer")]
    PosixOffsetOutOfRange { seconds: i32 },
    #[error("TZif version {version:?} cannot represent transition timestamp {timestamp}")]
    TransitionOutOfRangeForVersion { version: Version, timestamp: i64 },
    #[error("TZif version {version:?} cannot include a footer")]
    VersionCannotIncludeFooter { version: Version },
    #[error("TZif version {version:?} cannot represent the POSIX TZ string extension")]
    VersionCannotRepresentFooterExtension { version: Version },
    #[error("POSIX TZ month {month} is outside the range 1..=12")]
    InvalidPosixMonth { month: u8 },
    #[error("POSIX TZ week {week} is outside the range 1..=5")]
    InvalidPosixWeek { week: u8 },
    #[error("POSIX TZ weekday {weekday} is outside the range 0..=6")]
    InvalidPosixWeekday { weekday: u8 },
    #[error("POSIX TZ Julian day {day} is outside the range 1..=365")]
    InvalidPosixJulianDay { day: u16 },
    #[error("POSIX TZ zero-based day {day} is outside the range 0..=365")]
    InvalidPosixZeroBasedDay { day: u16 },
    #[error("POSIX TZ transition time {seconds} seconds is outside the supported range")]
    InvalidPosixTransitionTime { seconds: i32 },
    #[error(transparent)]
    InvalidTzif(#[from] TzifError),
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum TzdistError {
    #[error(transparent)]
    InvalidTzif(#[from] TzifError),
    #[error("unsupported TZif media type {0}")]
    UnsupportedMediaType(String),
    #[error("TZDIST capabilities must advertise application/tzif when advertising application/tzif-leap")]
    TzifLeapCapabilityRequiresTzif,
    #[error("application/tzif MUST NOT contain leap-second records; use application/tzif-leap")]
    LeapSecondsNotAllowedForApplicationTzif,
    #[error("TZDIST truncation requires a version 2 or later TZif file")]
    TruncationRequiresVersion2Plus,
    #[error("TZDIST truncation requires at least one version 2+ transition")]
    TruncationRequiresVersion2PlusTransitions,
    #[error("start truncation transition mismatch: expected {expected}, got {actual}")]
    StartTruncationTransitionMismatch { expected: i64, actual: i64 },
    #[error("start truncation requires time type 0 to be a -00 placeholder")]
    StartTruncationTypeZeroMustBePlaceholder,
    #[error("end truncation transition mismatch: expected {expected}, got {actual}")]
    EndTruncationTransitionMismatch { expected: i64, actual: i64 },
    #[error("end truncation requires an empty TZ string footer")]
    EndTruncationRequiresEmptyFooter,
    #[error("end truncation requires the last transition type to be a -00 placeholder")]
    EndTruncationLastTypeMustBePlaceholder,
    #[error("invalid TZDIST truncation range: start {start} must be before end {end}")]
    InvalidTruncationRange { start: i64, end: i64 },
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum TzifError {
    #[error("input ended unexpectedly at byte {offset} while reading {context}")]
    UnexpectedEof {
        offset: usize,
        context: &'static str,
    },
    #[error("expected TZif magic at byte {offset}")]
    InvalidMagic { offset: usize },
    #[error("invalid TZif version byte 0x{0:02x}")]
    InvalidVersion(u8),
    #[error("version mismatch between first header {first:?} and second header {second:?}")]
    VersionMismatch { first: Version, second: Version },
    #[error("expected newline at byte {offset} before TZif footer")]
    MissingFooterStart { offset: usize },
    #[error("missing newline terminator for TZif footer starting at byte {offset}")]
    MissingFooterEnd { offset: usize },
    #[error("TZif footer is not valid UTF-8")]
    InvalidFooterUtf8,
    #[error("trailing data starts at byte {offset}")]
    TrailingData { offset: usize },
    #[error("{field} count {count} cannot fit in memory on this platform")]
    CountTooLarge { field: &'static str, count: u32 },
    #[error("data block byte length overflow while calculating {field}")]
    DataBlockLengthOverflow { field: &'static str },
    #[error("local time type {index} has invalid isdst value {value}")]
    InvalidDstIndicator { index: usize, value: u8 },
    #[error("{field} indicator {index} has invalid value {value}")]
    InvalidBooleanIndicator {
        field: &'static str,
        index: usize,
        value: u8,
    },
    #[error("version 1 files must not include a v2+ data block or footer")]
    UnexpectedV2PlusData,
    #[error("version {0:?} files must include a v2+ data block and footer")]
    MissingV2PlusData(Version),
    #[error("{field} has {actual} entries, but expected {expected}")]
    CountMismatch {
        field: &'static str,
        expected: usize,
        actual: usize,
    },
    #[error("{field} count {count} exceeds TZif u32 count range")]
    CountOverflow { field: &'static str, count: usize },
    #[error("local time type count must not be zero")]
    EmptyLocalTimeTypes,
    #[error("designation table must not be empty")]
    EmptyDesignations,
    #[error("local time type count {0} exceeds 256")]
    TooManyLocalTimeTypes(usize),
    #[error(
        "transition type {transition_type} at index {index} does not reference a local time type"
    )]
    InvalidTransitionType { index: usize, transition_type: u8 },
    #[error("designation index {designation_index} at local time type {index} is out of range")]
    InvalidDesignationIndex { index: usize, designation_index: u8 },
    #[error("transition time {value} at index {index} is outside the version 1 i32 range")]
    Version1TransitionOutOfRange { index: usize, value: i64 },
    #[error("leap-second occurrence {value} at index {index} is outside the version 1 i32 range")]
    Version1LeapSecondOutOfRange { index: usize, value: i64 },
    #[error("transition time at index {index} is not strictly ascending")]
    TransitionTimesNotAscending { index: usize },
    #[error(
        "designation index {designation_index} at local time type {index} has no NUL terminator"
    )]
    UnterminatedDesignation { index: usize, designation_index: u8 },
    #[error("local time type {index} has invalid UTC offset -2^31")]
    InvalidUtcOffset { index: usize },
    #[error("time zone designation at local time type {index} violates RFC 9636 designation requirements")]
    InvalidDesignation { index: usize, designation: Vec<u8> },
    #[error("UT/local indicator {index} is set without the corresponding standard/wall indicator")]
    InvalidUtLocalIndicatorCombination { index: usize },
    #[error("first leap-second occurrence {value} at index 0 must be non-negative")]
    FirstLeapSecondOccurrenceNegative { value: i64 },
    #[error("leap-second occurrence at index {index} is not strictly ascending")]
    LeapSecondOccurrencesNotAscending { index: usize },
    #[error("first leap-second correction {correction} must be +1 or -1 unless using version 4 truncation")]
    InvalidFirstLeapSecondCorrection { correction: i32 },
    #[error("leap-second correction at index {index} must differ from the previous correction by +1 or -1")]
    InvalidLeapSecondCorrection { index: usize },
    #[error("version {version:?} cannot contain a leap-second table truncated at the start")]
    LeapSecondTruncationRequiresVersion4 { version: Version },
    #[error("version {version:?} cannot contain a leap-second table expiration time")]
    LeapSecondExpirationRequiresVersion4 { version: Version },
    #[error("leap-second occurrence at index {index} does not occur at a UTC month boundary")]
    LeapSecondOccurrenceNotAtMonthEnd { index: usize },
    #[error("TZif footer must be ASCII")]
    InvalidFooterAscii,
    #[error("TZif footer must not contain NUL or newline bytes")]
    InvalidFooterControlByte,
    #[error("TZif version {version:?} cannot use the POSIX TZ string extension")]
    FooterExtensionRequiresVersion3 { version: Version },
    #[error("TZif footer is not a valid POSIX TZ string")]
    InvalidFooterSyntax,
    #[error("TZif footer is inconsistent with the last transition type")]
    FooterInconsistentWithLastTransition,
}