use crate::error::{Error, ErrorCode, Result};
use crate::temporal::Instant;
use core::fmt;
use structured_asn1::{Element, Reader, Tag, universal};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Relation {
Before,
At,
After,
Uncertain(crate::temporal::Uncertainty),
}
pub(crate) fn relation_in<W: structured_asn1::memory::Work>(
label: Instant,
now: crate::temporal::Reading,
utc: &crate::temporal::LeapTable<'_>,
execution: &W,
origin: (&'static str, u64),
) -> Result<Relation> {
let (field, at) = origin;
let searches = u64::from(usize::BITS - utc.transitions().len().leading_zeros() + 1);
execution
.charge_work(searches * 6 + 64)
.map_err(|reason| Error::resource(reason, field, at))?;
for compared in [label, now.start()] {
if let Err(reason) = utc.shift(compared, 0) {
return Ok(Relation::Uncertain(reason));
}
}
Ok(
if label < now.start() || (label == now.start() && now.has_subnanosecond_remainder()) {
Relation::Before
} else if label == now.start() {
Relation::At
} else {
Relation::After
},
)
}
pub(crate) fn read_in(
parent: &Reader<'_>,
element: &Element<'_>,
field: &'static str,
) -> Result<Time> {
let work = u64::try_from(element.contents().len())
.ok()
.and_then(|length| length.checked_mul(4))
.and_then(|work| work.checked_add(64))
.ok_or_else(|| {
Error::resource(
structured_asn1::memory::MemoryError::WorkExceeded,
field,
element.span().start,
)
})?;
parent.charge(work).map_err(|error| {
Error::new(
ErrorCode::Encoding(error.code()),
field,
element.span().start,
)
})?;
read(element, field)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Form {
Utc,
Generalized,
}
#[derive(Debug, Clone, Copy)]
pub struct Time {
instant: Instant,
form: Form,
}
pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
impl Time {
#[must_use]
pub fn seconds(self) -> i64 {
self.instant.seconds()
}
#[must_use]
pub fn instant(self) -> Instant {
self.instant
}
#[must_use]
pub fn is_leap_second(self) -> bool {
self.instant.is_leap_second()
}
#[must_use]
pub fn form(self) -> Form {
self.form
}
#[must_use]
pub fn is_no_well_defined_expiry(self) -> bool {
self.seconds() == NO_WELL_DEFINED_EXPIRY && !self.is_leap_second()
}
}
impl PartialEq for Time {
fn eq(&self, other: &Self) -> bool {
self.instant == other.instant
}
}
impl Eq for Time {}
impl PartialOrd for Time {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Time {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.instant.cmp(&other.instant)
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (year, month, day, hour, minute, ordinary) = civil_from_seconds(self.seconds());
let second = if self.is_leap_second() { 60 } else { ordinary };
write!(
f,
"{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
)
}
}
pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
read_as(element, field, Fraction::Absent)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Fraction {
Absent,
Written,
}
pub(crate) fn read_as(
element: &Element<'_>,
field: &'static str,
fraction: Fraction,
) -> Result<Time> {
let fail = |code| Error::new(code, field, element.span().start);
let form = if element.tag() == Tag::primitive(universal::UTC_TIME) {
Form::Utc
} else if element.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
Form::Generalized
} else {
return Err(fail(ErrorCode::Encoding(
structured_asn1::ErrorCode::UnexpectedTag,
)));
};
let contents = element.contents();
let (year, rest) = match form {
Form::Utc => {
let (digits, rest) =
split(contents, 2).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
let two = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
(if two >= 50 { 1900 + two } else { 2000 + two }, rest)
}
Form::Generalized => {
let (digits, rest) =
split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
let four = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
(four, rest)
}
};
let ([month, day, hour, minute, second], tail) =
fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
let closed = match tail.split_first() {
Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
matches!(digits.split_last(), Some((b'Z', fraction)) if !fraction.is_empty()
&& fraction.iter().all(u8::is_ascii_digit)
&& fraction.last() != Some(&b'0'))
}
_ => tail == b"Z",
};
if !closed {
return Err(fail(ErrorCode::MalformedTime));
}
if form == Form::Generalized {
let instant = crate::temporal::generalized(element, field)?.start();
return Ok(Time { instant, form });
}
if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
return Err(fail(ErrorCode::TimeOutOfRange));
}
if hour > 23 || minute > 59 || second > 59 {
return Err(fail(ErrorCode::TimeOutOfRange));
}
let days = days_from_civil(i64::from(year), month, day);
let seconds =
days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
let instant = Instant::new(seconds, 0).ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
Ok(Time { instant, form })
}
pub(crate) fn ber_fits(contents: &[u8], generalized: bool) -> bool {
let year_width = if generalized { 4 } else { 2 };
let Some((year, rest)) = split(contents, year_width) else {
return false;
};
let Some(year) = number(year) else {
return false;
};
let (clock, zone) = match rest
.iter()
.position(|byte| matches!(byte, b'Z' | b'+' | b'-'))
{
Some(at) => rest.split_at(at),
None => (rest, &[][..]),
};
if !ber_zone(zone, generalized) {
return false;
}
let digits = match clock.iter().position(|byte| matches!(byte, b'.' | b',')) {
Some(at) if generalized => {
let (digits, fraction) = clock.split_at(at);
if fraction.len() < 2 || !fraction[1..].iter().all(u8::is_ascii_digit) {
return false;
}
digits
}
Some(_) => return false,
None => clock,
};
let Some(([month, day, hour], tail)) = fields_of(digits) else {
return false;
};
if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) || hour > 23 {
return false;
}
if tail.is_empty() {
return generalized;
}
let Some(([minute], tail)) = fields_of(tail) else {
return false;
};
if minute > 59 {
return false;
}
if tail.is_empty() {
return true;
}
let Some(([second], [])) = fields_of(tail) else {
return false;
};
second <= if generalized { 60 } else { 59 }
}
fn ber_zone(zone: &[u8], generalized: bool) -> bool {
if zone.is_empty() {
return generalized;
}
if zone == b"Z" {
return true;
}
let Some((b'+' | b'-', digits)) = zone.split_first() else {
return false;
};
let Some(([hour], tail)) = fields_of(digits) else {
return false;
};
if hour > 23 {
return false;
}
if tail.is_empty() {
return generalized;
}
matches!(fields_of(tail), Some(([minute], [])) if minute <= 59)
}
pub(super) fn split(contents: &[u8], count: usize) -> Option<(&[u8], &[u8])> {
let (digits, rest) = contents.split_at_checked(count)?;
digits
.iter()
.all(u8::is_ascii_digit)
.then_some((digits, rest))
}
pub(super) fn number(digits: &[u8]) -> Option<u32> {
digits.iter().try_fold(0u32, |value, digit| {
value
.checked_mul(10)
.and_then(|value| value.checked_add(u32::from(digit - b'0')))
})
}
fn in_cycle(digits: &str, negative: bool) -> u32 {
let counted = digits.as_bytes().iter().fold(0u32, |cycle, digit| {
(cycle * 10 + u32::from(digit - b'0')) % 400
});
if negative {
(400 - counted) % 400
} else {
counted
}
}
fn weeks_in_year(year: u32) -> u32 {
let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
let long = opens(year) == 4 || year.checked_sub(1).is_some_and(|before| opens(before) == 3);
if long { 53 } else { 52 }
}
fn is_leap(year: u32) -> bool {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
}
pub(super) fn days_in_month(year: u32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap(year) => 29,
2 => 28,
_ => 0,
}
}
pub(super) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let month = i64::from(month);
let year = if month <= 2 { year - 1 } else { year };
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = year - era * 400;
let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
pub(super) fn civil_from_seconds(seconds: i64) -> (i64, i64, i64, i64, i64, i64) {
let days = seconds.div_euclid(86_400);
let within = seconds.rem_euclid(86_400);
let shifted = days + 719_468;
let era = if shifted >= 0 {
shifted
} else {
shifted - 146_096
} / 146_097;
let day_of_era = shifted - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let shifted_month = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
let month = if shifted_month < 10 {
shifted_month + 3
} else {
shifted_month - 9
};
(
if month <= 2 { year + 1 } else { year },
month,
day,
within / 3_600,
within / 60 % 60,
within % 60,
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Defined {
Date,
TimeOfDay,
DateTime,
Duration,
Any,
}
pub(crate) fn defined_fits(kind: Defined, contents: &[u8]) -> bool {
match kind {
Defined::Date => date_fits(contents),
Defined::TimeOfDay => time_of_day_fits(contents),
Defined::DateTime => contents
.split_at_checked(8)
.is_some_and(|(date, time)| date_fits(date) && time_of_day_fits(time)),
Defined::Duration => duration_fits(contents),
Defined::Any => core::str::from_utf8(contents).is_ok_and(any_fits),
}
}
fn any_fits(text: &str) -> bool {
let interval = match text.strip_prefix('R') {
Some(after) => {
let count = after.bytes().take_while(u8::is_ascii_digit).count();
let (repeats, tail) = after.split_at(count);
if repeats.starts_with('0') && repeats.len() > 1 {
return false;
}
let Some(rest) = tail.strip_prefix('/') else {
return false;
};
rest
}
None => text,
};
match interval.split_once('/') {
Some((start, end)) => match (part_fits(start), part_fits(end)) {
(Some(starts), Some(ends)) => !(starts == Part::Duration && ends == Part::Duration),
_ => false,
},
None => part_fits(interval).is_some(),
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Part {
Point,
Duration,
}
fn part_fits(text: &str) -> Option<Part> {
if let Some(duration) = text.strip_prefix('P') {
return duration_fits(duration.as_bytes()).then_some(Part::Duration);
}
point_fits(text).then_some(Part::Point)
}
fn point_fits(text: &str) -> bool {
match text.split_once('T') {
Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
None => date_part_fits(text) || zoned_time_fits(text),
}
}
fn zoned_time_fits(text: &str) -> bool {
let stated = match text.strip_suffix('Z') {
Some(rest) => rest,
None => match text.rfind(['+', '-']) {
Some(at) if difference_fits(&text[at..]) => &text[..at],
_ => text,
},
};
time_part_fits(stated)
}
fn difference_fits(text: &str) -> bool {
let Some(rest) = text.strip_prefix(['+', '-']) else {
return false;
};
match rest.split_once(':') {
Some((hours, minutes)) => {
two_digits(hours).is_some_and(|hours| hours <= 23)
&& two_digits(minutes).is_some_and(|minutes| minutes <= 59)
}
None => two_digits(rest).is_some_and(|hours| hours <= 23),
}
}
fn date_part_fits(text: &str) -> bool {
let signed = text.starts_with(['+', '-']);
let rest = if signed { &text[1..] } else { text };
let mut parts = rest.split('-');
let Some(year) = parts.next() else {
return false;
};
if year.len() < 4 || !year.bytes().all(|byte| byte.is_ascii_digit()) {
return false;
}
let Some(next) = parts.next() else {
return parts.next().is_none();
};
let counted = in_cycle(year, signed);
if let Some(week) = next.strip_prefix('W') {
let last = weeks_in_year(counted);
let weeks = two_digits(week).is_some_and(|week| (1..=last).contains(&week));
return weeks
&& match parts.next() {
Some(day) => {
day.len() == 1
&& day.bytes().all(|byte| (b'1'..=b'7').contains(&byte))
&& parts.next().is_none()
}
None => true,
};
}
if next.len() == 3 {
let last = if is_leap(counted) { 366 } else { 365 };
return next.bytes().all(|byte| byte.is_ascii_digit())
&& number(next.as_bytes()).is_some_and(|day| (1..=last).contains(&day))
&& parts.next().is_none();
}
let Some(month) = two_digits(next).filter(|month| (1..=12).contains(month)) else {
return false;
};
match parts.next() {
Some(day) => {
let Some(day) = two_digits(day) else {
return false;
};
let last = days_in_month(counted, month);
day >= 1 && day <= last && parts.next().is_none()
}
None => true,
}
}
fn time_part_fits(text: &str) -> bool {
let mut parts = text.split(':');
let bounds = [23u32, 59, 60];
let mut carried = None;
let mut counted = 0usize;
let mut ending = false;
for (piece, most) in parts.by_ref().zip(bounds) {
let (digits, fraction) = match piece.split_once(['.', ',']) {
Some((digits, fraction)) => (digits, Some(fraction)),
None => (piece, None),
};
let Some(value) = two_digits(digits) else {
return false;
};
if let Some(fraction) = fraction {
if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
return false;
}
carried = Some(counted);
}
counted += 1;
if carried.is_some_and(|at| at + 1 != counted) {
return false;
}
if ending && fraction.is_some() {
return false;
}
if counted == 1 && value == 24 {
ending = true;
if fraction.is_some() {
return false;
}
continue;
}
if value > most || (ending && value != 0) {
return false;
}
}
counted > 0 && parts.next().is_none()
}
fn two_digits(text: &str) -> Option<u32> {
(text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_digit()))
.then(|| number(text.as_bytes()))?
}
fn date_fits(contents: &[u8]) -> bool {
let Some((year, rest)) = split(contents, 4) else {
return false;
};
let Some(([month, day], [])) = fields_of(rest) else {
return false;
};
let Some(year) = number(year) else {
return false;
};
(1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
}
fn time_of_day_fits(contents: &[u8]) -> bool {
let Some(([hour, minute, second], [])) = fields_of(contents) else {
return false;
};
let midnight = hour == 24 && minute == 0 && second == 0;
(hour <= 23 || midnight) && minute <= 59 && second <= 60
}
fn duration_fits(contents: &[u8]) -> bool {
let Ok(text) = core::str::from_utf8(contents) else {
return false;
};
if text.ends_with('W') {
return components_fit(text, b"W").is_some_and(|weeks| weeks.any);
}
let (date, time) = match text.split_once('T') {
Some((date, time)) => (date, Some(time)),
None => (text, None),
};
if time.is_some_and(str::is_empty) {
return false;
}
let Some(dated) = components_fit(date, b"YMD") else {
return false;
};
if dated.fractional && time.is_some() {
return false;
}
let Some(timed) = time.map_or(Some(Components::NONE), |time| components_fit(time, b"HMS"))
else {
return false;
};
dated.any || timed.any
}
#[derive(Clone, Copy)]
struct Components {
any: bool,
fractional: bool,
}
impl Components {
const NONE: Self = Self {
any: false,
fractional: false,
};
}
fn components_fit(text: &str, order: &[u8]) -> Option<Components> {
let mut rest = text.as_bytes();
let mut left = order;
let mut seen = Components::NONE;
while !rest.is_empty() {
let count = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
if count == 0 || (rest[0] == b'0' && count > 1) {
return None;
}
let after = &rest[count..];
let (after, fractional) = match after.split_first() {
Some((b'.' | b',', tail)) => {
let digits = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
if digits == 0 {
return None;
}
(&tail[digits..], true)
}
_ => (after, false),
};
let (designator, tail) = after.split_first()?;
let at = left.iter().position(|kind| kind == designator)?;
left = &left[at + 1..];
if seen.fractional {
return None;
}
seen = Components {
any: true,
fractional,
};
rest = tail;
}
Some(seen)
}
pub(super) fn fields_of<const N: usize>(contents: &[u8]) -> Option<([u32; N], &[u8])> {
let mut values = [0u32; N];
let mut rest = contents;
for value in &mut values {
let (digits, tail) = split(rest, 2)?;
*value = number(digits)?;
rest = tail;
}
Some((values, rest))
}
#[cfg(test)]
mod tests;