use super::{Instant, Reading};
use crate::error::{Error, ErrorCode, Result};
use crate::time::{days_from_civil, days_in_month, fields_of, number, split};
use structured_asn1::memory::MemoryError;
use structured_asn1::{Element, Reader, Tag, universal};
pub fn read_generalized(
parent: &Reader<'_>,
element: &Element<'_>,
field: &'static str,
) -> Result<Reading> {
if element.tag() == Tag::constructed(universal::GENERALIZED_TIME) {
let mut contents = parent.memory().vector();
crate::name::segments::walk(parent, element, field, false, Some(&mut contents))?;
admit_label(parent, contents.len(), element.span().start, field)?;
return generalized_label(&contents, element.span().start, field);
}
if element.tag() != Tag::primitive(universal::GENERALIZED_TIME) {
return Err(Error::new(
ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
field,
element.span().start,
));
}
admit_generalized(parent, element, field)?;
generalized(element, field)
}
pub(crate) fn admit_generalized(
parent: &Reader<'_>,
element: &Element<'_>,
field: &'static str,
) -> Result<()> {
admit_label(
parent,
element.contents().len(),
element.span().start,
field,
)
}
fn admit_label(parent: &Reader<'_>, length: usize, offset: u64, field: &'static str) -> Result<()> {
let work = u64::try_from(length)
.ok()
.and_then(|length| length.checked_mul(2))
.and_then(|work| work.checked_add(1))
.ok_or_else(|| Error::resource(MemoryError::WorkExceeded, field, offset))?;
parent
.charge(work)
.map_err(|error| Error::new(ErrorCode::Encoding(error.code()), field, offset))
}
pub(crate) fn generalized(element: &Element<'_>, field: &'static str) -> Result<Reading> {
if element.tag() != Tag::primitive(universal::GENERALIZED_TIME) {
return Err(Error::new(
ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
field,
element.span().start,
));
}
generalized_label(element.contents(), element.span().start, field)
}
fn generalized_label(contents: &[u8], offset: u64, field: &'static str) -> Result<Reading> {
let fail = |code| Error::new(code, field, offset);
let (year, rest) = split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
let year = number(year).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
let ([month, day, hour], mut rest) =
fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
let (mut minute, mut second, mut unit) = (0, 0, 3_600_000_000_000i128);
if let Some((digits, tail)) = split(rest, 2) {
minute = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
(rest, unit) = (tail, 60_000_000_000);
if let Some((digits, tail)) = split(rest, 2) {
second = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
(rest, unit) = (tail, 1_000_000_000);
}
}
let (mut fraction, mut exact) = (0i128, true);
if let Some((b'.' | b',', tail)) = rest.split_first() {
let count = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
if count == 0 {
return Err(fail(ErrorCode::MalformedTime));
}
let (digits, after) = tail.split_at(count);
let mut carry = 0i128;
for digit in digits.iter().rev() {
let product = i128::from(digit - b'0') * unit + carry;
exact &= product % 10 == 0;
carry = product / 10;
}
fraction = carry;
rest = after;
}
let offset = zone(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
if !(1..=12).contains(&month)
|| day < 1
|| day > days_in_month(year, month)
|| hour > 23
|| minute > 59
|| second > 60
{
return Err(fail(ErrorCode::TimeOutOfRange));
}
let local = days_from_civil(i64::from(year), month, day) * 86_400
+ i64::from(hour) * 3_600
+ i64::from(minute) * 60
+ i64::from(second.min(59));
let seconds = local - offset;
let start = if second == 60 {
Instant::leap_after(
seconds,
u32::try_from(fraction).map_err(|_| fail(ErrorCode::TimeOutOfRange))?,
)
} else {
Instant::new(
seconds
+ i64::try_from(fraction / super::NANOS)
.map_err(|_| fail(ErrorCode::TimeOutOfRange))?,
u32::try_from(fraction % super::NANOS).map_err(|_| fail(ErrorCode::TimeOutOfRange))?,
)
}
.ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
Ok(Reading {
start,
subnanosecond: !exact,
})
}
fn zone(rest: &[u8]) -> Option<i64> {
match rest {
[b'Z'] => Some(0),
[sign @ (b'+' | b'-'), zone @ ..] => {
let ([hours, minutes], tail) = match zone.len() {
2 => fields_of::<1>(zone).map(|([hours], tail)| ([hours, 0], tail)),
4 => fields_of::<2>(zone),
_ => None,
}?;
if !tail.is_empty() || hours > 23 || minutes > 59 {
return None;
}
let seconds = i64::from(hours) * 3_600 + i64::from(minutes) * 60;
Some(if *sign == b'+' { seconds } else { -seconds })
}
_ => None,
}
}