use super::{Form, NO_WELL_DEFINED_EXPIRY, Time, read};
use crate::error::ErrorCode;
use crate::fixtures::{generalized_time, printable, utc_time};
use alloc::format;
use structured_asn1::Parser;
fn time_of(der: &[u8]) -> crate::error::Result<Time> {
let parser = Parser::der(crate::fixtures::memory());
let mut reader = parser.read(der, 0).unwrap();
let element = reader.next_element().unwrap().unwrap();
read(&element, "test")
}
#[test]
fn calendar_projection_reuses_gregorian_core_and_preserves_leap_labels() {
let work = crate::fixtures::memory();
for (encoded, offset, expected) in [
("20260101000000Z", -1440, (2025, 12, 31, 0, 0, 0)),
("99991231235959Z", 1, (10000, 1, 1, 0, 0, 59)),
("00000101000000Z", -1, (-1, 12, 31, 23, 59, 0)),
("20161231235960Z", 150, (2017, 1, 1, 2, 29, 60)),
] {
let time = time_of(&generalized_time(encoded)).unwrap();
let projected = time.calendar_at_minutes(offset, &work).unwrap();
assert_eq!(
(
projected.year,
projected.month,
projected.day,
projected.hour,
projected.minute,
projected.second
),
expected
);
assert_eq!(projected.nanos, 0);
}
let fractional = Time {
instant: crate::temporal::Instant::new(1_767_225_600, 123_456_789).unwrap(),
form: Form::Generalized,
};
assert_eq!(
fractional.calendar_at_minutes(0, &work).unwrap().nanos,
123_456_789
);
}
#[test]
fn calendar_projection_admits_fixed_work_before_conversion() {
use alloc::sync::Arc;
use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
let time = time_of(&generalized_time("20260101000000Z")).unwrap();
let denied = Memory::new(Arc::new(MemoryAccount::new(0, 63)));
assert_eq!(
time.calendar_at_minutes(0, &denied),
Err(MemoryError::WorkExceeded)
);
assert_eq!(denied.work_used(), 0);
let exact = Memory::new(Arc::new(MemoryAccount::new(0, 64)));
assert_eq!(time.calendar_at_minutes(0, &exact).unwrap().year, 2026);
assert_eq!(exact.work_used(), 64);
assert_eq!(exact.used(), 0);
}
#[test]
fn xsd10_normalization_has_its_own_calendar_value_contract() {
use super::Xsd10Label;
let work = crate::fixtures::memory();
let label = Xsd10Label {
year: -1,
month: 3,
day: 1,
hour: 0,
minute: 30,
second: 0,
nanos: 0,
};
let utc = label.at_utc(60, &work).unwrap().unwrap();
assert_eq!(
(utc.year, utc.month, utc.day, utc.hour, utc.minute),
(-1, 2, 28, 23, 30)
);
let leap = time_of(&generalized_time("20161231235960Z")).unwrap();
let utc = leap
.xsd10_label(&work)
.unwrap()
.at_utc(0, &work)
.unwrap()
.unwrap();
assert_eq!(
(
utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second
),
(2017, 1, 1, 0, 0, 0)
);
assert!(leap.is_leap_second());
for (year, month, day, expected) in [(-1, 2, 29, false), (-4, 2, 29, true), (0, 1, 1, false)] {
assert_eq!(
Xsd10Label {
year,
month,
day,
..label
}
.at_utc(0, &work)
.unwrap()
.is_some(),
expected
);
}
let end = Xsd10Label {
year: -1,
month: 12,
day: 31,
hour: 24,
minute: 0,
..label
};
assert_eq!(end.at_utc(0, &work).unwrap().unwrap().year, 1);
}
#[test]
fn xsd10_normalization_refusal_precedes_any_conversion() {
use alloc::sync::Arc;
use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
let label = super::Xsd10Label {
year: 2026,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
nanos: 0,
};
let denied = Memory::new(Arc::new(MemoryAccount::new(0, 127)));
assert_eq!(label.at_utc(0, &denied), Err(MemoryError::WorkExceeded));
assert_eq!(denied.work_used(), 0);
let exact = Memory::new(Arc::new(MemoryAccount::new(0, 128)));
assert_eq!(label.at_utc(0, &exact).unwrap(), Some(label));
assert_eq!(exact.work_used(), 128);
assert_eq!(exact.used(), 0);
}
#[test]
fn both_forms_name_the_same_moment() {
let two_digit = time_of(&utc_time("240701091500Z")).unwrap();
let four_digit = time_of(&generalized_time("20240701091500Z")).unwrap();
assert_eq!(two_digit, four_digit);
assert_eq!(two_digit.seconds(), 1_719_825_300);
assert_eq!(format!("{two_digit}"), "2024-07-01T09:15:00Z");
assert_eq!(two_digit.form(), Form::Utc);
assert_eq!(four_digit.form(), Form::Generalized);
}
#[test]
fn a_two_digit_year_lands_in_the_window_the_profile_fixes() {
let older = time_of(&utc_time("980115000000Z")).unwrap();
assert_eq!(format!("{older}"), "1998-01-15T00:00:00Z");
let newer = time_of(&utc_time("490115000000Z")).unwrap();
assert_eq!(format!("{newer}"), "2049-01-15T00:00:00Z");
let pivot = time_of(&utc_time("500101000000Z")).unwrap();
assert_eq!(format!("{pivot}"), "1950-01-01T00:00:00Z");
}
#[test]
fn moments_before_the_epoch_are_read_and_rendered() {
let old = time_of(&utc_time("960201123000Z")).unwrap();
assert_eq!(old.seconds(), 823_177_800);
let older = time_of(&generalized_time("19510302000000Z")).unwrap();
assert!(older.seconds() < 0);
assert_eq!(format!("{older}"), "1951-03-02T00:00:00Z");
}
#[test]
fn the_leap_day_exists_only_in_a_leap_year() {
assert!(time_of(&generalized_time("20240229120000Z")).is_ok());
assert!(time_of(&generalized_time("20000229120000Z")).is_ok());
for absent in ["20230229120000Z", "19000229120000Z"] {
let error = time_of(&generalized_time(absent)).unwrap_err();
assert_eq!(error.code(), ErrorCode::TimeOutOfRange, "{absent}");
}
}
#[test]
fn a_moment_that_does_not_exist_is_refused() {
for absent in [
"20240001000000Z", "20241301000000Z", "20240100000000Z", "20240631000000Z", "20240101240000Z", "20240101006000Z", "20161231235860Z", "20161231235961Z", ] {
let error = time_of(&generalized_time(absent)).unwrap_err();
assert_eq!(error.code(), ErrorCode::TimeOutOfRange, "{absent}");
}
}
#[test]
fn generalized_leap_label_is_distinct_and_der_conforming() {
let before = time_of(&generalized_time("20161231235959Z")).unwrap();
let leap = time_of(&generalized_time("20161231235960Z")).unwrap();
let after = time_of(&generalized_time("20170101000000Z")).unwrap();
assert!(before < leap && leap < after);
assert_eq!(format!("{leap}"), "2016-12-31T23:59:60Z");
for text in ["20161231235960Z", "20161231235960.1234567891Z"] {
let bytes = generalized_time(text);
let parser = Parser::der(crate::fixtures::memory());
let mut reader = parser.read(&bytes, 37).unwrap();
let element = reader.next_element().unwrap().unwrap();
crate::der::conforms(&reader, &element, "test").unwrap();
assert!(
crate::temporal::read_generalized(&reader, &element, "test")
.unwrap()
.start()
.is_leap_second()
);
}
}
#[test]
fn utc_time_does_not_inherit_generalized_leap_grammar() {
let error = time_of(&utc_time("161231235960Z")).unwrap_err();
assert_eq!(error.code(), ErrorCode::TimeOutOfRange);
}
#[test]
fn only_the_fixed_width_form_with_seconds_and_zulu_is_read() {
for other in [
"2024070109150Z", "202407010915000Z", "20240701091500", "20240701091500+0200", "20240701091500.5Z", "202407010915Z", "2024070109150aZ", ] {
let error = time_of(&generalized_time(other)).unwrap_err();
assert_eq!(error.code(), ErrorCode::MalformedTime, "{other}");
}
for other in ["2407010915Z", "240701091500+0200", "2407010915000Z"] {
let error = time_of(&utc_time(other)).unwrap_err();
assert_eq!(error.code(), ErrorCode::MalformedTime, "{other}");
}
}
#[test]
fn an_element_that_is_not_a_time_is_refused() {
let error = time_of(&printable("20240701091500Z")).unwrap_err();
assert_eq!(
error.code(),
ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag)
);
}
#[test]
fn the_moment_the_profile_uses_for_no_expiry_is_reported_as_itself() {
let forever = time_of(&generalized_time("99991231235959Z")).unwrap();
assert_eq!(forever.seconds(), NO_WELL_DEFINED_EXPIRY);
assert!(forever.is_no_well_defined_expiry());
assert_eq!(format!("{forever}"), "9999-12-31T23:59:59Z");
let ordinary = time_of(&generalized_time("99991231235958Z")).unwrap();
assert!(!ordinary.is_no_well_defined_expiry());
}
#[test]
fn a_year_is_written_in_fifty_two_weeks_or_fifty_three() {
for year in [2004, 2009, 2015, 2020, 2026, 2032] {
assert_eq!(super::weeks_in_year(year), 53, "{year}");
}
for year in [2019, 2021, 2022, 2023, 2024, 2025] {
assert_eq!(super::weeks_in_year(year), 52, "{year}");
}
}