Skip to main content

http_date/
lib.rs

1//! Parsing and formatting of HTTP date values as defined in
2//! [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7).
3//!
4//! An HTTP date can appear in one of three formats — IMF-fixdate, RFC 850, or
5//! asctime. Use [`decode`] to parse a value into a [`HttpDate`]:
6//!
7//! ```
8//! use http_date::{decode, HttpDate};
9//!
10//! let date = decode("Sun, 06 Nov 1994 08:49:37 GMT")
11//!     .expect("valid IMF-fixdate");
12//! assert!(date.is_imf_fixdate());
13//! ```
14//!
15//! Values can also be constructed directly from components:
16//!
17//! ```
18//! use http_date::{Date, DateTime, DayName, HttpDate, Time, encode};
19//!
20//! fn datetime(year: u16) -> DateTime {
21//!     DateTime {
22//!         dayname: DayName::Sun,
23//!         date: Date::new(year, 11, 6).expect("valid date"),
24//!         time: Time::new(8, 49, 37).expect("valid time"),
25//!     }
26//! }
27//!
28//! let imf = HttpDate::imf_fixdate(datetime(1994));
29//! assert!(imf.is_imf_fixdate());
30//!
31//! // RFC 850 uses a two-digit year; construction rejects anything larger.
32//! let rfc850 = HttpDate::rfc850(datetime(94)).expect("year is 0-99");
33//! assert!(rfc850.is_rfc850());
34//!
35//! let asctime = HttpDate::asctime(datetime(1994));
36//! assert!(asctime.is_asctime());
37//!
38//! assert_eq!(encode(&imf), "Sun, 06 Nov 1994 08:49:37 GMT");
39//! assert_eq!(encode(&rfc850), "Sunday, 06-Nov-94 08:49:37 GMT");
40//! assert_eq!(encode(&asctime), "Sun Nov  6 08:49:37 1994");
41//! ```
42//!
43//! The decoded components are available on the [`DateTime`] inside the
44//! returned [`HttpDate`].
45
46use std::fmt;
47#[cfg(feature = "chrono")]
48pub mod chrono;
49
50#[derive(Clone, Copy)]
51enum DayNameTok {
52    Short(DayName),
53    Long(DayName),
54}
55
56/// The day of the week as used in HTTP date values.
57#[derive(Debug, PartialEq, Eq, Clone, Copy)]
58pub enum DayName {
59    /// Monday.
60    Mon,
61    /// Tuesday.
62    Tue,
63    /// Wednesday.
64    Wed,
65    /// Thursday.
66    Thu,
67    /// Friday.
68    Fri,
69    /// Saturday.
70    Sat,
71    /// Sunday.
72    Sun,
73}
74
75const DAYS: [DayName; 7] = [
76    DayName::Mon,
77    DayName::Tue,
78    DayName::Wed,
79    DayName::Thu,
80    DayName::Fri,
81    DayName::Sat,
82    DayName::Sun,
83];
84
85/// Three-letter month names, January first.
86const MONTHS: [&str; 12] = [
87    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
88];
89
90#[derive(Clone, Copy)]
91enum PunctuationTok {
92    Comma,
93    Space,
94}
95
96/// The calendar date portion of an HTTP date.
97#[derive(Debug, PartialEq, Eq, Clone, Copy)]
98pub struct Date {
99    /// The year, e.g. `1994`.
100    year: u16,
101    /// The month, 1–12, where 1 is January.
102    month: u8,
103    /// The day of the month, 1–31.
104    day: u8,
105}
106
107impl Date {
108    /// Constructs a date.
109    ///
110    /// # Errors
111    ///
112    /// Returns a [`Error`] if a component is out of range: year 0–9999,
113    /// month 1–12, day 1–31.
114    pub fn new(year: u16, month: u8, day: u8) -> Result<Self, Error> {
115        if year > 9999 || month == 0 || month > 12 || day == 0 || day > 31 {
116            return Err(Error::new("date out of range"));
117        }
118        Ok(Self { year, month, day })
119    }
120
121    #[must_use]
122    pub fn year(&self) -> u16 {
123        self.year
124    }
125
126    #[must_use]
127    pub fn month(&self) -> u8 {
128        self.month
129    }
130
131    #[must_use]
132    pub fn day(&self) -> u8 {
133        self.day
134    }
135}
136
137/// The time-of-day portion of an HTTP date.
138///
139/// Constructed with [`Time::new`], which enforces the valid ranges, so a
140/// `Time` value is always well-formed (hour 0–23, minute/second 0–59).
141#[derive(Debug, PartialEq, Eq, Clone, Copy)]
142pub struct Time {
143    /// The hour, 0–23.
144    hour: u8,
145    /// The minute, 0–59.
146    minute: u8,
147    /// The second, 0–59.
148    second: u8,
149}
150
151impl Time {
152    /// Constructs a time.
153    ///
154    /// # Errors
155    ///
156    /// Returns a [`Error`] if a component is out of range: hour 0–23,
157    /// minute 0–59, second 0–59.
158    pub fn new(hour: u8, minute: u8, second: u8) -> Result<Self, Error> {
159        if hour > 23 || minute > 59 || second > 59 {
160            return Err(Error::new("time out of range"));
161        }
162        Ok(Self {
163            hour,
164            minute,
165            second,
166        })
167    }
168
169    #[must_use]
170    pub fn hour(&self) -> u8 {
171        self.hour
172    }
173
174    #[must_use]
175    pub fn minute(&self) -> u8 {
176        self.minute
177    }
178
179    #[must_use]
180    pub fn second(&self) -> u8 {
181        self.second
182    }
183}
184
185/// A fully decoded HTTP date: weekday, calendar date, and time of day.
186///
187/// This is the value carried by the [`HttpDate`] variants.
188#[derive(Debug, PartialEq, Eq, Clone, Copy)]
189pub struct DateTime {
190    /// The day of the week.
191    pub dayname: DayName,
192    /// The calendar date.
193    pub date: Date,
194    /// The time of day.
195    pub time: Time,
196}
197
198/// An HTTP date in one of the three formats defined in
199/// [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7).
200///
201/// Values are created with [`HttpDate::imf_fixdate`], [`HttpDate::rfc850`],
202/// and [`HttpDate::asctime`], or parsed with [`decode`]. Construction
203/// guarantees the components are always in range, so [`encode`] never fails.
204#[derive(Debug, PartialEq, Eq, Clone, Copy)]
205pub struct HttpDate {
206    format: Format,
207    dt: DateTime,
208}
209
210/// The textual format of an [`HttpDate`].
211///
212/// Private: `HttpDate` is constructor-only, so the RFC 850 two-digit-year
213/// invariant can be enforced at construction.
214#[derive(Debug, PartialEq, Eq, Clone, Copy)]
215enum Format {
216    ImfFixdate,
217    Rfc850,
218    Asctime,
219}
220
221impl HttpDate {
222    /// Constructs an IMF-fixdate, e.g. `Sun, 06 Nov 1994 08:49:37 GMT`.
223    #[must_use]
224    pub fn imf_fixdate(dt: DateTime) -> Self {
225        Self {
226            format: Format::ImfFixdate,
227            dt,
228        }
229    }
230
231    /// Constructs an RFC 850 date, e.g. `Sunday, 06-Nov-94 08:49:37 GMT`.
232    ///
233    /// The year must be in the range 0–99.
234    ///
235    /// # Errors
236    ///
237    /// Returns a [`Error`] if the year is outside 0–99, which cannot be
238    /// represented in RFC 850's two-digit-year format.
239    pub fn rfc850(dt: DateTime) -> Result<Self, Error> {
240        if dt.date.year > 99 {
241            return Err(Error::new("RFC 850 year out of range (0-99)"));
242        }
243        Ok(Self {
244            format: Format::Rfc850,
245            dt,
246        })
247    }
248
249    /// Constructs an asctime date, e.g. `Sun Nov  6 08:49:37 1994`.
250    #[must_use]
251    pub fn asctime(dt: DateTime) -> Self {
252        Self {
253            format: Format::Asctime,
254            dt,
255        }
256    }
257
258    /// Returns the [`DateTime`] components of this HTTP date.
259    #[must_use]
260    pub fn datetime(&self) -> &DateTime {
261        &self.dt
262    }
263
264    #[must_use]
265    pub fn is_imf_fixdate(&self) -> bool {
266        self.format == Format::ImfFixdate
267    }
268
269    #[must_use]
270    pub fn is_rfc850(&self) -> bool {
271        self.format == Format::Rfc850
272    }
273
274    #[must_use]
275    pub fn is_asctime(&self) -> bool {
276        self.format == Format::Asctime
277    }
278}
279
280/* ------------------- decoder ------------------- */
281/// An error returned when an HTTP date cannot be parsed.
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub struct Error {
284    msg: &'static str,
285    pos: Option<usize>,
286}
287
288impl Error {
289    /// Constructs a new decode error with the given message.
290    #[must_use]
291    const fn new(msg: &'static str) -> Self {
292        Self { msg, pos: None }
293    }
294
295    /// Sets the position in the input where decoding failed.
296    const fn at(self, pos: usize) -> Self {
297        Self {
298            pos: Some(pos),
299            ..self
300        }
301    }
302
303    #[must_use]
304    pub const fn position(&self) -> Option<usize> {
305        self.pos
306    }
307}
308
309impl fmt::Display for Error {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        f.write_str(self.msg)?;
312        if let Some(pos) = self.pos {
313            write!(f, " at position {pos}")?;
314        }
315        Ok(())
316    }
317}
318
319impl std::error::Error for Error {}
320
321/// A streaming parser for HTTP date values, used internally by [`decode`].
322struct Decoder<'a> {
323    buf: &'a str,
324    pos: usize,
325}
326
327impl<'a> Decoder<'a> {
328    fn new(buf: &'a str) -> Self {
329        Decoder { buf, pos: 0 }
330    }
331
332    #[inline]
333    fn advance(&mut self, n: usize) {
334        self.pos += n;
335    }
336
337    #[inline]
338    fn byte_at(&self, idx: usize) -> Result<u8, Error> {
339        self.buf
340            .as_bytes()
341            .get(idx)
342            .copied()
343            .ok_or_else(|| Error::new("unexpected end of input").at(idx))
344    }
345
346    fn expect(&mut self, expected: u8) -> Result<(), Error> {
347        let actual = self.byte_at(self.pos)?;
348        if actual == expected {
349            self.advance(1);
350            Ok(())
351        } else {
352            Err(Error::new("unexpected character").at(self.pos))
353        }
354    }
355
356    #[inline]
357    fn space(&mut self) -> Result<(), Error> {
358        self.expect(b' ')
359    }
360
361    #[inline]
362    fn comma(&mut self) -> Result<(), Error> {
363        self.expect(b',')
364    }
365
366    #[inline]
367    fn colon(&mut self) -> Result<(), Error> {
368        self.expect(b':')
369    }
370
371    fn month(&mut self) -> Result<u8, Error> {
372        let m = self
373            .buf
374            .as_bytes()
375            .get(self.pos..self.pos + 3)
376            .ok_or_else(|| Error::new("unexpected end of input").at(self.pos))?;
377        let n = (1..)
378            .zip(MONTHS)
379            .find_map(|(n, name)| (name.as_bytes() == m).then_some(n))
380            .ok_or_else(|| Error::new("invalid month value").at(self.pos))?;
381        self.advance(3);
382        Ok(n)
383    }
384
385    fn digits(&mut self, n: usize) -> Result<u16, Error> {
386        let buf = self.buf.as_bytes();
387        let mut value: u16 = 0;
388        for i in 0..n {
389            let pos = self.pos + i;
390            match buf.get(pos) {
391                Some(&c @ b'0'..=b'9') => {
392                    value = value * 10 + u16::from(c - b'0');
393                }
394                Some(_) => return Err(Error::new("expected digit").at(pos)),
395                None => return Err(Error::new("unexpected end of input").at(pos)),
396            }
397        }
398        self.advance(n);
399        Ok(value)
400    }
401
402    fn year(&mut self) -> Result<u16, Error> {
403        self.digits(4)
404    }
405
406    fn day(&mut self) -> Result<u8, Error> {
407        self.digits_u8(2)
408    }
409
410    /// Reads a one- or two-digit field, which always fits in a `u8`.
411    fn digits_u8(&mut self, n: usize) -> Result<u8, Error> {
412        let value = self.digits(n)?;
413        Ok(u8::try_from(value).expect("at most two digits"))
414    }
415
416    /// Consumes a run of ASCII letters and returns it as a slice of the input.
417    fn string(&mut self) -> &'a str {
418        let start = self.pos;
419        while self.pos < self.buf.len() && self.buf.as_bytes()[self.pos].is_ascii_alphabetic() {
420            self.advance(1);
421        }
422        // Only ASCII bytes are ever consumed, so both ends are char boundaries.
423        &self.buf[start..self.pos]
424    }
425
426    fn dayname_tok(&mut self) -> Result<DayNameTok, Error> {
427        let start = self.pos;
428        let s = self.string();
429        for d in DAYS {
430            if s == d.short() {
431                return Ok(DayNameTok::Short(d));
432            }
433            if s == d.long() {
434                return Ok(DayNameTok::Long(d));
435            }
436        }
437        Err(Error::new("invalid day name").at(start))
438    }
439
440    fn punctuation_tok(&mut self) -> Result<PunctuationTok, Error> {
441        let c = self.byte_at(self.pos)?;
442        let tok = match c {
443            b',' => PunctuationTok::Comma,
444            b' ' => PunctuationTok::Space,
445            _ => return Err(Error::new("expected ',' or ' ' after day name").at(self.pos)),
446        };
447        self.advance(1);
448        Ok(tok)
449    }
450
451    fn date1(&mut self) -> Result<Date, Error> {
452        let start = self.pos;
453        let day = self.day()?;
454        self.space()?;
455        let month = self.month()?;
456        self.space()?;
457        let year = self.year()?;
458        Date::new(year, month, day).map_err(|e| e.at(start))
459    }
460
461    fn time(&mut self) -> Result<Time, Error> {
462        let start = self.pos;
463        let hour = self.digits_u8(2)?;
464        self.colon()?;
465        let minute = self.digits_u8(2)?;
466        self.colon()?;
467        let second = self.digits_u8(2)?;
468        Time::new(hour, minute, second).map_err(|e| e.at(start))
469    }
470
471    fn gmt(&mut self) -> Result<(), Error> {
472        let start = self.pos;
473        if self.string() != "GMT" {
474            return Err(Error::new("expected 'GMT'").at(start));
475        }
476        Ok(())
477    }
478
479    // IMF-fixdate: day-name "," SP date1 SP time SP "GMT"
480    fn imf_fixdate(&mut self, dayname: DayName) -> Result<HttpDate, Error> {
481        self.space()?;
482        let date = self.date1()?;
483        self.space()?;
484        let time = self.time()?;
485        self.space()?;
486        self.gmt()?;
487        Ok(HttpDate::imf_fixdate(DateTime {
488            dayname,
489            date,
490            time,
491        }))
492    }
493
494    fn date2(&mut self) -> Result<Date, Error> {
495        let start = self.pos;
496        let day = self.day()?;
497        self.expect(b'-')?;
498        let month = self.month()?;
499        self.expect(b'-')?;
500        let year = self.digits(2)?;
501        Date::new(year, month, day).map_err(|e| e.at(start))
502    }
503
504    // RFC 850 date: day-name "," SP date2 SP time SP "GMT"
505    fn rfc850_date(&mut self, dayname: DayName) -> Result<HttpDate, Error> {
506        self.comma()?;
507        self.space()?;
508        let date = self.date2()?;
509        self.space()?;
510        let time = self.time()?;
511        self.space()?;
512        self.gmt()?;
513        let date = DateTime {
514            dayname,
515            date,
516            time,
517        };
518        HttpDate::rfc850(date)
519    }
520
521    fn date3(&mut self) -> Result<(u8, u8), Error> {
522        // month
523        let m = self.month()?;
524        self.space()?;
525        // day
526        let d = match self.byte_at(self.pos)? {
527            b' ' => {
528                self.space()?;
529                self.digits_u8(1)?
530            }
531            _ => self.digits_u8(2)?,
532        };
533        Ok((m, d))
534    }
535
536    // asctime date: day-name SP month SP (2DIGIT / (SP 1DIGIT)) SP time SP 4DIGIT
537    fn asctime_date(&mut self, dayname: DayName) -> Result<HttpDate, Error> {
538        let start = self.pos;
539        let (month, day) = self.date3()?;
540        self.space()?;
541        let time = self.time()?;
542        self.space()?;
543        let year = self.year()?;
544        let date = Date::new(year, month, day).map_err(|e| e.at(start))?;
545        Ok(HttpDate::asctime(DateTime {
546            dayname,
547            date,
548            time,
549        }))
550    }
551}
552
553/// Parses an HTTP date from its textual representation.
554///
555/// Accepts any of the three date formats defined in
556/// [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7):
557///
558/// | Format      | Example                          |
559/// |-------------|----------------------------------|
560/// | IMF-fixdate | `Sun, 06 Nov 1994 08:49:37 GMT`  |
561/// | RFC 850     | `Sunday, 06-Nov-94 08:49:37 GMT` |
562/// | asctime     | `Sun Nov  6 08:49:37 1994`       |
563///
564/// # Errors
565///
566/// Returns a [`Error`] if `buf` is not a well-formed HTTP date, for
567/// example when the day name or month is unknown, a required separator is
568/// missing, the input is truncated, there is trailing data after the date,
569/// or a component is out of range (day 1–31, hour 0–23, minute/second 0–59).
570///
571/// # Examples
572///
573/// An IMF-fixdate (the format HTTP servers must emit):
574///
575/// ```
576/// use http_date::{decode, HttpDate};
577///
578/// let date = decode("Sun, 06 Nov 1994 08:49:37 GMT")
579///     .expect("valid IMF-fixdate");
580/// assert!(date.is_imf_fixdate());
581/// ```
582///
583/// An RFC 850 date:
584///
585/// ```
586/// use http_date::{decode, HttpDate};
587///
588/// let date = decode("Sunday, 06-Nov-94 08:49:37 GMT")
589///     .expect("valid RFC 850 date");
590/// assert!(date.is_rfc850());
591/// ```
592///
593/// An asctime date:
594///
595/// ```
596/// use http_date::{decode, HttpDate};
597///
598/// let date = decode("Sun Nov  6 08:49:37 1994")
599///     .expect("valid asctime date");
600/// assert!(date.is_asctime());
601/// ```
602///
603/// Malformed input is rejected:
604///
605/// ```
606/// use http_date::decode;
607///
608/// assert!(decode("not a date").is_err());
609/// ```
610pub fn decode(buf: &str) -> Result<HttpDate, Error> {
611    let mut decoder = Decoder::new(buf);
612    let date = match decoder.dayname_tok()? {
613        DayNameTok::Long(dayname) => decoder.rfc850_date(dayname),
614        DayNameTok::Short(dayname) => match decoder.punctuation_tok()? {
615            PunctuationTok::Comma => decoder.imf_fixdate(dayname),
616            PunctuationTok::Space => decoder.asctime_date(dayname),
617        },
618    }?;
619    // The RFC 9110 `HTTP-date` grammar spans the whole field value, so any
620    // leftover input means `buf` is not a well-formed HTTP date.
621    if decoder.pos != decoder.buf.len() {
622        return Err(Error::new("trailing data after HTTP date").at(decoder.pos));
623    }
624    Ok(date)
625}
626
627/* ------------------- encoder ------------------- */
628
629impl DayName {
630    /// The three-letter abbreviation used by IMF-fixdate and asctime, e.g. `Mon`.
631    fn short(self) -> &'static str {
632        match self {
633            DayName::Mon => "Mon",
634            DayName::Tue => "Tue",
635            DayName::Wed => "Wed",
636            DayName::Thu => "Thu",
637            DayName::Fri => "Fri",
638            DayName::Sat => "Sat",
639            DayName::Sun => "Sun",
640        }
641    }
642
643    /// The full name used by RFC 850 dates, e.g. `Monday`.
644    fn long(self) -> &'static str {
645        match self {
646            DayName::Mon => "Monday",
647            DayName::Tue => "Tuesday",
648            DayName::Wed => "Wednesday",
649            DayName::Thu => "Thursday",
650            DayName::Fri => "Friday",
651            DayName::Sat => "Saturday",
652            DayName::Sun => "Sunday",
653        }
654    }
655}
656
657/// The three-letter month abbreviation, e.g. `Jan`.
658fn month_name(month: u8) -> &'static str {
659    MONTHS[usize::from(month) - 1]
660}
661
662/// Formats an HTTP date as a string in its original textual format.
663///
664/// The output is guaranteed to parse back with [`decode`] to the same value.
665/// `HttpDate` values are always well-formed — constructed via
666/// [`HttpDate::imf_fixdate`], [`HttpDate::rfc850`], or [`HttpDate::asctime`],
667/// or parsed with [`decode`] — so formatting cannot fail.
668///
669/// # Examples
670///
671/// ```
672/// use http_date::{decode, encode};
673///
674/// let date = decode("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
675/// assert_eq!(encode(&date), "Sun, 06 Nov 1994 08:49:37 GMT");
676/// ```
677#[must_use]
678pub fn encode(date: &HttpDate) -> String {
679    date.to_string()
680}
681
682impl fmt::Display for HttpDate {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        let dt = &self.dt;
685        let month = month_name(dt.date.month());
686        let day = dt.date.day();
687        let year = dt.date.year();
688        let time = dt.time;
689
690        match self.format {
691            Format::ImfFixdate => write!(
692                f,
693                "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
694                dt.dayname.short(),
695                day,
696                month,
697                year,
698                time.hour(),
699                time.minute(),
700                time.second(),
701            ),
702            // `format` is private, so `Format::Rfc850` implies year <= 99 by
703            // construction (`HttpDate::rfc850`).
704            Format::Rfc850 => write!(
705                f,
706                "{}, {:02}-{}-{:02} {:02}:{:02}:{:02} GMT",
707                dt.dayname.long(),
708                day,
709                month,
710                year,
711                time.hour(),
712                time.minute(),
713                time.second(),
714            ),
715            Format::Asctime => write!(
716                f,
717                "{} {} {:2} {:02}:{:02}:{:02} {:04}",
718                dt.dayname.short(),
719                month,
720                day,
721                time.hour(),
722                time.minute(),
723                time.second(),
724                year,
725            ),
726        }
727    }
728}
729
730/* ------------------- tests ------------------- */
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735    use expect_test::expect;
736    use std::fmt::Write;
737
738    // Test cases for decode
739
740    fn expect_imf_fixdate(decoded: &HttpDate) -> DateTime {
741        assert!(decoded.is_imf_fixdate(), "expected ImfFixdate");
742        *decoded.datetime()
743    }
744
745    fn expect_rfc850(decoded: &HttpDate) -> DateTime {
746        assert!(decoded.is_rfc850(), "expected Rfc850");
747        *decoded.datetime()
748    }
749
750    fn expect_asctime(decoded: &HttpDate) -> DateTime {
751        assert!(decoded.is_asctime(), "expected Asctime");
752        *decoded.datetime()
753    }
754
755    fn decode_all(inputs: &[&str]) -> String {
756        let mut out = String::new();
757        for input in inputs {
758            match decode(input) {
759                Ok(date) => writeln!(out, "{input:?} => {date}").unwrap(),
760                Err(err) => writeln!(out, "{input:?} => {err}").unwrap(),
761            }
762        }
763        out
764    }
765
766    #[test]
767    fn decode_out_of_range_and_boundaries() {
768        let actual = decode_all(&[
769            "Sun, 00 Nov 1994 08:49:37 GMT",
770            "Sun, 32 Nov 1994 08:49:37 GMT",
771            "Sunday, 32-Nov-94 08:49:37 GMT",
772            "Sun Nov 32 08:49:37 1994",
773            "Sun Nov  0 08:49:37 1994",
774            "Sun, 06 Nov 1994 24:49:37 GMT",
775            "Sun, 06 Nov 1994 08:60:37 GMT",
776            "Sun, 06 Nov 1994 08:49:60 GMT",
777            "Sun, 31 Nov 1994 08:49:37 GMT",
778            "Sun, 06 Nov 1994 23:59:59 GMT",
779        ]);
780        expect![[r#"
781            "Sun, 00 Nov 1994 08:49:37 GMT" => date out of range at position 5
782            "Sun, 32 Nov 1994 08:49:37 GMT" => date out of range at position 5
783            "Sunday, 32-Nov-94 08:49:37 GMT" => date out of range at position 8
784            "Sun Nov 32 08:49:37 1994" => date out of range at position 4
785            "Sun Nov  0 08:49:37 1994" => date out of range at position 4
786            "Sun, 06 Nov 1994 24:49:37 GMT" => time out of range at position 17
787            "Sun, 06 Nov 1994 08:60:37 GMT" => time out of range at position 17
788            "Sun, 06 Nov 1994 08:49:60 GMT" => time out of range at position 17
789            "Sun, 31 Nov 1994 08:49:37 GMT" => Sun, 31 Nov 1994 08:49:37 GMT
790            "Sun, 06 Nov 1994 23:59:59 GMT" => Sun, 06 Nov 1994 23:59:59 GMT
791        "#]]
792        .assert_eq(&actual);
793    }
794
795    #[test]
796    fn decode_parses_imf_fixdate() {
797        expect![[r"
798            Ok(
799                HttpDate {
800                    format: ImfFixdate,
801                    dt: DateTime {
802                        dayname: Sun,
803                        date: Date {
804                            year: 1994,
805                            month: 11,
806                            day: 6,
807                        },
808                        time: Time {
809                            hour: 8,
810                            minute: 49,
811                            second: 37,
812                        },
813                    },
814                },
815            )
816        "]]
817        .assert_debug_eq(&decode("Sun, 06 Nov 1994 08:49:37 GMT"));
818    }
819
820    #[test]
821    fn decode_parses_rfc850_date() {
822        let dt = expect_rfc850(&decode("Sunday, 06-Nov-94 08:49:37 GMT").unwrap());
823        assert_eq!(dt.dayname, DayName::Sun);
824        assert_eq!(dt.date, Date::new(94, 11, 6).unwrap());
825        assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
826    }
827
828    #[test]
829    fn decode_parses_asctime() {
830        let dt = expect_asctime(&decode("Sun Nov  6 08:49:37 1994").unwrap());
831        assert_eq!(dt.dayname, DayName::Sun);
832        assert_eq!(dt.date, Date::new(1994, 11, 6).unwrap());
833        assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
834    }
835
836    #[test]
837    fn decode_parses_all_imf_fixdate_weekdays() {
838        let cases = [
839            ("Mon", DayName::Mon),
840            ("Tue", DayName::Tue),
841            ("Wed", DayName::Wed),
842            ("Thu", DayName::Thu),
843            ("Fri", DayName::Fri),
844            ("Sat", DayName::Sat),
845            ("Sun", DayName::Sun),
846        ];
847        for (name, dayname) in cases {
848            let input = format!("{name}, 06 Nov 1994 08:49:37 GMT");
849            let dt = expect_imf_fixdate(&decode(&input).unwrap());
850            assert_eq!(dt.dayname, dayname, "weekday {name}");
851        }
852    }
853
854    #[test]
855    fn decode_rejects_malformed_input() {
856        let actual = decode_all(&[
857            "",
858            "Funday, 06 Nov 1994 08:49:37 GMT",
859            "mon, 06 Nov 1994 08:49:37 GMT",
860            "Wednes, 06-Nov-94 08:49:37 GMT",
861            ", 06 Nov 1994 08:49:37 GMT",
862            "Sun",
863            "Sun;06 Nov 1994 08:49:37 GMT",
864            "Sun,06 Nov 1994 08:49:37 GMT",
865            "Sun 06 Nov 1994",
866            "Sun, 0x Nov 1994 08:49:37 GMT",
867            "Sun, 06Nov 1994 08:49:37 GMT",
868            "Sun, 06 Xyz 1994 08:49:37 GMT",
869            "Sun, 06 Nov",
870            "Sun, 06 Nov 19",
871            "Sun, 06 Nov 1994 0849:37 GMT",
872            "Sun, 06 Nov 1994 08:x9:37 GMT",
873            "Sun, 06 Nov 1994 08:49:3",
874            "Sun, 06 Nov 1994 08:49:37 ",
875            "Sunday 06-Nov-94 08:49:37 GMT",
876            "Sunday, 06 Nov 94 08:49:37 GMT",
877        ]);
878        expect![[r#"
879            "" => invalid day name at position 0
880            "Funday, 06 Nov 1994 08:49:37 GMT" => invalid day name at position 0
881            "mon, 06 Nov 1994 08:49:37 GMT" => invalid day name at position 0
882            "Wednes, 06-Nov-94 08:49:37 GMT" => invalid day name at position 0
883            ", 06 Nov 1994 08:49:37 GMT" => invalid day name at position 0
884            "Sun" => unexpected end of input at position 3
885            "Sun;06 Nov 1994 08:49:37 GMT" => expected ',' or ' ' after day name at position 3
886            "Sun,06 Nov 1994 08:49:37 GMT" => unexpected character at position 4
887            "Sun 06 Nov 1994" => invalid month value at position 4
888            "Sun, 0x Nov 1994 08:49:37 GMT" => expected digit at position 6
889            "Sun, 06Nov 1994 08:49:37 GMT" => unexpected character at position 7
890            "Sun, 06 Xyz 1994 08:49:37 GMT" => invalid month value at position 8
891            "Sun, 06 Nov" => unexpected end of input at position 11
892            "Sun, 06 Nov 19" => unexpected end of input at position 14
893            "Sun, 06 Nov 1994 0849:37 GMT" => unexpected character at position 19
894            "Sun, 06 Nov 1994 08:x9:37 GMT" => expected digit at position 20
895            "Sun, 06 Nov 1994 08:49:3" => unexpected end of input at position 24
896            "Sun, 06 Nov 1994 08:49:37 " => expected 'GMT' at position 26
897            "Sunday 06-Nov-94 08:49:37 GMT" => unexpected character at position 6
898            "Sunday, 06 Nov 94 08:49:37 GMT" => unexpected character at position 10
899        "#]]
900        .assert_eq(&actual);
901    }
902
903    #[test]
904    fn decode_rejects_trailing_data() {
905        let actual = decode_all(&[
906            "Sun, 06 Nov 1994 08:49:37 GMT ",
907            "Sun, 06 Nov 1994 08:49:37 GMT 123",
908            "Sun, 06 Nov 1994 08:49:37 GMT.",
909            "Sunday, 06-Nov-94 08:49:37 GMT extra",
910            "Sun Nov  6 08:49:37 1994!",
911        ]);
912        expect![[r#"
913            "Sun, 06 Nov 1994 08:49:37 GMT " => trailing data after HTTP date at position 29
914            "Sun, 06 Nov 1994 08:49:37 GMT 123" => trailing data after HTTP date at position 29
915            "Sun, 06 Nov 1994 08:49:37 GMT." => trailing data after HTTP date at position 29
916            "Sunday, 06-Nov-94 08:49:37 GMT extra" => trailing data after HTTP date at position 30
917            "Sun Nov  6 08:49:37 1994!" => trailing data after HTTP date at position 24
918        "#]]
919        .assert_eq(&actual);
920    }
921
922    // A fixed valid `DateTime` used by the constructor tests below.
923    fn sun_nov_6(year: u16) -> DateTime {
924        DateTime {
925            dayname: DayName::Sun,
926            date: Date::new(year, 11, 6).unwrap(),
927            time: Time::new(8, 49, 37).unwrap(),
928        }
929    }
930
931    #[test]
932    fn date_new_rejects_out_of_range() {
933        assert!(Date::new(1994, 11, 6).is_ok());
934        assert!(Date::new(10_000, 11, 6).is_err());
935        assert!(Date::new(1994, 0, 6).is_err());
936        assert!(Date::new(1994, 13, 6).is_err());
937        assert!(Date::new(1994, 11, 0).is_err());
938        assert!(Date::new(1994, 11, 32).is_err());
939    }
940
941    #[test]
942    fn time_new_rejects_out_of_range() {
943        assert!(Time::new(8, 49, 37).is_ok());
944        assert!(Time::new(24, 0, 0).is_err());
945        assert!(Time::new(0, 60, 0).is_err());
946        assert!(Time::new(0, 0, 60).is_err());
947    }
948
949    #[test]
950    fn rfc850_constructor_rejects_year_above_99() {
951        assert!(HttpDate::rfc850(sun_nov_6(99)).is_ok());
952        assert!(HttpDate::rfc850(sun_nov_6(100)).is_err());
953        assert!(HttpDate::rfc850(sun_nov_6(1994)).is_err());
954    }
955
956    #[test]
957    fn encode_round_trips_constructed_values() {
958        let cases = [
959            HttpDate::imf_fixdate(sun_nov_6(1994)),
960            HttpDate::rfc850(sun_nov_6(94)).unwrap(),
961            HttpDate::asctime(sun_nov_6(1994)),
962        ];
963        for date in cases {
964            let s = encode(&date);
965            assert_eq!(decode(&s).unwrap(), date, "encode produced {s:?}");
966        }
967    }
968}