tzif-codec 0.1.2

Codec, validator, and builder for RFC 9636 TZif files
Documentation
#![allow(
    clippy::needless_pass_by_value,
    reason = "integration tests pass owned warning fixtures"
)]

mod common;

use common::{AssertOk, TestResult};
use tzif_codec::{DataBlock, InteroperabilityWarning, LeapSecond, LocalTimeType, TzifFile};

#[test]
fn warns_when_version_one_data_omits_representable_v2_transitions() -> TestResult {
    let file = TzifFile::v2(
        DataBlock::placeholder(),
        DataBlock {
            transition_times: vec![0],
            transition_types: vec![0],
            local_time_types: vec![ltt(0, false, 0)],
            designations: b"UTC\0".to_vec(),
            leap_seconds: vec![],
            standard_wall_indicators: vec![],
            ut_local_indicators: vec![],
        },
        "",
    );

    assert_contains(
        &file,
        InteroperabilityWarning::VersionOneDataMayBeIncomplete,
    )?;

    Ok(())
}

#[test]
fn warns_about_footer_dependent_files() -> TestResult {
    let file = TzifFile::v3(DataBlock::placeholder(), DataBlock::placeholder(), "<UTC>0");
    let warnings = file.interoperability_warnings().assert_ok()?;

    assert!(warnings
        .contains(&InteroperabilityWarning::VersionThreeOrLaterFooterMayConfuseVersionTwoReaders));
    assert!(warnings.contains(&InteroperabilityWarning::FooterMayBeIgnoredByReaders));
    assert!(warnings.contains(&InteroperabilityWarning::FooterContainsAngleBracket));

    Ok(())
}

#[test]
fn warns_about_version_four_leap_second_tables() -> TestResult {
    let mut block = DataBlock::placeholder();
    block.leap_seconds.push(LeapSecond {
        occurrence: 1_483_228_826,
        correction: 27,
    });
    let file = TzifFile::v4(DataBlock::placeholder(), block, "");

    assert_contains(
        &file,
        InteroperabilityWarning::VersionFourLeapSecondTableMayConfuseStrictRfc8536Readers,
    )?;

    Ok(())
}

#[test]
fn warns_when_first_transition_is_not_an_early_noop() -> TestResult {
    let file = TzifFile::v1(DataBlock {
        transition_times: vec![0],
        transition_types: vec![1],
        local_time_types: vec![ltt(0, false, 0), ltt(3_600, false, 4)],
        designations: b"UTC\0ONE\0".to_vec(),
        leap_seconds: vec![],
        standard_wall_indicators: vec![],
        ut_local_indicators: vec![],
    });
    let warnings = file.interoperability_warnings().assert_ok()?;

    assert!(warnings.contains(&InteroperabilityWarning::MissingEarlyNoOpTransition { block: "v1" }));
    assert!(warnings.contains(
        &InteroperabilityWarning::FirstTransitionAfterRecommendedCompatibilityPoint {
            block: "v1",
            timestamp: 0,
        }
    ));

    Ok(())
}

#[test]
fn warns_about_extreme_and_negative_transitions() -> TestResult {
    let file = TzifFile::v2(
        DataBlock::placeholder(),
        DataBlock {
            transition_times: vec![i64::MIN, -1],
            transition_types: vec![0, 0],
            local_time_types: vec![ltt(0, false, 0)],
            designations: b"UTC\0".to_vec(),
            leap_seconds: vec![],
            standard_wall_indicators: vec![],
            ut_local_indicators: vec![],
        },
        "",
    );
    let warnings = file.interoperability_warnings().assert_ok()?;

    assert!(
        warnings.contains(&InteroperabilityWarning::MinimumI64Transition {
            block: "v2_plus",
            index: 0,
        })
    );
    assert!(warnings.contains(
        &InteroperabilityWarning::TransitionBeforeRecommendedLowerBound {
            block: "v2_plus",
            index: 0,
            timestamp: i64::MIN,
        }
    ));
    assert!(
        warnings.contains(&InteroperabilityWarning::NegativeTransition {
            block: "v2_plus",
            index: 1,
            timestamp: -1,
        })
    );

    Ok(())
}

#[test]
fn warns_about_non_portable_designations() -> TestResult {
    let file = TzifFile::v1(DataBlock {
        transition_times: vec![],
        transition_types: vec![],
        local_time_types: vec![ltt(0, false, 0)],
        designations: b"-00\0".to_vec(),
        leap_seconds: vec![],
        standard_wall_indicators: vec![],
        ut_local_indicators: vec![],
    });
    let warnings = file.interoperability_warnings().assert_ok()?;

    assert!(
        warnings.contains(&InteroperabilityWarning::UnspecifiedLocalTimeDesignation {
            block: "v1",
            index: 0,
        })
    );

    Ok(())
}

#[test]
fn warns_about_offsets_known_to_break_old_readers() -> TestResult {
    let file = TzifFile::v1(DataBlock {
        transition_times: vec![],
        transition_types: vec![],
        local_time_types: vec![
            ltt(14 * 3_600, false, 0),
            ltt(-30, false, 5),
            ltt(5 * 3_600 + 10 * 60, false, 9),
            ltt(3_600, false, 13),
            ltt(0, true, 17),
            ltt(94_000, false, 21),
        ],
        designations: b"LINT\0NEG\0ODD\0IST\0GMT\0BIG\0".to_vec(),
        leap_seconds: vec![LeapSecond {
            occurrence: 78_796_800,
            correction: 1,
        }],
        standard_wall_indicators: vec![],
        ut_local_indicators: vec![],
    });
    let warnings = file.interoperability_warnings().assert_ok()?;

    assert!(
        warnings.contains(&InteroperabilityWarning::OffsetOutsideConventionalRange {
            block: "v1",
            index: 0,
            offset: 14 * 3_600,
        })
    );
    assert!(
        warnings.contains(&InteroperabilityWarning::OffsetOutsideRecommendedRange {
            block: "v1",
            index: 5,
            offset: 94_000,
        })
    );
    assert!(
        warnings.contains(&InteroperabilityWarning::NegativeSubHourOffset {
            block: "v1",
            index: 1,
            offset: -30,
        })
    );
    assert!(
        warnings.contains(&InteroperabilityWarning::OffsetNotMultipleOfMinute {
            block: "v1",
            index: 1,
            offset: -30,
        })
    );
    assert!(
        warnings.contains(&InteroperabilityWarning::OffsetNotMultipleOfQuarterHour {
            block: "v1",
            index: 2,
            offset: 5 * 3_600 + 10 * 60,
        })
    );
    assert!(
        warnings.contains(&InteroperabilityWarning::LeapSecondWithSubMinuteOffset {
            block: "v1",
            offset: -30,
        })
    );
    assert!(warnings.contains(
        &InteroperabilityWarning::DaylightOffsetLessThanStandardOffset {
            block: "v1",
            daylight_offset: 0,
            standard_offset: 94_000,
        }
    ));

    Ok(())
}

#[test]
fn warns_about_unused_local_time_types_and_designation_octets() -> TestResult {
    let file = TzifFile::v1(DataBlock {
        transition_times: vec![0],
        transition_types: vec![1],
        local_time_types: vec![ltt(0, false, 0), ltt(3_600, false, 4), ltt(7_200, false, 8)],
        designations: b"UTC\0ONE\0TWO\0XXX\0".to_vec(),
        leap_seconds: vec![],
        standard_wall_indicators: vec![],
        ut_local_indicators: vec![],
    });
    let warnings = file.interoperability_warnings().assert_ok()?;

    assert!(
        warnings.contains(&InteroperabilityWarning::UnusedLocalTimeType {
            block: "v1",
            index: 2,
        })
    );
    for index in 12..=15 {
        assert!(
            warnings
                .contains(&InteroperabilityWarning::UnusedDesignationOctet { block: "v1", index }),
            "missing unused designation octet warning for index {index}: {warnings:?}"
        );
    }

    Ok(())
}

const fn ltt(utc_offset: i32, is_dst: bool, designation_index: u8) -> LocalTimeType {
    LocalTimeType {
        utc_offset,
        is_dst,
        designation_index,
    }
}

fn assert_contains(file: &TzifFile, warning: InteroperabilityWarning) -> TestResult {
    let warnings = file.interoperability_warnings().assert_ok()?;
    assert!(
        warnings.contains(&warning),
        "missing warning {warning:?}; got {warnings:?}"
    );
    Ok(())
}