use std::time::SystemTime;
use super::calendar::{days_from_civil, try_days_in_month};
use super::error::{Field, ParseError};
use super::format::from_unix_parts;
fn check_min_len(bytes: &[u8]) -> Result<(), ParseError> {
if bytes.len() < 19 {
Err(ParseError::TooShort {
len: bytes.len(),
at: bytes.len(),
})
} else {
Ok(())
}
}
fn expect_byte(bytes: &[u8], at: usize, expected: u8) -> Result<(), ParseError> {
match bytes.get(at) {
Some(&actual) if actual == expected => Ok(()),
Some(&actual) => Err(ParseError::Malformed { at, byte: actual }),
None => Err(ParseError::TooShort {
len: bytes.len(),
at,
}),
}
}
fn check_digit(byte: u8, target_field: Field, at: usize) -> Result<u32, ParseError> {
if byte.is_ascii_digit() {
Ok(u32::from(byte.saturating_sub(b'0')))
} else {
Err(ParseError::NonDigit {
field: target_field,
at,
byte,
})
}
}
fn parse_digit_field(
bytes: &[u8],
start: usize,
len: usize,
target_field: Field,
) -> Result<u32, ParseError> {
let mut acc = 0u32;
for offset in 0..len {
let at = start.saturating_add(offset);
let byte = *bytes.get(at).ok_or(ParseError::TooShort {
len: bytes.len(),
at,
})?;
let digit = check_digit(byte, target_field, at)?;
acc = acc.saturating_mul(10).saturating_add(digit);
}
Ok(acc)
}
fn check_range(field: Field, value: u32, min: u32, max: u32, at: usize) -> Result<(), ParseError> {
if value < min || value > max {
Err(ParseError::OutOfRange {
field,
value,
min,
max,
at,
})
} else {
Ok(())
}
}
fn parse_year(bytes: &[u8]) -> Result<i64, ParseError> {
let year_val = parse_digit_field(bytes, 0, 4, Field::Year)?;
expect_byte(bytes, 4, b'-')?;
Ok(i64::from(year_val))
}
fn parse_month(bytes: &[u8]) -> Result<u32, ParseError> {
let month_val = parse_digit_field(bytes, 5, 2, Field::Month)?;
expect_byte(bytes, 7, b'-')?;
check_range(Field::Month, month_val, 1, 12, 5)?;
Ok(month_val)
}
fn parse_day(bytes: &[u8], year: i64, month: u32) -> Result<u32, ParseError> {
let day_val = parse_digit_field(bytes, 8, 2, Field::Day)?;
let month_days = try_days_in_month(year, month).ok_or(ParseError::OutOfRange {
field: Field::Month,
value: month,
min: 1,
max: 12,
at: 5,
})?;
check_range(Field::Day, day_val, 1, month_days, 8)?;
Ok(day_val)
}
fn parse_date(bytes: &[u8]) -> Result<(i64, u32, u32), ParseError> {
let year = parse_year(bytes)?;
let month = parse_month(bytes)?;
let day = parse_day(bytes, year, month)?;
Ok((year, month, day))
}
fn check_time_separator(bytes: &[u8]) -> Result<(), ParseError> {
match bytes.get(10).copied() {
Some(b'T' | b't' | b' ') => Ok(()),
Some(byte) => Err(ParseError::Malformed { at: 10, byte }),
None => Err(ParseError::TooShort {
len: bytes.len(),
at: 10,
}),
}
}
fn parse_hour(bytes: &[u8]) -> Result<u32, ParseError> {
let hour_val = parse_digit_field(bytes, 11, 2, Field::Hour)?;
expect_byte(bytes, 13, b':')?;
check_range(Field::Hour, hour_val, 0, 23, 11)?;
Ok(hour_val)
}
fn parse_minute(bytes: &[u8]) -> Result<u32, ParseError> {
let min_val = parse_digit_field(bytes, 14, 2, Field::Minute)?;
expect_byte(bytes, 16, b':')?;
check_range(Field::Minute, min_val, 0, 59, 14)?;
Ok(min_val)
}
fn parse_second(bytes: &[u8]) -> Result<u32, ParseError> {
let sec_val = parse_digit_field(bytes, 17, 2, Field::Second)?;
if sec_val == 60 {
let refusal = Err(ParseError::UnsupportedLeapSecond { at: 17 });
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "parse_second: returning an error to the caller");
return refusal;
}
check_range(Field::Second, sec_val, 0, 59, 17)?;
Ok(sec_val)
}
fn parse_time(bytes: &[u8]) -> Result<(u32, u32, u32), ParseError> {
check_time_separator(bytes)?;
let hour = parse_hour(bytes)?;
let minute = parse_minute(bytes)?;
let second = parse_second(bytes)?;
Ok((hour, minute, second))
}
fn compute_fraction(bytes: &[u8], start: usize, digits: usize) -> Result<u32, ParseError> {
let mut scaled = 0u32;
for digit_idx in 0..9 {
let digit = if digit_idx < digits {
let at = start.saturating_add(digit_idx);
let byte = *bytes.get(at).ok_or(ParseError::TooShort {
len: bytes.len(),
at,
})?;
u32::from(byte.saturating_sub(b'0'))
} else {
0
};
scaled = scaled.saturating_mul(10).saturating_add(digit);
}
Ok(scaled)
}
fn parse_fraction_digits(
bytes: &[u8],
cursor: &mut usize,
dot_pos: usize,
) -> Result<u32, ParseError> {
let start = *cursor;
while *cursor < bytes.len() && bytes[*cursor].is_ascii_digit() {
*cursor = cursor.saturating_add(1);
}
let digits = cursor.saturating_sub(start);
if digits == 0 || digits > 9 {
let refusal = Err(ParseError::FractionWidth {
digits,
at: dot_pos,
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "parse_fraction_digits: refusing a fraction that is empty or over-wide");
return refusal;
}
compute_fraction(bytes, start, digits)
}
fn parse_fraction(bytes: &[u8], cursor: &mut usize) -> Result<u32, ParseError> {
if bytes.get(*cursor) != Some(&b'.') {
return Ok(0);
}
let dot_pos = *cursor;
*cursor = cursor.saturating_add(1);
parse_fraction_digits(bytes, cursor, dot_pos)
}
fn parse_numeric_offset(
bytes: &[u8],
cursor: usize,
sign_negative: bool,
) -> Result<i64, ParseError> {
let end = cursor
.checked_add(6)
.ok_or(ParseError::MissingOffset { at: cursor })?;
if end > bytes.len() {
Err(ParseError::MissingOffset { at: cursor })
} else if end < bytes.len() {
let byte = bytes
.get(end)
.copied()
.ok_or(ParseError::MissingOffset { at: cursor })?;
Err(ParseError::Malformed { at: end, byte })
} else {
let magnitude = offset_magnitude(bytes, cursor)?;
if sign_negative {
Ok(magnitude.saturating_neg())
} else {
Ok(magnitude)
}
}
}
fn offset_magnitude(bytes: &[u8], cursor: usize) -> Result<i64, ParseError> {
let offset_hour = parse_digit_field(bytes, cursor.saturating_add(1), 2, Field::OffsetHour)?;
expect_byte(bytes, cursor.saturating_add(3), b':')?;
let offset_minute = parse_digit_field(bytes, cursor.saturating_add(4), 2, Field::OffsetMinute)?;
check_range(
Field::OffsetHour,
offset_hour,
0,
23,
cursor.saturating_add(1),
)?;
check_range(
Field::OffsetMinute,
offset_minute,
0,
59,
cursor.saturating_add(4),
)?;
Ok(i64::from(offset_hour)
.saturating_mul(60)
.saturating_add(i64::from(offset_minute)))
}
fn parse_offset(bytes: &[u8], cursor: usize) -> Result<i64, ParseError> {
match bytes.get(cursor).copied() {
None => Err(ParseError::MissingOffset { at: cursor }),
Some(b'Z' | b'z') if cursor.saturating_add(1) == bytes.len() => Ok(0),
Some(b'Z' | b'z') => {
let at = cursor.saturating_add(1);
match bytes.get(at).copied() {
Some(byte) => Err(ParseError::Malformed { at, byte }),
None => Err(ParseError::MissingOffset { at }),
}
}
Some(b'+') => parse_numeric_offset(bytes, cursor, false),
Some(b'-') => parse_numeric_offset(bytes, cursor, true),
Some(byte) => Err(ParseError::Malformed { at: cursor, byte }),
}
}
fn checked_system_time(seconds: i64, nanoseconds: u32) -> Result<SystemTime, ParseError> {
preserve_system_time_error(from_unix_parts(seconds, nanoseconds))
}
fn preserve_system_time_error(
conversion: Result<SystemTime, super::error::UnixTimeError>,
) -> Result<SystemTime, ParseError> {
conversion.map_err(ParseError::UnrepresentableInstant)
}
pub fn parse_rfc3339(text: &str) -> Result<SystemTime, ParseError> {
let bytes = text.as_bytes();
check_min_len(bytes)?;
let (year, month, day) = parse_date(bytes)?;
let (hour, minute, second) = parse_time(bytes)?;
let mut cursor = 19;
let nanos = parse_fraction(bytes, &mut cursor)?;
let offset_minutes = parse_offset(bytes, cursor)?;
let secs = days_from_civil(year, month, day)
.saturating_mul(86_400)
.saturating_add(i64::from(hour).saturating_mul(3_600))
.saturating_add(i64::from(minute).saturating_mul(60))
.saturating_add(i64::from(second))
.saturating_sub(offset_minutes.saturating_mul(60));
checked_system_time(secs, nanos)
}
#[cfg(test)]
mod tests {
use super::{checked_system_time, preserve_system_time_error};
use crate::time::{ParseError, UnixTimeError};
#[test]
fn conversion_seam_preserves_platform_range_failure() {
assert_eq!(
checked_system_time(i64::MAX, 1_000_000_000),
Err(ParseError::UnrepresentableInstant(
UnixTimeError::SecondsOverflow {
seconds: i64::MAX,
nanoseconds: 1_000_000_000,
}
)),
"parser conversion errors must remain typed instead of becoming epoch success"
);
assert_eq!(
preserve_system_time_error(Err(UnixTimeError::SystemTimeOutOfRange {
seconds: i64::MAX,
nanoseconds: 0,
})),
Err(ParseError::UnrepresentableInstant(
UnixTimeError::SystemTimeOutOfRange {
seconds: i64::MAX,
nanoseconds: 0,
}
)),
"a platform range refusal must remain visible in ParseError"
);
}
}