dash-runtime 0.4.0

Sans-IO MPEG-DASH (ISO/IEC 23009-1): MPD model and parser, segment addressing, UTC clock and live-edge arithmetic, and a client for static and dynamic presentations. No I/O, no clock reads.
Documentation
//! The accept/reject table of `private/specs-verified/dash/docs-specs/xs-datetime-subset.md`, whose
//! instants were hand-checked against an independent calendar implementation.

use dash_runtime::timing::datetime::DateTimeError;
use dash_runtime::timing::datetime::parse_ntp_timestamp;
use dash_runtime::timing::datetime::parse_xs_datetime;

const SECOND: i64 = 1_000_000;

#[test]
fn accepted_table() {
    let cases: &[(&str, i64)] = &[
        ("1970-01-01T00:00:00Z", 0),
        ("2026-10-26T08:39:04.952Z", 1_793_003_944 * SECOND + 952_000),
        ("2026-10-26T09:18:20.766Z", 1_793_006_300 * SECOND + 766_000),
        ("2002-10-10T12:00:00-05:00", 1_034_269_200 * SECOND),
        ("2026-03-01T00:00:00+14:00", 1_772_272_800 * SECOND),
        ("2026-04-01T00:00:00+05:30", 1_774_981_800 * SECOND),
        ("2026-02-01T00:00:00", 1_769_904_000 * SECOND),
        ("2026-02-01T24:00:00Z", 1_769_990_400 * SECOND),
        ("2024-02-29T12:00:00Z", 1_709_208_000 * SECOND),
        ("0001-01-01T00:00:00Z", -62_135_596_800 * SECOND),
        ("9999-12-30T22:00:00Z", 253_402_207_200 * SECOND),
        (
            "2026-10-26T08:39:04.123456789012Z",
            1_793_003_944 * SECOND + 123_456,
        ),
        ("  2026-02-01T00:00:00Z\n", 1_769_904_000 * SECOND),
    ];
    for (text, us) in cases {
        let got = parse_xs_datetime(text).unwrap_or_else(|e| panic!("{text}: {e}"));
        assert_eq!(got.unix_us, *us, "{text}");
    }
}

#[test]
fn rejected_table() {
    for text in [
        "2025-02-29T12:00:00Z",
        "2026-13-01T00:00:00Z",
        "2016-12-31T23:59:60Z",
        "2026-02-01T24:00:01Z",
        "2026-02-01T24:00:00.5Z",
        "-0001-01-01T00:00:00Z",
        "12026-01-01T00:00:00Z",
        "0000-01-01T00:00:00Z",
        "2026-02-01T00:00:00+14:30",
        "2026-02-01T00:00:00+0100",
        "2026-02-01t00:00:00z",
        "2026-02-01 00:00:00Z",
        "20260201T000000Z",
        "2026-02-01T00:00:00.Z",
        "2026-02-01T00:00:00,5Z",
        "9999-12-31T23:59:59Z",
        "",
        "2026-02-01",
        "2026-02-01T00:00:00Z extra",
        "2026-02-01T00:00:00 Z",
    ] {
        assert!(
            matches!(
                parse_xs_datetime(text),
                Err(dash_runtime::timing::datetime::DateTimeError::Field(..)
                    | dash_runtime::timing::datetime::DateTimeError::OutOfRange
                    | dash_runtime::timing::datetime::DateTimeError::Syntax(..))
            ),
            "{text:?} must be rejected"
        );
    }
}

#[test]
fn resolution_follows_fraction_digits() {
    let r = |t: &str| parse_xs_datetime(t).unwrap().resolution_us();
    assert_eq!(r("2026-10-26T08:39:04Z"), 1_000_000);
    assert_eq!(r("2026-10-26T08:39:04.9Z"), 100_000);
    assert_eq!(r("2026-10-26T08:39:04.952Z"), 1_000);
    assert_eq!(r("2026-10-26T08:39:04.952123Z"), 1);
    assert_eq!(r("2026-10-26T08:39:04.952123789Z"), 1);
}

#[test]
fn errors_are_typed() {
    assert!(matches!(
        parse_xs_datetime("2026-13-01T00:00:00Z"),
        Err(DateTimeError::Field(_))
    ));
    assert!(matches!(
        parse_xs_datetime("9999-12-31T23:59:59Z"),
        Err(DateTimeError::OutOfRange)
    ));
    assert!(matches!(
        parse_xs_datetime("nonsense"),
        Err(DateTimeError::Syntax(_))
    ));
}

#[test]
fn ntp_timestamp_maps_prime_epoch() {
    let unix_epoch_in_ntp_seconds: u32 = 2_208_988_800;
    let mut bytes = unix_epoch_in_ntp_seconds.to_be_bytes().to_vec();
    bytes.extend_from_slice(&0x8000_0000u32.to_be_bytes());
    assert_eq!(parse_ntp_timestamp(&bytes).unwrap(), 500_000);
    assert!(matches!(
        parse_ntp_timestamp(&bytes[..7]),
        Err(dash_runtime::timing::datetime::DateTimeError::Ntp)
    ));
}