use crate::validate::{trim_ows, trim_ows_start};
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DateError {
Empty,
InvalidFormat,
InvalidDayName,
InvalidDay,
InvalidMonth,
InvalidYear,
Rfc850Date,
InvalidHour,
InvalidMinute,
InvalidSecond,
NotGmt,
}
impl fmt::Display for DateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DateError::Empty => write!(f, "empty date"),
DateError::InvalidFormat => write!(f, "invalid date format"),
DateError::InvalidDayName => write!(f, "invalid day name"),
DateError::InvalidDay => write!(f, "invalid day"),
DateError::InvalidMonth => write!(f, "invalid month"),
DateError::InvalidYear => write!(f, "invalid year"),
DateError::Rfc850Date => write!(f, "rfc850-date format requires reference year"),
DateError::InvalidHour => write!(f, "invalid hour"),
DateError::InvalidMinute => write!(f, "invalid minute"),
DateError::InvalidSecond => write!(f, "invalid second"),
DateError::NotGmt => write!(f, "timezone is not GMT"),
}
}
}
impl core::error::Error for DateError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DayOfWeek {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}
impl DayOfWeek {
pub fn short_name(&self) -> &'static str {
match self {
DayOfWeek::Sunday => "Sun",
DayOfWeek::Monday => "Mon",
DayOfWeek::Tuesday => "Tue",
DayOfWeek::Wednesday => "Wed",
DayOfWeek::Thursday => "Thu",
DayOfWeek::Friday => "Fri",
DayOfWeek::Saturday => "Sat",
}
}
fn from_name(s: &str) -> Option<Self> {
match s {
"Sun" | "Sunday" => Some(DayOfWeek::Sunday),
"Mon" | "Monday" => Some(DayOfWeek::Monday),
"Tue" | "Tuesday" => Some(DayOfWeek::Tuesday),
"Wed" | "Wednesday" => Some(DayOfWeek::Wednesday),
"Thu" | "Thursday" => Some(DayOfWeek::Thursday),
"Fri" | "Friday" => Some(DayOfWeek::Friday),
"Sat" | "Saturday" => Some(DayOfWeek::Saturday),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpDate {
day_of_week: DayOfWeek,
day: u8,
month: u8,
year: u16,
hour: u8,
minute: u8,
second: u8,
}
impl PartialOrd for HttpDate {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for HttpDate {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
)
.cmp(&(
other.year,
other.month,
other.day,
other.hour,
other.minute,
other.second,
))
}
}
fn max_day_in_month(month: u8, year: u16) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
let leap =
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400));
if leap { 29 } else { 28 }
}
_ => 0,
}
}
impl HttpDate {
pub fn parse(input: &str) -> Result<Self, DateError> {
let input = trim_ows(input);
if input.is_empty() {
return Err(DateError::Empty);
}
if let Some(comma_pos) = input.find(',') {
let day_name = &input[..comma_pos];
let rest = trim_ows_start(&input[comma_pos + 1..]);
if rest.contains('-') {
Err(DateError::Rfc850Date)
} else {
parse_imf_fixdate(day_name, rest)
}
} else {
parse_asctime(input)
}
}
pub fn parse_rfc850(input: &str, reference_year: u16) -> Result<Self, DateError> {
let input = trim_ows(input);
if input.is_empty() {
return Err(DateError::Empty);
}
let comma_pos = input.find(',').ok_or(DateError::InvalidFormat)?;
let day_name = &input[..comma_pos];
let rest = trim_ows_start(&input[comma_pos + 1..]);
if !rest.contains('-') {
return Err(DateError::InvalidFormat);
}
parse_rfc850_inner(day_name, rest, reference_year)
}
pub fn new(
day_of_week: DayOfWeek,
day: u8,
month: u8,
year: u16,
hour: u8,
minute: u8,
second: u8,
) -> Result<Self, DateError> {
if !(1..=12).contains(&month) {
return Err(DateError::InvalidMonth);
}
let max_day = max_day_in_month(month, year);
if day < 1 || day > max_day {
return Err(DateError::InvalidDay);
}
if year < 1 {
return Err(DateError::InvalidYear);
}
if hour > 23 {
return Err(DateError::InvalidHour);
}
if minute > 59 {
return Err(DateError::InvalidMinute);
}
if second > 60 {
return Err(DateError::InvalidSecond);
}
Ok(HttpDate {
day_of_week,
day,
month,
year,
hour,
minute,
second,
})
}
pub fn day_of_week(&self) -> DayOfWeek {
self.day_of_week
}
pub fn day(&self) -> u8 {
self.day
}
pub fn month(&self) -> u8 {
self.month
}
pub fn year(&self) -> u16 {
self.year
}
pub fn hour(&self) -> u8 {
self.hour
}
pub fn minute(&self) -> u8 {
self.minute
}
pub fn second(&self) -> u8 {
self.second
}
}
impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
self.day_of_week.short_name(),
self.day,
month_name(self.month),
self.year,
self.hour,
self.minute,
self.second
)
}
}
fn parse_imf_fixdate(day_name: &str, rest: &str) -> Result<HttpDate, DateError> {
let day_of_week = DayOfWeek::from_name(day_name).ok_or(DateError::InvalidDayName)?;
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() != 5 {
return Err(DateError::InvalidFormat);
}
let day = parts[0].parse::<u8>().map_err(|_| DateError::InvalidDay)?;
let month = parse_month(parts[1])?;
let year = parts[2]
.parse::<u16>()
.map_err(|_| DateError::InvalidYear)?;
let (hour, minute, second) = parse_time(parts[3])?;
if parts[4] != "GMT" {
return Err(DateError::NotGmt);
}
HttpDate::new(day_of_week, day, month, year, hour, minute, second)
}
fn parse_rfc850_inner(
day_name: &str,
rest: &str,
reference_year: u16,
) -> Result<HttpDate, DateError> {
let day_of_week = DayOfWeek::from_name(day_name).ok_or(DateError::InvalidDayName)?;
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() != 3 {
return Err(DateError::InvalidFormat);
}
let date_parts: Vec<&str> = parts[0].split('-').collect();
if date_parts.len() != 3 {
return Err(DateError::InvalidFormat);
}
let day = date_parts[0]
.parse::<u8>()
.map_err(|_| DateError::InvalidDay)?;
let month = parse_month(date_parts[1])?;
let raw_year_str = date_parts[2];
let raw_year = raw_year_str
.parse::<u16>()
.map_err(|_| DateError::InvalidYear)?;
let year = if raw_year_str.len() == 2 {
interpret_two_digit_year(raw_year, reference_year)
} else {
raw_year
};
let (hour, minute, second) = parse_time(parts[1])?;
if parts[2] != "GMT" {
return Err(DateError::NotGmt);
}
HttpDate::new(day_of_week, day, month, year, hour, minute, second)
}
fn parse_asctime(input: &str) -> Result<HttpDate, DateError> {
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() != 5 {
return Err(DateError::InvalidFormat);
}
let day_of_week = DayOfWeek::from_name(parts[0]).ok_or(DateError::InvalidDayName)?;
let month = parse_month(parts[1])?;
let day = parts[2].parse::<u8>().map_err(|_| DateError::InvalidDay)?;
let (hour, minute, second) = parse_time(parts[3])?;
let year = parts[4]
.parse::<u16>()
.map_err(|_| DateError::InvalidYear)?;
HttpDate::new(day_of_week, day, month, year, hour, minute, second)
}
fn parse_month(s: &str) -> Result<u8, DateError> {
match s {
"Jan" => Ok(1),
"Feb" => Ok(2),
"Mar" => Ok(3),
"Apr" => Ok(4),
"May" => Ok(5),
"Jun" => Ok(6),
"Jul" => Ok(7),
"Aug" => Ok(8),
"Sep" => Ok(9),
"Oct" => Ok(10),
"Nov" => Ok(11),
"Dec" => Ok(12),
_ => Err(DateError::InvalidMonth),
}
}
fn month_name(month: u8) -> &'static str {
match month {
1 => "Jan",
2 => "Feb",
3 => "Mar",
4 => "Apr",
5 => "May",
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec",
_ => "???",
}
}
fn interpret_two_digit_year(two_digit: u16, reference_year: u16) -> u16 {
let current_century = (reference_year / 100) * 100;
let candidate = current_century + two_digit;
if candidate > reference_year + 50 {
candidate - 100
} else {
candidate
}
}
fn parse_time(s: &str) -> Result<(u8, u8, u8), DateError> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 3 {
return Err(DateError::InvalidFormat);
}
let hour = parts[0].parse::<u8>().map_err(|_| DateError::InvalidHour)?;
let minute = parts[1]
.parse::<u8>()
.map_err(|_| DateError::InvalidMinute)?;
let second = parts[2]
.parse::<u8>()
.map_err(|_| DateError::InvalidSecond)?;
if hour > 23 {
return Err(DateError::InvalidHour);
}
if minute > 59 {
return Err(DateError::InvalidMinute);
}
if second > 60 {
return Err(DateError::InvalidSecond);
}
Ok((hour, minute, second))
}