tzif-codec 0.1.4

Codec, validator, and builder for RFC 9636 TZif files
Documentation
use crate::{
    common::TimeSize, footer::validate_footer, leap::validate_leap_seconds, DataBlock, TzifError,
    TzifFile, Version,
};

pub fn validate_file(file: &TzifFile) -> Result<(), TzifError> {
    validate_block(&file.v1, TimeSize::ThirtyTwo, file.version)?;
    if file.version == Version::V1 {
        if file.v2_plus.is_some() || file.footer.is_some() {
            return Err(TzifError::UnexpectedV2PlusData);
        }
    } else if let (Some(block), Some(footer)) = (&file.v2_plus, &file.footer) {
        validate_block(block, TimeSize::SixtyFour, file.version)?;
        validate_footer(file.version, block, footer)?;
    } else {
        return Err(TzifError::MissingV2PlusData(file.version));
    }
    Ok(())
}

fn validate_block(
    block: &DataBlock,
    time_size: TimeSize,
    version: Version,
) -> Result<(), TzifError> {
    if block.local_time_types.is_empty() {
        return Err(TzifError::EmptyLocalTimeTypes);
    }
    if block.local_time_types.len() > 256 {
        return Err(TzifError::TooManyLocalTimeTypes(
            block.local_time_types.len(),
        ));
    }
    if block.designations.is_empty() {
        return Err(TzifError::EmptyDesignations);
    }
    if block.transition_times.len() != block.transition_types.len() {
        return Err(TzifError::CountMismatch {
            field: "transition_types",
            expected: block.transition_times.len(),
            actual: block.transition_types.len(),
        });
    }
    validate_optional_indicator_count(
        "standard_wall_indicators",
        block.local_time_types.len(),
        block.standard_wall_indicators.len(),
    )?;
    validate_optional_indicator_count(
        "ut_local_indicators",
        block.local_time_types.len(),
        block.ut_local_indicators.len(),
    )?;
    validate_indicator_relationship(block)?;
    validate_u32_count("timecnt", block.transition_times.len())?;
    validate_u32_count("typecnt", block.local_time_types.len())?;
    validate_u32_count("charcnt", block.designations.len())?;
    validate_u32_count("leapcnt", block.leap_seconds.len())?;
    validate_u32_count("isstdcnt", block.standard_wall_indicators.len())?;
    validate_u32_count("isutcnt", block.ut_local_indicators.len())?;
    validate_strictly_ascending_transitions(block)?;
    for (index, &transition_type) in block.transition_types.iter().enumerate() {
        if usize::from(transition_type) >= block.local_time_types.len() {
            return Err(TzifError::InvalidTransitionType {
                index,
                transition_type,
            });
        }
    }
    for (index, local_time_type) in block.local_time_types.iter().enumerate() {
        if local_time_type.utc_offset == i32::MIN {
            return Err(TzifError::InvalidUtcOffset { index });
        }
        if usize::from(local_time_type.designation_index) >= block.designations.len() {
            return Err(TzifError::InvalidDesignationIndex {
                index,
                designation_index: local_time_type.designation_index,
            });
        }
        let Some(designation) = designation_at(block, local_time_type.designation_index) else {
            return Err(TzifError::UnterminatedDesignation {
                index,
                designation_index: local_time_type.designation_index,
            });
        };
        if !is_valid_wire_designation(block, designation) {
            return Err(TzifError::InvalidDesignation {
                index,
                designation: designation.to_vec(),
            });
        }
    }
    validate_leap_seconds(block, version)?;
    if matches!(time_size, TimeSize::ThirtyTwo) {
        for (index, &value) in block.transition_times.iter().enumerate() {
            if i32::try_from(value).is_err() {
                return Err(TzifError::Version1TransitionOutOfRange { index, value });
            }
        }
        for (index, leap_second) in block.leap_seconds.iter().enumerate() {
            if i32::try_from(leap_second.occurrence).is_err() {
                return Err(TzifError::Version1LeapSecondOutOfRange {
                    index,
                    value: leap_second.occurrence,
                });
            }
        }
    }
    Ok(())
}

fn validate_strictly_ascending_transitions(block: &DataBlock) -> Result<(), TzifError> {
    for (index, pair) in block.transition_times.windows(2).enumerate() {
        let [previous, next] = pair else {
            continue;
        };
        if previous >= next {
            return Err(TzifError::TransitionTimesNotAscending { index: index + 1 });
        }
    }
    Ok(())
}

fn validate_indicator_relationship(block: &DataBlock) -> Result<(), TzifError> {
    if block.ut_local_indicators.is_empty() {
        return Ok(());
    }
    for (index, &is_ut) in block.ut_local_indicators.iter().enumerate() {
        let is_standard = block
            .standard_wall_indicators
            .get(index)
            .copied()
            .unwrap_or(false);
        if is_ut && !is_standard {
            return Err(TzifError::InvalidUtLocalIndicatorCombination { index });
        }
    }
    Ok(())
}

fn is_valid_wire_designation(block: &DataBlock, designation: &[u8]) -> bool {
    if designation == b"-00" {
        return true;
    }
    if designation.is_empty() {
        return is_placeholder_block(block);
    }
    (3..=6).contains(&designation.len())
        && designation
            .iter()
            .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'+' || *byte == b'-')
}

fn is_placeholder_block(block: &DataBlock) -> bool {
    block.transition_times.is_empty()
        && block.transition_types.is_empty()
        && block.local_time_types.len() == 1
        && block.designations == [0]
}

fn designation_at(block: &DataBlock, designation_index: u8) -> Option<&[u8]> {
    let bytes = block.designations.get(usize::from(designation_index)..)?;
    let end = bytes.iter().position(|&byte| byte == 0)?;
    bytes.get(..end)
}

const fn validate_optional_indicator_count(
    field: &'static str,
    type_count: usize,
    actual: usize,
) -> Result<(), TzifError> {
    if actual != 0 && actual != type_count {
        return Err(TzifError::CountMismatch {
            field,
            expected: type_count,
            actual,
        });
    }
    Ok(())
}

fn validate_u32_count(field: &'static str, count: usize) -> Result<(), TzifError> {
    if u32::try_from(count).is_err() {
        return Err(TzifError::CountOverflow { field, count });
    }
    Ok(())
}