use std::fmt;
#[cfg(feature = "chrono")]
pub mod chrono;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum DayNameTok {
Short(DayName),
Long(DayName),
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DayName {
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
Sun,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum PunctuationTok {
Comma,
Space,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Date {
year: i32,
month: i32,
day: i32,
}
impl Date {
pub fn new(year: i32, month: i32, day: i32) -> Result<Self, DecodeError> {
if !(0..=9999).contains(&year) || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return Err(DecodeError::new("Date out of range"));
}
Ok(Self { year, month, day })
}
#[must_use]
pub fn year(&self) -> i32 {
self.year
}
#[must_use]
pub fn month(&self) -> i32 {
self.month
}
#[must_use]
pub fn day(&self) -> i32 {
self.day
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Time {
hour: i32,
minute: i32,
second: i32,
}
impl Time {
pub fn new(hour: i32, minute: i32, second: i32) -> Result<Self, DecodeError> {
if !(0..=23).contains(&hour) || !(0..=59).contains(&minute) || !(0..=59).contains(&second) {
return Err(DecodeError::new("Time out of range"));
}
Ok(Self {
hour,
minute,
second,
})
}
#[must_use]
pub fn hour(&self) -> i32 {
self.hour
}
#[must_use]
pub fn minute(&self) -> i32 {
self.minute
}
#[must_use]
pub fn second(&self) -> i32 {
self.second
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct DateTime {
pub dayname: DayName,
pub date: Date,
pub time: Time,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct HttpDate {
format: Format,
dt: DateTime,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum Format {
ImfFixdate,
Rfc850,
Asctime,
}
impl HttpDate {
#[must_use]
pub fn imf_fixdate(dt: DateTime) -> Self {
Self {
format: Format::ImfFixdate,
dt,
}
}
pub fn rfc850(dt: DateTime) -> Result<Self, DecodeError> {
if !(0..=99).contains(&dt.date.year) {
return Err(DecodeError::new("RFC 850 year out of range (0-99)"));
}
Ok(Self {
format: Format::Rfc850,
dt,
})
}
#[must_use]
pub fn asctime(dt: DateTime) -> Self {
Self {
format: Format::Asctime,
dt,
}
}
#[must_use]
pub fn datetime(&self) -> &DateTime {
&self.dt
}
#[must_use]
pub fn is_imf_fixdate(&self) -> bool {
self.format == Format::ImfFixdate
}
#[must_use]
pub fn is_rfc850(&self) -> bool {
self.format == Format::Rfc850
}
#[must_use]
pub fn is_asctime(&self) -> bool {
self.format == Format::Asctime
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeError(String);
impl DecodeError {
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DecodeError: {}", self.0)
}
}
impl std::error::Error for DecodeError {}
struct Decoder<'a> {
buf: &'a str,
pos: usize,
}
impl Decoder<'_> {
fn new(buf: &str) -> Decoder<'_> {
Decoder { buf, pos: 0 }
}
#[inline]
fn advance(&mut self, n: usize) {
self.pos += n;
}
#[inline]
fn byte_at(&self, idx: usize) -> Result<u8, DecodeError> {
self.buf
.as_bytes()
.get(idx)
.copied()
.ok_or_else(|| DecodeError::new("Unexpected end of input"))
}
fn expect(&mut self, expected: u8) -> Result<(), DecodeError> {
let actual = self.byte_at(self.pos)?;
if actual == expected {
self.advance(1);
Ok(())
} else {
Err(DecodeError::new(format!(
"Expected byte {}, but found {}",
expected as char, actual as char
)))
}
}
#[inline]
fn space(&mut self) -> Result<(), DecodeError> {
self.expect(b' ')
}
#[inline]
fn comma(&mut self) -> Result<(), DecodeError> {
self.expect(b',')
}
#[inline]
fn colon(&mut self) -> Result<(), DecodeError> {
self.expect(b':')
}
fn month(&mut self) -> Result<i32, DecodeError> {
let m = self
.buf
.get(self.pos..self.pos + 3)
.ok_or_else(|| DecodeError::new("Unexpected end of input"))?;
let n = match m {
"Jan" => 1,
"Feb" => 2,
"Mar" => 3,
"Apr" => 4,
"May" => 5,
"Jun" => 6,
"Jul" => 7,
"Aug" => 8,
"Sep" => 9,
"Oct" => 10,
"Nov" => 11,
"Dec" => 12,
_ => {
return Err(DecodeError::new(format!("Invalid month value: {m}")));
}
};
self.advance(3);
Ok(n)
}
fn digits(&mut self, n: usize) -> Result<i32, DecodeError> {
let buf = self.buf.as_bytes();
let mut value: i32 = 0;
for i in 0..n {
let pos = self.pos + i;
match buf.get(pos) {
Some(&c @ b'0'..=b'9') => {
value = value * 10 + i32::from(c - b'0');
}
Some(&c) => {
return Err(DecodeError::new(format!(
"Expected digit at position {}, but found {}",
pos, c as char
)));
}
None => return Err(DecodeError::new("Unexpected end of input")),
}
}
self.advance(n);
Ok(value)
}
fn year(&mut self) -> Result<i32, DecodeError> {
self.digits(4)
}
fn day(&mut self) -> Result<i32, DecodeError> {
self.digits(2)
}
fn string(&mut self) -> String {
let mut out = String::with_capacity(5);
while self.pos < self.buf.len() {
let c = self.buf.as_bytes()[self.pos];
if !c.is_ascii_alphabetic() {
break;
}
out.push(c as char);
self.advance(1);
}
out
}
fn dayname_tok(&mut self) -> Result<DayNameTok, DecodeError> {
let s = self.string();
let dayname = match s.as_str() {
"Mon" => DayNameTok::Short(DayName::Mon),
"Tue" => DayNameTok::Short(DayName::Tue),
"Wed" => DayNameTok::Short(DayName::Wed),
"Thu" => DayNameTok::Short(DayName::Thu),
"Fri" => DayNameTok::Short(DayName::Fri),
"Sat" => DayNameTok::Short(DayName::Sat),
"Sun" => DayNameTok::Short(DayName::Sun),
"Monday" => DayNameTok::Long(DayName::Mon),
"Tuesday" => DayNameTok::Long(DayName::Tue),
"Wednesday" => DayNameTok::Long(DayName::Wed),
"Thursday" => DayNameTok::Long(DayName::Thu),
"Friday" => DayNameTok::Long(DayName::Fri),
"Saturday" => DayNameTok::Long(DayName::Sat),
"Sunday" => DayNameTok::Long(DayName::Sun),
_ => return Err(DecodeError::new(format!("Invalid day name: {s}"))),
};
Ok(dayname)
}
fn punctuation_tok(&mut self) -> Result<PunctuationTok, DecodeError> {
let c = self.byte_at(self.pos)?;
let tok = match c {
b',' => PunctuationTok::Comma,
b' ' => PunctuationTok::Space,
_ => {
return Err(DecodeError::new(format!(
"Expected punctuation at position {}, but found {}",
self.pos, c as char
)));
}
};
self.advance(1);
Ok(tok)
}
fn date1(&mut self) -> Result<Date, DecodeError> {
let day = self.day()?;
self.space()?;
let month = self.month()?;
self.space()?;
let year = self.year()?;
Date::new(year, month, day)
}
fn time(&mut self) -> Result<Time, DecodeError> {
let hour = self.digits(2)?;
self.colon()?;
let minute = self.digits(2)?;
self.colon()?;
let second = self.digits(2)?;
Time::new(hour, minute, second)
}
fn gmt(&mut self) -> Result<(), DecodeError> {
let s = self.string();
if s != "GMT" {
return Err(DecodeError::new(format!("Expected 'GMT', but found '{s}'")));
}
Ok(())
}
fn imf_fixdate(&mut self, dayname: DayName) -> Result<HttpDate, DecodeError> {
self.space()?;
let date = self.date1()?;
self.space()?;
let time = self.time()?;
self.space()?;
self.gmt()?;
Ok(HttpDate::imf_fixdate(DateTime {
dayname,
date,
time,
}))
}
fn date2(&mut self) -> Result<Date, DecodeError> {
let day = self.day()?;
self.expect(b'-')?;
let month = self.month()?;
self.expect(b'-')?;
let year = self.digits(2)?;
Date::new(year, month, day)
}
fn rfc850_date(&mut self, dayname: DayName) -> Result<HttpDate, DecodeError> {
self.comma()?;
self.space()?;
let date = self.date2()?;
self.space()?;
let time = self.time()?;
self.space()?;
self.gmt()?;
let date = DateTime {
dayname,
date,
time,
};
HttpDate::rfc850(date)
}
fn date3(&mut self) -> Result<(i32, i32), DecodeError> {
let m = self.month()?;
self.space()?;
let d = match self.byte_at(self.pos)? {
b' ' => {
self.space()?;
self.digits(1)?
}
_ => self.digits(2)?,
};
Ok((m, d))
}
fn asctime_date(&mut self, dayname: DayName) -> Result<HttpDate, DecodeError> {
let (month, day) = self.date3()?;
self.space()?;
let time = self.time()?;
self.space()?;
let year = self.year()?;
let date = Date::new(year, month, day)?;
Ok(HttpDate::asctime(DateTime {
dayname,
date,
time,
}))
}
}
pub fn decode(buf: &str) -> Result<HttpDate, DecodeError> {
let mut decoder = Decoder::new(buf);
let date = match decoder.dayname_tok()? {
DayNameTok::Long(dayname) => decoder.rfc850_date(dayname),
DayNameTok::Short(dayname) => match decoder.punctuation_tok()? {
PunctuationTok::Comma => decoder.imf_fixdate(dayname),
PunctuationTok::Space => decoder.asctime_date(dayname),
},
}?;
if decoder.pos != decoder.buf.len() {
return Err(DecodeError::new(format!(
"Trailing data after HTTP date at position {}",
decoder.pos
)));
}
Ok(date)
}
impl DayName {
fn short(self) -> &'static str {
match self {
DayName::Mon => "Mon",
DayName::Tue => "Tue",
DayName::Wed => "Wed",
DayName::Thu => "Thu",
DayName::Fri => "Fri",
DayName::Sat => "Sat",
DayName::Sun => "Sun",
}
}
fn long(self) -> &'static str {
match self {
DayName::Mon => "Monday",
DayName::Tue => "Tuesday",
DayName::Wed => "Wednesday",
DayName::Thu => "Thursday",
DayName::Fri => "Friday",
DayName::Sat => "Saturday",
DayName::Sun => "Sunday",
}
}
}
fn month_name(month: i32) -> &'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",
_ => unreachable!("month {month} is out of range 1..=12"),
}
}
#[must_use]
pub fn encode(date: &HttpDate) -> String {
let dt = date.datetime();
let month = month_name(dt.date.month());
let day = dt.date.day();
let year = dt.date.year();
let time = dt.time;
match date.format {
Format::ImfFixdate => format!(
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
dt.dayname.short(),
day,
month,
year,
time.hour(),
time.minute(),
time.second(),
),
Format::Rfc850 => format!(
"{}, {:02}-{}-{:02} {:02}:{:02}:{:02} GMT",
dt.dayname.long(),
day,
month,
year,
time.hour(),
time.minute(),
time.second(),
),
Format::Asctime => format!(
"{} {} {:2} {:02}:{:02}:{:02} {:04}",
dt.dayname.short(),
month,
day,
time.hour(),
time.minute(),
time.second(),
year,
),
}
}
impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&encode(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expect_matching_byte_advances_position() {
let mut d = Decoder::new("abc");
assert_eq!(d.expect(b'a'), Ok(()));
assert_eq!(d.pos, 1);
}
#[test]
fn expect_mismatched_byte_returns_error() {
let mut d = Decoder::new("abc");
let result = d.expect(b'b');
assert!(result.is_err());
assert_eq!(d.pos, 0); }
#[test]
fn expect_at_end_of_input_returns_error() {
let mut d = Decoder::new("ab");
d.pos = 2;
let err = d.expect(b'a').unwrap_err();
assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
assert_eq!(d.pos, 2);
}
#[test]
fn parsing_punctuated_sequence_advances_through_input() {
let mut d = Decoder::new("Sun, 06");
d.expect(b'S').unwrap();
d.expect(b'u').unwrap();
d.expect(b'n').unwrap();
d.comma().unwrap();
d.space().unwrap();
d.expect(b'0').unwrap();
d.expect(b'6').unwrap();
assert_eq!(d.pos, 7); }
#[test]
fn parsing_date_sequence_with_month_advances_through_input() {
let mut d = Decoder::new("Sun, 06 Nov 1994");
d.expect(b'S').unwrap();
d.expect(b'u').unwrap();
d.expect(b'n').unwrap();
d.comma().unwrap();
d.space().unwrap();
assert_eq!(d.day().unwrap(), 6);
d.space().unwrap();
let month = d.month().unwrap();
assert_eq!(month, 11);
d.space().unwrap();
assert_eq!(d.year().unwrap(), 1994);
assert_eq!(d.pos, 16); }
#[test]
fn string_reads_alphabetic_run_and_advances() {
let mut d = Decoder::new("Sun, 06");
assert_eq!(d.string(), "Sun");
assert_eq!(d.pos, 3); }
#[test]
fn string_reads_to_end_of_input() {
let mut d = Decoder::new("GMT");
assert_eq!(d.string(), "GMT");
assert_eq!(d.pos, 3); }
#[test]
fn string_returns_empty_when_current_byte_is_not_alphabetic() {
let mut d = Decoder::new("123");
assert_eq!(d.string(), "");
assert_eq!(d.pos, 0); }
#[test]
fn string_returns_empty_at_end_of_input() {
let mut d = Decoder::new("Sun,");
d.pos = 4; assert_eq!(d.string(), "");
assert_eq!(d.pos, 4); }
#[test]
fn dayname_tok_parses_all_names() {
let cases = [
("Mon", DayNameTok::Short(DayName::Mon)),
("Tue", DayNameTok::Short(DayName::Tue)),
("Wed", DayNameTok::Short(DayName::Wed)),
("Thu", DayNameTok::Short(DayName::Thu)),
("Fri", DayNameTok::Short(DayName::Fri)),
("Sat", DayNameTok::Short(DayName::Sat)),
("Sun", DayNameTok::Short(DayName::Sun)),
("Monday", DayNameTok::Long(DayName::Mon)),
("Tuesday", DayNameTok::Long(DayName::Tue)),
("Wednesday", DayNameTok::Long(DayName::Wed)),
("Thursday", DayNameTok::Long(DayName::Thu)),
("Friday", DayNameTok::Long(DayName::Fri)),
("Saturday", DayNameTok::Long(DayName::Sat)),
("Sunday", DayNameTok::Long(DayName::Sun)),
];
for (s, expected) in cases {
let mut d = Decoder::new(s);
assert_eq!(d.dayname_tok(), Ok(expected), "day name {s}");
assert_eq!(d.pos, s.len(), "pos after {s}");
}
}
#[test]
fn dayname_tok_rejects_invalid_name() {
let mut d = Decoder::new("Funday");
let result = d.dayname_tok();
assert!(result.is_err());
assert_eq!(d.pos, 6); }
#[test]
fn dayname_tok_is_case_sensitive() {
let mut d = Decoder::new("mon");
let result = d.dayname_tok();
assert!(result.is_err());
assert_eq!(d.pos, 3); }
#[test]
fn dayname_tok_rejects_empty_run() {
let mut d = Decoder::new(", 06 Nov 1994");
let result = d.dayname_tok();
assert!(result.is_err());
assert_eq!(d.pos, 0); }
#[test]
fn dayname_tok_rejects_truncated_long_name() {
let mut d = Decoder::new("Wednes");
let result = d.dayname_tok();
assert!(result.is_err());
assert_eq!(d.pos, 6); }
#[test]
fn punctuation_tok_parses_comma() {
let mut d = Decoder::new(",06 Nov");
assert_eq!(d.punctuation_tok(), Ok(PunctuationTok::Comma));
assert_eq!(d.pos, 1);
}
#[test]
fn punctuation_tok_parses_space() {
let mut d = Decoder::new(" 06 Nov");
assert_eq!(d.punctuation_tok(), Ok(PunctuationTok::Space));
assert_eq!(d.pos, 1);
}
#[test]
fn punctuation_tok_rejects_non_punctuation() {
let mut d = Decoder::new("06 Nov");
let err = d.punctuation_tok().unwrap_err();
assert!(
err.to_string()
.contains("Expected punctuation at position 0, but found 0")
);
assert_eq!(d.pos, 0); }
#[test]
fn punctuation_tok_at_end_of_input_returns_error() {
let mut d = Decoder::new("06");
d.pos = 2; let err = d.punctuation_tok().unwrap_err();
assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
assert_eq!(d.pos, 2);
}
#[test]
fn date1_parses_valid_date() {
let mut d = Decoder::new("06 Nov 1994");
assert_eq!(d.date1(), Date::new(1994, 11, 6));
assert_eq!(d.pos, 11); }
#[test]
fn date1_parses_leading_zero_day() {
let mut d = Decoder::new("01 Jan 2000");
assert_eq!(d.date1(), Date::new(2000, 1, 1));
assert_eq!(d.pos, 11);
}
#[test]
fn date1_rejects_non_digit_day() {
let mut d = Decoder::new("0x Nov 1994");
let err = d.date1().unwrap_err();
assert!(
err.to_string()
.contains("Expected digit at position 1, but found x")
);
}
#[test]
fn date1_rejects_missing_space_after_day() {
let mut d = Decoder::new("06Nov 1994");
let err = d.date1().unwrap_err();
assert!(err.to_string().contains("but found N"));
}
#[test]
fn date1_rejects_invalid_month() {
let mut d = Decoder::new("06 Xyz 1994");
let err = d.date1().unwrap_err();
assert!(err.to_string().contains("Invalid month value: Xyz"));
}
#[test]
fn date1_rejects_truncated_year() {
let mut d = Decoder::new("06 Nov 19");
let err = d.date1().unwrap_err();
assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
}
#[test]
fn time_parses_valid_time() {
let mut d = Decoder::new("08:49:37 GMT");
assert_eq!(d.time(), Time::new(8, 49, 37));
assert_eq!(d.pos, 8); }
#[test]
fn time_rejects_missing_colon() {
let mut d = Decoder::new("0849:37");
let err = d.time().unwrap_err();
assert!(err.to_string().contains("but found 4"));
}
#[test]
fn time_rejects_non_digit_minute() {
let mut d = Decoder::new("08:x9:37");
let err = d.time().unwrap_err();
assert!(
err.to_string()
.contains("Expected digit at position 3, but found x")
);
}
#[test]
fn time_rejects_truncated_seconds() {
let mut d = Decoder::new("08:49:3");
let err = d.time().unwrap_err();
assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
}
fn expect_imf_fixdate(decoded: &HttpDate) -> DateTime {
assert!(decoded.is_imf_fixdate(), "expected ImfFixdate");
*decoded.datetime()
}
fn expect_rfc850(decoded: &HttpDate) -> DateTime {
assert!(decoded.is_rfc850(), "expected Rfc850");
*decoded.datetime()
}
fn expect_asctime(decoded: &HttpDate) -> DateTime {
assert!(decoded.is_asctime(), "expected Asctime");
*decoded.datetime()
}
#[test]
fn decode_parses_imf_fixdate() {
let dt = expect_imf_fixdate(&decode("Sun, 06 Nov 1994 08:49:37 GMT").unwrap());
assert_eq!(dt.dayname, DayName::Sun);
assert_eq!(dt.date, Date::new(1994, 11, 6).unwrap());
assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
}
#[test]
fn decode_parses_rfc850_date() {
let dt = expect_rfc850(&decode("Sunday, 06-Nov-94 08:49:37 GMT").unwrap());
assert_eq!(dt.dayname, DayName::Sun);
assert_eq!(dt.date, Date::new(94, 11, 6).unwrap());
assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
}
#[test]
fn decode_parses_asctime() {
let dt = expect_asctime(&decode("Sun Nov 6 08:49:37 1994").unwrap());
assert_eq!(dt.dayname, DayName::Sun);
assert_eq!(dt.date, Date::new(1994, 11, 6).unwrap());
assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
}
#[test]
fn decode_parses_all_imf_fixdate_weekdays() {
let cases = [
("Mon", DayName::Mon),
("Tue", DayName::Tue),
("Wed", DayName::Wed),
("Thu", DayName::Thu),
("Fri", DayName::Fri),
("Sat", DayName::Sat),
("Sun", DayName::Sun),
];
for (name, dayname) in cases {
let input = format!("{name}, 06 Nov 1994 08:49:37 GMT");
let dt = expect_imf_fixdate(&decode(&input).unwrap());
assert_eq!(dt.dayname, dayname, "weekday {name}");
}
}
#[test]
fn decode_rejects_invalid_dayname() {
let err = decode("Funday, 06 Nov 1994 08:49:37 GMT").expect_err("expected decode to fail");
assert!(err.to_string().contains("Invalid day name: Funday"));
}
#[test]
fn decode_rejects_missing_punctuation() {
let err = decode("Sun 06 Nov 1994").expect_err("expected decode to fail");
assert!(err.to_string().contains("Invalid month value: 06"));
}
#[test]
fn decode_rejects_imf_fixdate_missing_gmt() {
let err = decode("Sun, 06 Nov 1994 08:49:37 ").expect_err("expected decode to fail");
assert!(err.to_string().contains("Expected 'GMT', but found ''"));
}
#[test]
fn decode_rejects_truncated_input() {
let err = decode("Sun, 06 Nov").expect_err("expected decode to fail");
assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
}
#[test]
fn decode_rejects_empty_input() {
let err = decode("").expect_err("expected decode to fail");
assert_eq!(err.to_string(), "DecodeError: Invalid day name: ");
}
#[test]
fn decode_rejects_trailing_data() {
for input in [
"Sun, 06 Nov 1994 08:49:37 GMT ",
"Sun, 06 Nov 1994 08:49:37 GMT 123",
"Sun, 06 Nov 1994 08:49:37 GMT.",
"Sunday, 06-Nov-94 08:49:37 GMT extra",
"Sun Nov 6 08:49:37 1994!",
] {
let err = decode(input)
.err()
.unwrap_or_else(|| panic!("expected {input:?} to be rejected"));
assert!(
err.to_string().contains("Trailing data"),
"{input:?} failed for a different reason: {err}"
);
}
}
#[test]
fn decode_rejects_out_of_range_components() {
for input in [
"Sun, 00 Nov 1994 08:49:37 GMT",
"Sun, 32 Nov 1994 08:49:37 GMT",
"Sunday, 32-Nov-94 08:49:37 GMT",
"Sun Nov 32 08:49:37 1994",
"Sun Nov 0 08:49:37 1994", "Sun, 06 Nov 1994 24:49:37 GMT",
"Sun, 06 Nov 1994 08:60:37 GMT",
"Sun, 06 Nov 1994 08:49:60 GMT",
] {
let err = decode(input)
.err()
.unwrap_or_else(|| panic!("expected {input:?} to be rejected"));
assert!(
err.to_string().contains("out of range"),
"{input:?} failed for a different reason: {err}"
);
}
}
#[test]
fn decode_accepts_boundary_components() {
assert!(decode("Sun, 31 Nov 1994 08:49:37 GMT").is_ok());
assert!(decode("Sun, 06 Nov 1994 23:59:59 GMT").is_ok());
}
fn sun_nov_6(year: i32) -> DateTime {
DateTime {
dayname: DayName::Sun,
date: Date::new(year, 11, 6).unwrap(),
time: Time::new(8, 49, 37).unwrap(),
}
}
#[test]
fn date_new_rejects_out_of_range() {
assert!(Date::new(1994, 11, 6).is_ok());
assert!(Date::new(-1, 11, 6).is_err());
assert!(Date::new(10_000, 11, 6).is_err());
assert!(Date::new(1994, 0, 6).is_err());
assert!(Date::new(1994, 13, 6).is_err());
assert!(Date::new(1994, 11, 0).is_err());
assert!(Date::new(1994, 11, 32).is_err());
}
#[test]
fn time_new_rejects_out_of_range() {
assert!(Time::new(8, 49, 37).is_ok());
assert!(Time::new(24, 0, 0).is_err());
assert!(Time::new(0, 60, 0).is_err());
assert!(Time::new(0, 0, 60).is_err());
assert!(Time::new(-1, 0, 0).is_err());
}
#[test]
fn rfc850_constructor_rejects_year_above_99() {
assert!(HttpDate::rfc850(sun_nov_6(99)).is_ok());
assert!(HttpDate::rfc850(sun_nov_6(100)).is_err());
assert!(HttpDate::rfc850(sun_nov_6(1994)).is_err());
}
#[test]
fn encode_round_trips_constructed_values() {
let cases = [
HttpDate::imf_fixdate(sun_nov_6(1994)),
HttpDate::rfc850(sun_nov_6(94)).unwrap(),
HttpDate::asctime(sun_nov_6(1994)),
];
for date in cases {
let s = encode(&date);
assert_eq!(decode(&s).unwrap(), date, "encode produced {s:?}");
}
}
}