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: i32) -> 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(Debug, PartialEq, Eq, 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
75#[derive(Debug, PartialEq, Eq, Clone, Copy)]
76enum PunctuationTok {
77    Comma,
78    Space,
79}
80
81/// The calendar date portion of an HTTP date.
82#[derive(Debug, PartialEq, Eq, Clone, Copy)]
83pub struct Date {
84    /// The year, e.g. `1994`.
85    year: i32,
86    /// The month, 1–12, where 1 is January.
87    month: i32,
88    /// The day of the month, 1–31.
89    day: i32,
90}
91
92impl Date {
93    /// Constructs a date.
94    ///
95    /// # Errors
96    ///
97    /// Returns a [`DecodeError`] if a component is out of range: year 0–9999,
98    /// month 1–12, day 1–31.
99    pub fn new(year: i32, month: i32, day: i32) -> Result<Self, DecodeError> {
100        if !(0..=9999).contains(&year) || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
101            return Err(DecodeError::new("Date out of range"));
102        }
103        Ok(Self { year, month, day })
104    }
105
106    #[must_use]
107    pub fn year(&self) -> i32 {
108        self.year
109    }
110
111    #[must_use]
112    pub fn month(&self) -> i32 {
113        self.month
114    }
115
116    #[must_use]
117    pub fn day(&self) -> i32 {
118        self.day
119    }
120}
121
122/// The time-of-day portion of an HTTP date.
123///
124/// Constructed with [`Time::new`], which enforces the valid ranges, so a
125/// `Time` value is always well-formed (hour 0–23, minute/second 0–59).
126#[derive(Debug, PartialEq, Eq, Clone, Copy)]
127pub struct Time {
128    /// The hour, 0–23.
129    hour: i32,
130    /// The minute, 0–59.
131    minute: i32,
132    /// The second, 0–59.
133    second: i32,
134}
135
136impl Time {
137    /// Constructs a time.
138    ///
139    /// # Errors
140    ///
141    /// Returns a [`DecodeError`] if a component is out of range: hour 0–23,
142    /// minute 0–59, second 0–59.
143    pub fn new(hour: i32, minute: i32, second: i32) -> Result<Self, DecodeError> {
144        if !(0..=23).contains(&hour) || !(0..=59).contains(&minute) || !(0..=59).contains(&second) {
145            return Err(DecodeError::new("Time out of range"));
146        }
147        Ok(Self {
148            hour,
149            minute,
150            second,
151        })
152    }
153
154    #[must_use]
155    pub fn hour(&self) -> i32 {
156        self.hour
157    }
158
159    #[must_use]
160    pub fn minute(&self) -> i32 {
161        self.minute
162    }
163
164    #[must_use]
165    pub fn second(&self) -> i32 {
166        self.second
167    }
168}
169
170/// A fully decoded HTTP date: weekday, calendar date, and time of day.
171///
172/// This is the value carried by the [`HttpDate`] variants.
173#[derive(Debug, PartialEq, Eq, Clone, Copy)]
174pub struct DateTime {
175    /// The day of the week.
176    pub dayname: DayName,
177    /// The calendar date.
178    pub date: Date,
179    /// The time of day.
180    pub time: Time,
181}
182
183/// An HTTP date in one of the three formats defined in
184/// [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7).
185///
186/// Values are created with [`HttpDate::imf_fixdate`], [`HttpDate::rfc850`],
187/// and [`HttpDate::asctime`], or parsed with [`decode`]. Construction
188/// guarantees the components are always in range, so [`encode`] never fails.
189#[derive(Debug, PartialEq, Eq, Clone, Copy)]
190pub struct HttpDate {
191    format: Format,
192    dt: DateTime,
193}
194
195/// The textual format of an [`HttpDate`].
196///
197/// Private: `HttpDate` is constructor-only, so the RFC 850 two-digit-year
198/// invariant can be enforced at construction.
199#[derive(Debug, PartialEq, Eq, Clone, Copy)]
200enum Format {
201    ImfFixdate,
202    Rfc850,
203    Asctime,
204}
205
206impl HttpDate {
207    /// Constructs an IMF-fixdate, e.g. `Sun, 06 Nov 1994 08:49:37 GMT`.
208    #[must_use]
209    pub fn imf_fixdate(dt: DateTime) -> Self {
210        Self {
211            format: Format::ImfFixdate,
212            dt,
213        }
214    }
215
216    /// Constructs an RFC 850 date, e.g. `Sunday, 06-Nov-94 08:49:37 GMT`.
217    ///
218    /// The year must be in the range 0–99.
219    ///
220    /// # Errors
221    ///
222    /// Returns a [`DecodeError`] if the year is outside 0–99, which cannot be
223    /// represented in RFC 850's two-digit-year format.
224    pub fn rfc850(dt: DateTime) -> Result<Self, DecodeError> {
225        if !(0..=99).contains(&dt.date.year) {
226            return Err(DecodeError::new("RFC 850 year out of range (0-99)"));
227        }
228        Ok(Self {
229            format: Format::Rfc850,
230            dt,
231        })
232    }
233
234    /// Constructs an asctime date, e.g. `Sun Nov  6 08:49:37 1994`.
235    #[must_use]
236    pub fn asctime(dt: DateTime) -> Self {
237        Self {
238            format: Format::Asctime,
239            dt,
240        }
241    }
242
243    /// Returns the [`DateTime`] components of this HTTP date.
244    #[must_use]
245    pub fn datetime(&self) -> &DateTime {
246        &self.dt
247    }
248
249    #[must_use]
250    pub fn is_imf_fixdate(&self) -> bool {
251        self.format == Format::ImfFixdate
252    }
253
254    #[must_use]
255    pub fn is_rfc850(&self) -> bool {
256        self.format == Format::Rfc850
257    }
258
259    #[must_use]
260    pub fn is_asctime(&self) -> bool {
261        self.format == Format::Asctime
262    }
263}
264
265/* ------------------- decoder ------------------- */
266/// An error returned when an HTTP date cannot be parsed.
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct DecodeError(String);
269
270impl DecodeError {
271    /// Constructs a new decode error with the given message.
272    pub fn new(msg: impl Into<String>) -> Self {
273        Self(msg.into())
274    }
275}
276
277impl fmt::Display for DecodeError {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        write!(f, "DecodeError: {}", self.0)
280    }
281}
282
283impl std::error::Error for DecodeError {}
284
285/// A streaming parser for HTTP date values, used internally by [`decode`].
286struct Decoder<'a> {
287    buf: &'a str,
288    pos: usize,
289}
290
291impl Decoder<'_> {
292    fn new(buf: &str) -> Decoder<'_> {
293        Decoder { buf, pos: 0 }
294    }
295
296    #[inline]
297    fn advance(&mut self, n: usize) {
298        self.pos += n;
299    }
300
301    #[inline]
302    fn byte_at(&self, idx: usize) -> Result<u8, DecodeError> {
303        self.buf
304            .as_bytes()
305            .get(idx)
306            .copied()
307            .ok_or_else(|| DecodeError::new("Unexpected end of input"))
308    }
309
310    fn expect(&mut self, expected: u8) -> Result<(), DecodeError> {
311        let actual = self.byte_at(self.pos)?;
312        if actual == expected {
313            self.advance(1);
314            Ok(())
315        } else {
316            Err(DecodeError::new(format!(
317                "Expected byte {}, but found {}",
318                expected as char, actual as char
319            )))
320        }
321    }
322
323    #[inline]
324    fn space(&mut self) -> Result<(), DecodeError> {
325        self.expect(b' ')
326    }
327
328    #[inline]
329    fn comma(&mut self) -> Result<(), DecodeError> {
330        self.expect(b',')
331    }
332
333    #[inline]
334    fn colon(&mut self) -> Result<(), DecodeError> {
335        self.expect(b':')
336    }
337
338    fn month(&mut self) -> Result<i32, DecodeError> {
339        let m = self
340            .buf
341            .get(self.pos..self.pos + 3)
342            .ok_or_else(|| DecodeError::new("Unexpected end of input"))?;
343        let n = match m {
344            "Jan" => 1,
345            "Feb" => 2,
346            "Mar" => 3,
347            "Apr" => 4,
348            "May" => 5,
349            "Jun" => 6,
350            "Jul" => 7,
351            "Aug" => 8,
352            "Sep" => 9,
353            "Oct" => 10,
354            "Nov" => 11,
355            "Dec" => 12,
356            _ => {
357                return Err(DecodeError::new(format!("Invalid month value: {m}")));
358            }
359        };
360        self.advance(3);
361        Ok(n)
362    }
363
364    fn digits(&mut self, n: usize) -> Result<i32, DecodeError> {
365        let buf = self.buf.as_bytes();
366        let mut value: i32 = 0;
367        for i in 0..n {
368            let pos = self.pos + i;
369            match buf.get(pos) {
370                Some(&c @ b'0'..=b'9') => {
371                    value = value * 10 + i32::from(c - b'0');
372                }
373                Some(&c) => {
374                    return Err(DecodeError::new(format!(
375                        "Expected digit at position {}, but found {}",
376                        pos, c as char
377                    )));
378                }
379                None => return Err(DecodeError::new("Unexpected end of input")),
380            }
381        }
382        self.advance(n);
383        Ok(value)
384    }
385
386    fn year(&mut self) -> Result<i32, DecodeError> {
387        self.digits(4)
388    }
389
390    fn day(&mut self) -> Result<i32, DecodeError> {
391        self.digits(2)
392    }
393
394    fn string(&mut self) -> String {
395        let mut out = String::with_capacity(5);
396        while self.pos < self.buf.len() {
397            let c = self.buf.as_bytes()[self.pos];
398            if !c.is_ascii_alphabetic() {
399                break;
400            }
401            out.push(c as char);
402            self.advance(1);
403        }
404        out
405    }
406
407    fn dayname_tok(&mut self) -> Result<DayNameTok, DecodeError> {
408        let s = self.string();
409        let dayname = match s.as_str() {
410            "Mon" => DayNameTok::Short(DayName::Mon),
411            "Tue" => DayNameTok::Short(DayName::Tue),
412            "Wed" => DayNameTok::Short(DayName::Wed),
413            "Thu" => DayNameTok::Short(DayName::Thu),
414            "Fri" => DayNameTok::Short(DayName::Fri),
415            "Sat" => DayNameTok::Short(DayName::Sat),
416            "Sun" => DayNameTok::Short(DayName::Sun),
417            "Monday" => DayNameTok::Long(DayName::Mon),
418            "Tuesday" => DayNameTok::Long(DayName::Tue),
419            "Wednesday" => DayNameTok::Long(DayName::Wed),
420            "Thursday" => DayNameTok::Long(DayName::Thu),
421            "Friday" => DayNameTok::Long(DayName::Fri),
422            "Saturday" => DayNameTok::Long(DayName::Sat),
423            "Sunday" => DayNameTok::Long(DayName::Sun),
424            _ => return Err(DecodeError::new(format!("Invalid day name: {s}"))),
425        };
426        Ok(dayname)
427    }
428
429    fn punctuation_tok(&mut self) -> Result<PunctuationTok, DecodeError> {
430        let c = self.byte_at(self.pos)?;
431        let tok = match c {
432            b',' => PunctuationTok::Comma,
433            b' ' => PunctuationTok::Space,
434            _ => {
435                return Err(DecodeError::new(format!(
436                    "Expected punctuation at position {}, but found {}",
437                    self.pos, c as char
438                )));
439            }
440        };
441        self.advance(1);
442        Ok(tok)
443    }
444
445    fn date1(&mut self) -> Result<Date, DecodeError> {
446        let day = self.day()?;
447        self.space()?;
448        let month = self.month()?;
449        self.space()?;
450        let year = self.year()?;
451        Date::new(year, month, day)
452    }
453
454    fn time(&mut self) -> Result<Time, DecodeError> {
455        let hour = self.digits(2)?;
456        self.colon()?;
457        let minute = self.digits(2)?;
458        self.colon()?;
459        let second = self.digits(2)?;
460        Time::new(hour, minute, second)
461    }
462
463    fn gmt(&mut self) -> Result<(), DecodeError> {
464        let s = self.string();
465        if s != "GMT" {
466            return Err(DecodeError::new(format!("Expected 'GMT', but found '{s}'")));
467        }
468        Ok(())
469    }
470
471    // IMF-fixdate: day-name "," SP date1 SP time SP "GMT"
472    fn imf_fixdate(&mut self, dayname: DayName) -> Result<HttpDate, DecodeError> {
473        self.space()?;
474        let date = self.date1()?;
475        self.space()?;
476        let time = self.time()?;
477        self.space()?;
478        self.gmt()?;
479        Ok(HttpDate::imf_fixdate(DateTime {
480            dayname,
481            date,
482            time,
483        }))
484    }
485
486    fn date2(&mut self) -> Result<Date, DecodeError> {
487        let day = self.day()?;
488        self.expect(b'-')?;
489        let month = self.month()?;
490        self.expect(b'-')?;
491        let year = self.digits(2)?;
492        Date::new(year, month, day)
493    }
494
495    // RFC 850 date: day-name "," SP date2 SP time SP "GMT"
496    fn rfc850_date(&mut self, dayname: DayName) -> Result<HttpDate, DecodeError> {
497        self.comma()?;
498        self.space()?;
499        let date = self.date2()?;
500        self.space()?;
501        let time = self.time()?;
502        self.space()?;
503        self.gmt()?;
504        let date = DateTime {
505            dayname,
506            date,
507            time,
508        };
509        HttpDate::rfc850(date)
510    }
511
512    fn date3(&mut self) -> Result<(i32, i32), DecodeError> {
513        // month
514        let m = self.month()?;
515        self.space()?;
516        // day
517        let d = match self.byte_at(self.pos)? {
518            b' ' => {
519                self.space()?;
520                self.digits(1)?
521            }
522            _ => self.digits(2)?,
523        };
524        Ok((m, d))
525    }
526
527    // asctime date: day-name SP month SP (2DIGIT / (SP 1DIGIT)) SP time SP 4DIGIT
528    fn asctime_date(&mut self, dayname: DayName) -> Result<HttpDate, DecodeError> {
529        let (month, day) = self.date3()?;
530        self.space()?;
531        let time = self.time()?;
532        self.space()?;
533        let year = self.year()?;
534        let date = Date::new(year, month, day)?;
535        Ok(HttpDate::asctime(DateTime {
536            dayname,
537            date,
538            time,
539        }))
540    }
541}
542
543/// Parses an HTTP date from its textual representation.
544///
545/// Accepts any of the three date formats defined in
546/// [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7):
547///
548/// | Format      | Example                          |
549/// |-------------|----------------------------------|
550/// | IMF-fixdate | `Sun, 06 Nov 1994 08:49:37 GMT`  |
551/// | RFC 850     | `Sunday, 06-Nov-94 08:49:37 GMT` |
552/// | asctime     | `Sun Nov  6 08:49:37 1994`       |
553///
554/// # Errors
555///
556/// Returns a [`DecodeError`] if `buf` is not a well-formed HTTP date, for
557/// example when the day name or month is unknown, a required separator is
558/// missing, the input is truncated, there is trailing data after the date,
559/// or a component is out of range (day 1–31, hour 0–23, minute/second 0–59).
560///
561/// # Examples
562///
563/// An IMF-fixdate (the format HTTP servers must emit):
564///
565/// ```
566/// use http_date::{decode, HttpDate};
567///
568/// let date = decode("Sun, 06 Nov 1994 08:49:37 GMT")
569///     .expect("valid IMF-fixdate");
570/// assert!(date.is_imf_fixdate());
571/// ```
572///
573/// An RFC 850 date:
574///
575/// ```
576/// use http_date::{decode, HttpDate};
577///
578/// let date = decode("Sunday, 06-Nov-94 08:49:37 GMT")
579///     .expect("valid RFC 850 date");
580/// assert!(date.is_rfc850());
581/// ```
582///
583/// An asctime date:
584///
585/// ```
586/// use http_date::{decode, HttpDate};
587///
588/// let date = decode("Sun Nov  6 08:49:37 1994")
589///     .expect("valid asctime date");
590/// assert!(date.is_asctime());
591/// ```
592///
593/// Malformed input is rejected:
594///
595/// ```
596/// use http_date::decode;
597///
598/// assert!(decode("not a date").is_err());
599/// ```
600pub fn decode(buf: &str) -> Result<HttpDate, DecodeError> {
601    let mut decoder = Decoder::new(buf);
602    let date = match decoder.dayname_tok()? {
603        DayNameTok::Long(dayname) => decoder.rfc850_date(dayname),
604        DayNameTok::Short(dayname) => match decoder.punctuation_tok()? {
605            PunctuationTok::Comma => decoder.imf_fixdate(dayname),
606            PunctuationTok::Space => decoder.asctime_date(dayname),
607        },
608    }?;
609    // The RFC 9110 `HTTP-date` grammar spans the whole field value, so any
610    // leftover input means `buf` is not a well-formed HTTP date.
611    if decoder.pos != decoder.buf.len() {
612        return Err(DecodeError::new(format!(
613            "Trailing data after HTTP date at position {}",
614            decoder.pos
615        )));
616    }
617    Ok(date)
618}
619
620/* ------------------- encoder ------------------- */
621
622impl DayName {
623    /// The three-letter abbreviation used by IMF-fixdate and asctime, e.g. `Mon`.
624    fn short(self) -> &'static str {
625        match self {
626            DayName::Mon => "Mon",
627            DayName::Tue => "Tue",
628            DayName::Wed => "Wed",
629            DayName::Thu => "Thu",
630            DayName::Fri => "Fri",
631            DayName::Sat => "Sat",
632            DayName::Sun => "Sun",
633        }
634    }
635
636    /// The full name used by RFC 850 dates, e.g. `Monday`.
637    fn long(self) -> &'static str {
638        match self {
639            DayName::Mon => "Monday",
640            DayName::Tue => "Tuesday",
641            DayName::Wed => "Wednesday",
642            DayName::Thu => "Thursday",
643            DayName::Fri => "Friday",
644            DayName::Sat => "Saturday",
645            DayName::Sun => "Sunday",
646        }
647    }
648}
649
650/// The three-letter month abbreviation, e.g. `Jan`.
651fn month_name(month: i32) -> &'static str {
652    match month {
653        1 => "Jan",
654        2 => "Feb",
655        3 => "Mar",
656        4 => "Apr",
657        5 => "May",
658        6 => "Jun",
659        7 => "Jul",
660        8 => "Aug",
661        9 => "Sep",
662        10 => "Oct",
663        11 => "Nov",
664        12 => "Dec",
665        _ => unreachable!("month {month} is out of range 1..=12"),
666    }
667}
668
669/// Formats an HTTP date as a string in its original textual format.
670///
671/// The output is guaranteed to parse back with [`decode`] to the same value.
672/// `HttpDate` values are always well-formed — constructed via
673/// [`HttpDate::imf_fixdate`], [`HttpDate::rfc850`], or [`HttpDate::asctime`],
674/// or parsed with [`decode`] — so formatting cannot fail.
675///
676/// # Examples
677///
678/// ```
679/// use http_date::{decode, encode};
680///
681/// let date = decode("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
682/// assert_eq!(encode(&date), "Sun, 06 Nov 1994 08:49:37 GMT");
683/// ```
684#[must_use]
685pub fn encode(date: &HttpDate) -> String {
686    let dt = date.datetime();
687    let month = month_name(dt.date.month());
688    let day = dt.date.day();
689    let year = dt.date.year();
690    let time = dt.time;
691    match date.format {
692        Format::ImfFixdate => format!(
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 => format!(
705            "{}, {:02}-{}-{:02} {:02}:{:02}:{:02} GMT",
706            dt.dayname.long(),
707            day,
708            month,
709            year,
710            time.hour(),
711            time.minute(),
712            time.second(),
713        ),
714        Format::Asctime => format!(
715            "{} {} {:2} {:02}:{:02}:{:02} {:04}",
716            dt.dayname.short(),
717            month,
718            day,
719            time.hour(),
720            time.minute(),
721            time.second(),
722            year,
723        ),
724    }
725}
726
727impl fmt::Display for HttpDate {
728    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729        f.write_str(&encode(self))
730    }
731}
732
733/* ------------------- tests ------------------- */
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    #[test]
740    fn expect_matching_byte_advances_position() {
741        let mut d = Decoder::new("abc");
742        assert_eq!(d.expect(b'a'), Ok(()));
743        assert_eq!(d.pos, 1);
744    }
745
746    #[test]
747    fn expect_mismatched_byte_returns_error() {
748        let mut d = Decoder::new("abc");
749        let result = d.expect(b'b');
750        assert!(result.is_err());
751        assert_eq!(d.pos, 0); // Position should not advance on error
752    }
753
754    #[test]
755    fn expect_at_end_of_input_returns_error() {
756        let mut d = Decoder::new("ab");
757        d.pos = 2;
758        let err = d.expect(b'a').unwrap_err();
759        assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
760        assert_eq!(d.pos, 2);
761    }
762
763    #[test]
764    fn parsing_punctuated_sequence_advances_through_input() {
765        let mut d = Decoder::new("Sun, 06");
766        // weekday(Sun)
767        d.expect(b'S').unwrap();
768        d.expect(b'u').unwrap();
769        d.expect(b'n').unwrap();
770        d.comma().unwrap();
771        d.space().unwrap();
772        // day-of-month(06)
773        d.expect(b'0').unwrap();
774        d.expect(b'6').unwrap();
775        assert_eq!(d.pos, 7); // consumed "Sun, 06"
776    }
777
778    #[test]
779    fn parsing_date_sequence_with_month_advances_through_input() {
780        let mut d = Decoder::new("Sun, 06 Nov 1994");
781        // weekday(Sun)
782        d.expect(b'S').unwrap();
783        d.expect(b'u').unwrap();
784        d.expect(b'n').unwrap();
785        d.comma().unwrap();
786        d.space().unwrap();
787        // day-of-month(06)
788        assert_eq!(d.day().unwrap(), 6);
789        d.space().unwrap();
790        // month(Nov)
791        let month = d.month().unwrap();
792        assert_eq!(month, 11);
793        d.space().unwrap();
794        // year(1994)
795        assert_eq!(d.year().unwrap(), 1994);
796        assert_eq!(d.pos, 16); // consumed "Sun, 06 Nov 1994"
797    }
798
799    #[test]
800    fn string_reads_alphabetic_run_and_advances() {
801        let mut d = Decoder::new("Sun, 06");
802        assert_eq!(d.string(), "Sun");
803        assert_eq!(d.pos, 3); // consumed "Sun"
804    }
805
806    #[test]
807    fn string_reads_to_end_of_input() {
808        let mut d = Decoder::new("GMT");
809        assert_eq!(d.string(), "GMT");
810        assert_eq!(d.pos, 3); // consumed "GMT"
811    }
812
813    #[test]
814    fn string_returns_empty_when_current_byte_is_not_alphabetic() {
815        let mut d = Decoder::new("123");
816        assert_eq!(d.string(), "");
817        assert_eq!(d.pos, 0); // position should not advance
818    }
819
820    #[test]
821    fn string_returns_empty_at_end_of_input() {
822        let mut d = Decoder::new("Sun,");
823        d.pos = 4; // position at end of input
824        assert_eq!(d.string(), "");
825        assert_eq!(d.pos, 4); // position should not advance
826    }
827
828    // Test cases for dayname_tok
829
830    #[test]
831    fn dayname_tok_parses_all_names() {
832        let cases = [
833            ("Mon", DayNameTok::Short(DayName::Mon)),
834            ("Tue", DayNameTok::Short(DayName::Tue)),
835            ("Wed", DayNameTok::Short(DayName::Wed)),
836            ("Thu", DayNameTok::Short(DayName::Thu)),
837            ("Fri", DayNameTok::Short(DayName::Fri)),
838            ("Sat", DayNameTok::Short(DayName::Sat)),
839            ("Sun", DayNameTok::Short(DayName::Sun)),
840            ("Monday", DayNameTok::Long(DayName::Mon)),
841            ("Tuesday", DayNameTok::Long(DayName::Tue)),
842            ("Wednesday", DayNameTok::Long(DayName::Wed)),
843            ("Thursday", DayNameTok::Long(DayName::Thu)),
844            ("Friday", DayNameTok::Long(DayName::Fri)),
845            ("Saturday", DayNameTok::Long(DayName::Sat)),
846            ("Sunday", DayNameTok::Long(DayName::Sun)),
847        ];
848        for (s, expected) in cases {
849            let mut d = Decoder::new(s);
850            assert_eq!(d.dayname_tok(), Ok(expected), "day name {s}");
851            assert_eq!(d.pos, s.len(), "pos after {s}");
852        }
853    }
854
855    #[test]
856    fn dayname_tok_rejects_invalid_name() {
857        let mut d = Decoder::new("Funday");
858        let result = d.dayname_tok();
859        assert!(result.is_err());
860        assert_eq!(d.pos, 6); // consumed the invalid name
861    }
862
863    #[test]
864    fn dayname_tok_is_case_sensitive() {
865        let mut d = Decoder::new("mon");
866        let result = d.dayname_tok();
867        assert!(result.is_err());
868        assert_eq!(d.pos, 3); // consumed the invalid name
869    }
870
871    #[test]
872    fn dayname_tok_rejects_empty_run() {
873        let mut d = Decoder::new(", 06 Nov 1994");
874        let result = d.dayname_tok();
875        assert!(result.is_err());
876        assert_eq!(d.pos, 0); // position should not advance
877    }
878
879    #[test]
880    fn dayname_tok_rejects_truncated_long_name() {
881        let mut d = Decoder::new("Wednes");
882        let result = d.dayname_tok();
883        assert!(result.is_err());
884        assert_eq!(d.pos, 6); // consumed the partial name
885    }
886
887    // Test cases for punctuation_tok
888
889    #[test]
890    fn punctuation_tok_parses_comma() {
891        let mut d = Decoder::new(",06 Nov");
892        assert_eq!(d.punctuation_tok(), Ok(PunctuationTok::Comma));
893        assert_eq!(d.pos, 1);
894    }
895
896    #[test]
897    fn punctuation_tok_parses_space() {
898        let mut d = Decoder::new(" 06 Nov");
899        assert_eq!(d.punctuation_tok(), Ok(PunctuationTok::Space));
900        assert_eq!(d.pos, 1);
901    }
902
903    #[test]
904    fn punctuation_tok_rejects_non_punctuation() {
905        let mut d = Decoder::new("06 Nov");
906        let err = d.punctuation_tok().unwrap_err();
907        assert!(
908            err.to_string()
909                .contains("Expected punctuation at position 0, but found 0")
910        );
911        assert_eq!(d.pos, 0); // position unchanged on error
912    }
913
914    #[test]
915    fn punctuation_tok_at_end_of_input_returns_error() {
916        let mut d = Decoder::new("06");
917        d.pos = 2; // at end of input
918        let err = d.punctuation_tok().unwrap_err();
919        assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
920        assert_eq!(d.pos, 2);
921    }
922
923    // Test cases for date1
924
925    #[test]
926    fn date1_parses_valid_date() {
927        let mut d = Decoder::new("06 Nov 1994");
928        assert_eq!(d.date1(), Date::new(1994, 11, 6));
929        assert_eq!(d.pos, 11); // consumed "06 Nov 1994"
930    }
931
932    #[test]
933    fn date1_parses_leading_zero_day() {
934        let mut d = Decoder::new("01 Jan 2000");
935        assert_eq!(d.date1(), Date::new(2000, 1, 1));
936        assert_eq!(d.pos, 11);
937    }
938
939    #[test]
940    fn date1_rejects_non_digit_day() {
941        let mut d = Decoder::new("0x Nov 1994");
942        let err = d.date1().unwrap_err();
943        assert!(
944            err.to_string()
945                .contains("Expected digit at position 1, but found x")
946        );
947    }
948
949    #[test]
950    fn date1_rejects_missing_space_after_day() {
951        let mut d = Decoder::new("06Nov 1994");
952        let err = d.date1().unwrap_err();
953        assert!(err.to_string().contains("but found N"));
954    }
955
956    #[test]
957    fn date1_rejects_invalid_month() {
958        let mut d = Decoder::new("06 Xyz 1994");
959        let err = d.date1().unwrap_err();
960        assert!(err.to_string().contains("Invalid month value: Xyz"));
961    }
962
963    #[test]
964    fn date1_rejects_truncated_year() {
965        let mut d = Decoder::new("06 Nov 19");
966        let err = d.date1().unwrap_err();
967        assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
968    }
969
970    #[test]
971    fn time_parses_valid_time() {
972        let mut d = Decoder::new("08:49:37 GMT");
973        assert_eq!(d.time(), Time::new(8, 49, 37));
974        assert_eq!(d.pos, 8); // consumed "08:49:37"
975    }
976
977    #[test]
978    fn time_rejects_missing_colon() {
979        let mut d = Decoder::new("0849:37");
980        let err = d.time().unwrap_err();
981        assert!(err.to_string().contains("but found 4"));
982    }
983
984    #[test]
985    fn time_rejects_non_digit_minute() {
986        let mut d = Decoder::new("08:x9:37");
987        let err = d.time().unwrap_err();
988        assert!(
989            err.to_string()
990                .contains("Expected digit at position 3, but found x")
991        );
992    }
993
994    #[test]
995    fn time_rejects_truncated_seconds() {
996        let mut d = Decoder::new("08:49:3");
997        let err = d.time().unwrap_err();
998        assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
999    }
1000
1001    // Test cases for decode
1002
1003    fn expect_imf_fixdate(decoded: &HttpDate) -> DateTime {
1004        assert!(decoded.is_imf_fixdate(), "expected ImfFixdate");
1005        *decoded.datetime()
1006    }
1007
1008    fn expect_rfc850(decoded: &HttpDate) -> DateTime {
1009        assert!(decoded.is_rfc850(), "expected Rfc850");
1010        *decoded.datetime()
1011    }
1012
1013    fn expect_asctime(decoded: &HttpDate) -> DateTime {
1014        assert!(decoded.is_asctime(), "expected Asctime");
1015        *decoded.datetime()
1016    }
1017
1018    #[test]
1019    fn decode_parses_imf_fixdate() {
1020        let dt = expect_imf_fixdate(&decode("Sun, 06 Nov 1994 08:49:37 GMT").unwrap());
1021        assert_eq!(dt.dayname, DayName::Sun);
1022        assert_eq!(dt.date, Date::new(1994, 11, 6).unwrap());
1023        assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
1024    }
1025
1026    #[test]
1027    fn decode_parses_rfc850_date() {
1028        let dt = expect_rfc850(&decode("Sunday, 06-Nov-94 08:49:37 GMT").unwrap());
1029        assert_eq!(dt.dayname, DayName::Sun);
1030        assert_eq!(dt.date, Date::new(94, 11, 6).unwrap());
1031        assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
1032    }
1033
1034    #[test]
1035    fn decode_parses_asctime() {
1036        let dt = expect_asctime(&decode("Sun Nov  6 08:49:37 1994").unwrap());
1037        assert_eq!(dt.dayname, DayName::Sun);
1038        assert_eq!(dt.date, Date::new(1994, 11, 6).unwrap());
1039        assert_eq!(dt.time, Time::new(8, 49, 37).unwrap());
1040    }
1041
1042    #[test]
1043    fn decode_parses_all_imf_fixdate_weekdays() {
1044        let cases = [
1045            ("Mon", DayName::Mon),
1046            ("Tue", DayName::Tue),
1047            ("Wed", DayName::Wed),
1048            ("Thu", DayName::Thu),
1049            ("Fri", DayName::Fri),
1050            ("Sat", DayName::Sat),
1051            ("Sun", DayName::Sun),
1052        ];
1053        for (name, dayname) in cases {
1054            let input = format!("{name}, 06 Nov 1994 08:49:37 GMT");
1055            let dt = expect_imf_fixdate(&decode(&input).unwrap());
1056            assert_eq!(dt.dayname, dayname, "weekday {name}");
1057        }
1058    }
1059
1060    #[test]
1061    fn decode_rejects_invalid_dayname() {
1062        let err = decode("Funday, 06 Nov 1994 08:49:37 GMT").expect_err("expected decode to fail");
1063        assert!(err.to_string().contains("Invalid day name: Funday"));
1064    }
1065
1066    #[test]
1067    fn decode_rejects_missing_punctuation() {
1068        // "Sun 06 Nov..." is neither ", " (IMF) nor " " after a short name
1069        // followed by a valid asctime; "Sun 06" fails because a month is
1070        // expected next.
1071        let err = decode("Sun 06 Nov 1994").expect_err("expected decode to fail");
1072        assert!(err.to_string().contains("Invalid month value: 06"));
1073    }
1074
1075    #[test]
1076    fn decode_rejects_imf_fixdate_missing_gmt() {
1077        let err = decode("Sun, 06 Nov 1994 08:49:37 ").expect_err("expected decode to fail");
1078        assert!(err.to_string().contains("Expected 'GMT', but found ''"));
1079    }
1080
1081    #[test]
1082    fn decode_rejects_truncated_input() {
1083        let err = decode("Sun, 06 Nov").expect_err("expected decode to fail");
1084        assert_eq!(err.to_string(), "DecodeError: Unexpected end of input");
1085    }
1086
1087    #[test]
1088    fn decode_rejects_empty_input() {
1089        let err = decode("").expect_err("expected decode to fail");
1090        assert_eq!(err.to_string(), "DecodeError: Invalid day name: ");
1091    }
1092
1093    #[test]
1094    fn decode_rejects_trailing_data() {
1095        for input in [
1096            "Sun, 06 Nov 1994 08:49:37 GMT ",
1097            "Sun, 06 Nov 1994 08:49:37 GMT 123",
1098            "Sun, 06 Nov 1994 08:49:37 GMT.",
1099            "Sunday, 06-Nov-94 08:49:37 GMT extra",
1100            "Sun Nov  6 08:49:37 1994!",
1101        ] {
1102            let err = decode(input)
1103                .err()
1104                .unwrap_or_else(|| panic!("expected {input:?} to be rejected"));
1105            assert!(
1106                err.to_string().contains("Trailing data"),
1107                "{input:?} failed for a different reason: {err}"
1108            );
1109        }
1110    }
1111
1112    #[test]
1113    fn decode_rejects_out_of_range_components() {
1114        for input in [
1115            "Sun, 00 Nov 1994 08:49:37 GMT",
1116            "Sun, 32 Nov 1994 08:49:37 GMT",
1117            "Sunday, 32-Nov-94 08:49:37 GMT",
1118            "Sun Nov 32 08:49:37 1994",
1119            "Sun Nov  0 08:49:37 1994", // asctime single-digit day 0
1120            "Sun, 06 Nov 1994 24:49:37 GMT",
1121            "Sun, 06 Nov 1994 08:60:37 GMT",
1122            "Sun, 06 Nov 1994 08:49:60 GMT",
1123        ] {
1124            let err = decode(input)
1125                .err()
1126                .unwrap_or_else(|| panic!("expected {input:?} to be rejected"));
1127            assert!(
1128                err.to_string().contains("out of range"),
1129                "{input:?} failed for a different reason: {err}"
1130            );
1131        }
1132    }
1133
1134    #[test]
1135    fn decode_accepts_boundary_components() {
1136        assert!(decode("Sun, 31 Nov 1994 08:49:37 GMT").is_ok());
1137        assert!(decode("Sun, 06 Nov 1994 23:59:59 GMT").is_ok());
1138    }
1139
1140    // A fixed valid `DateTime` used by the constructor tests below.
1141    fn sun_nov_6(year: i32) -> DateTime {
1142        DateTime {
1143            dayname: DayName::Sun,
1144            date: Date::new(year, 11, 6).unwrap(),
1145            time: Time::new(8, 49, 37).unwrap(),
1146        }
1147    }
1148
1149    #[test]
1150    fn date_new_rejects_out_of_range() {
1151        assert!(Date::new(1994, 11, 6).is_ok());
1152        assert!(Date::new(-1, 11, 6).is_err());
1153        assert!(Date::new(10_000, 11, 6).is_err());
1154        assert!(Date::new(1994, 0, 6).is_err());
1155        assert!(Date::new(1994, 13, 6).is_err());
1156        assert!(Date::new(1994, 11, 0).is_err());
1157        assert!(Date::new(1994, 11, 32).is_err());
1158    }
1159
1160    #[test]
1161    fn time_new_rejects_out_of_range() {
1162        assert!(Time::new(8, 49, 37).is_ok());
1163        assert!(Time::new(24, 0, 0).is_err());
1164        assert!(Time::new(0, 60, 0).is_err());
1165        assert!(Time::new(0, 0, 60).is_err());
1166        assert!(Time::new(-1, 0, 0).is_err());
1167    }
1168
1169    #[test]
1170    fn rfc850_constructor_rejects_year_above_99() {
1171        assert!(HttpDate::rfc850(sun_nov_6(99)).is_ok());
1172        assert!(HttpDate::rfc850(sun_nov_6(100)).is_err());
1173        assert!(HttpDate::rfc850(sun_nov_6(1994)).is_err());
1174    }
1175
1176    #[test]
1177    fn encode_round_trips_constructed_values() {
1178        let cases = [
1179            HttpDate::imf_fixdate(sun_nov_6(1994)),
1180            HttpDate::rfc850(sun_nov_6(94)).unwrap(),
1181            HttpDate::asctime(sun_nov_6(1994)),
1182        ];
1183        for date in cases {
1184            let s = encode(&date);
1185            assert_eq!(decode(&s).unwrap(), date, "encode produced {s:?}");
1186        }
1187    }
1188}