Skip to main content

parse_net/
date.rs

1//! The proleptic Gregorian calendar and HTTP-date (RFC 9110 section 5.6.7).
2//!
3//! # Examples
4//!
5//! ```
6//! use parse_net as pn;
7//!
8//! let d = pn::HttpDate::parse_imf_fixdate(b"Sun, 06 Nov 1994 08:49:37 GMT")?;
9//! assert_eq!(d.unix(), 784_111_777);
10//! assert_eq!(&d.format(), b"Sun, 06 Nov 1994 08:49:37 GMT");
11//!
12//! let now = 1_792_540_800; // 2026-10-21
13//! assert_eq!(pn::http_date_at(b"Sun Nov  6 08:49:37 1994", now)?, d);
14//! assert_eq!(pn::http_date_at(b"Wednesday, 01-Jan-76 00:00:00 GMT", now)?.civil().year, 2076);
15//! assert_eq!(pn::http_date_at(b"Saturday, 01-Jan-77 00:00:00 GMT", now)?.civil().year, 1977);
16//! # Ok::<(), pn::ParseError>(())
17//! ```
18
19use crate::{
20    error::{ErrorKind, Result, WriteError, err},
21    limits::Limits,
22    text::trim_ows,
23    write::Sink,
24};
25
26/// Whether `year` is a leap year in the proleptic Gregorian calendar.
27///
28/// # Examples
29///
30/// ```
31/// use parse_net::date::is_leap_year;
32///
33/// assert!(is_leap_year(2000) && is_leap_year(2024) && !is_leap_year(1900) && !is_leap_year(2023));
34/// ```
35#[must_use]
36pub const fn is_leap_year(year: i32) -> bool {
37    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
38}
39
40/// The number of days in `month` (1 to 12) of `year`; 0 for any other month.
41///
42/// # Examples
43///
44/// ```
45/// use parse_net::date::days_in_month;
46///
47/// assert_eq!(days_in_month(2024, 2), 29);
48/// assert_eq!(days_in_month(2023, 2), 28);
49/// assert_eq!(days_in_month(2023, 13), 0);
50/// ```
51#[must_use]
52pub const fn days_in_month(year: i32, month: u8) -> u8 {
53    match month {
54        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
55        4 | 6 | 9 | 11 => 30,
56        2 if is_leap_year(year) => 29,
57        2 => 28,
58        _ => 0,
59    }
60}
61
62/// Days since 1970-01-01 for a civil date, after Howard Hinnant's
63/// `days_from_civil`.
64///
65/// The input is not validated: pair it with [`days_in_month`]. Any `i32`
66/// year and `u8` month and day give a result without overflow.
67///
68/// # Examples
69///
70/// ```
71/// use parse_net::date::days_from_civil;
72///
73/// assert_eq!(days_from_civil(1970, 1, 1), 0);
74/// assert_eq!(days_from_civil(2000, 3, 1), 11_017);
75/// assert_eq!(days_from_civil(1, 1, 1), -719_162);
76/// ```
77#[must_use]
78pub const fn days_from_civil(year: i32, month: u8, day: u8) -> i64 {
79    let m = month as i64;
80    let y = if m <= 2 { year as i64 - 1 } else { year as i64 };
81    let era = if y >= 0 { y } else { y - 399 } / 400;
82    let yoe = y - era * 400;
83    let mp = (m + 9) % 12;
84    let doy = (153 * mp + 2) / 5 + day as i64 - 1;
85    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
86    era * 146_097 + doe - 719_468
87}
88
89/// The day count beyond which `civil_from_days` clamps, keeping every year
90/// inside `i32`.
91const DAYS_LIMIT: i64 = 784_352_296_671;
92
93/// The civil date `(year, month, day)` for a day count since 1970-01-01,
94/// after Howard Hinnant's `civil_from_days`.
95///
96/// Accepts any `i64`; day counts beyond the years an `i32` can hold are
97/// clamped to them.
98///
99/// # Examples
100///
101/// ```
102/// use parse_net::date::civil_from_days;
103///
104/// assert_eq!(civil_from_days(0), (1970, 1, 1));
105/// assert_eq!(civil_from_days(-1), (1969, 12, 31));
106/// assert_eq!(civil_from_days(19_782), (2024, 2, 29));
107/// ```
108#[must_use]
109pub const fn civil_from_days(days: i64) -> (i32, u8, u8) {
110    let days = if days > DAYS_LIMIT {
111        DAYS_LIMIT
112    } else if days < -DAYS_LIMIT {
113        -DAYS_LIMIT
114    } else {
115        days
116    };
117    let z = days + 719_468;
118    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
119    let doe = z - era * 146_097;
120    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
121    let y = yoe + era * 400;
122    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
123    let mp = (5 * doy + 2) / 153;
124    let d = doy - (153 * mp + 2) / 5 + 1;
125    let m = if mp < 10 { mp + 3 } else { mp - 9 };
126    let y = if m <= 2 { y + 1 } else { y };
127    let y = if y > i32::MAX as i64 {
128        i32::MAX
129    } else if y < i32::MIN as i64 {
130        i32::MIN
131    } else {
132        y as i32
133    };
134    // `m` is 1 to 12 and `d` is 1 to 31 by construction.
135    (y, m as u8, d as u8)
136}
137
138/// The ISO 8601 weekday (1 = Monday ... 7 = Sunday) of a day count since
139/// 1970-01-01.
140///
141/// # Examples
142///
143/// ```
144/// assert_eq!(parse_net::date::weekday_from_days(0), 4); // a Thursday
145/// ```
146#[must_use]
147pub const fn weekday_from_days(days: i64) -> u8 {
148    // 1970-01-01 was a Thursday (4); the result is 1 to 7.
149    ((days.rem_euclid(7) + 3) % 7 + 1) as u8
150}
151
152/// A broken-down UTC time.
153///
154/// # Examples
155///
156/// ```
157/// let c = parse_net::HttpDate::from_unix(784_111_777)?.civil();
158/// assert_eq!((c.year, c.month, c.day, c.hour, c.minute, c.second, c.weekday), (1994, 11, 6, 8, 49, 37, 7));
159/// # Ok::<(), parse_net::ParseError>(())
160/// ```
161#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
162pub struct Civil {
163    /// The year, 1 to 9999.
164    pub year: u16,
165    /// The month, 1 to 12.
166    pub month: u8,
167    /// The day of the month, 1 to 31.
168    pub day: u8,
169    /// The hour, 0 to 23.
170    pub hour: u8,
171    /// The minute, 0 to 59.
172    pub minute: u8,
173    /// The second, 0 to 59.
174    pub second: u8,
175    /// The ISO 8601 weekday: 1 = Monday ... 7 = Sunday.
176    pub weekday: u8,
177}
178
179/// The HTTP-date format a value was written in.
180#[non_exhaustive]
181#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
182pub enum DateFormat {
183    /// `Sun, 06 Nov 1994 08:49:37 GMT`, the only format senders may generate.
184    ImfFixdate,
185    /// `Sunday, 06-Nov-94 08:49:37 GMT` (obsolete).
186    Rfc850,
187    /// `Sun Nov  6 08:49:37 1994` (obsolete).
188    Asctime,
189}
190
191const SECS_PER_DAY: i64 = 86_400;
192const MIN_SECS: i64 = days_from_civil(1, 1, 1) * SECS_PER_DAY;
193const MAX_SECS: i64 = days_from_civil(9999, 12, 31) * SECS_PER_DAY + SECS_PER_DAY - 1;
194
195const DAY_NAMES: [&[u8; 3]; 7] = [b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat", b"Sun"];
196const LONG_DAY_NAMES: [&[u8]; 7] = [
197    b"Monday",
198    b"Tuesday",
199    b"Wednesday",
200    b"Thursday",
201    b"Friday",
202    b"Saturday",
203    b"Sunday",
204];
205const MONTH_NAMES: [&[u8; 3]; 12] = [
206    b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov", b"Dec",
207];
208
209/// A point in time with one-second resolution, from year 1 to year 9999,
210/// UTC; the value of HTTP-date fields (`Date`, `Last-Modified`, `Expires`).
211///
212/// # Examples
213///
214/// ```
215/// use parse_net::HttpDate;
216///
217/// let d = HttpDate::from_unix(0)?;
218/// assert_eq!(&d.format(), b"Thu, 01 Jan 1970 00:00:00 GMT");
219/// assert!(HttpDate::from_unix(HttpDate::MAX.unix() + 1).is_err());
220/// # Ok::<(), parse_net::ParseError>(())
221/// ```
222#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
223pub struct HttpDate {
224    secs: i64,
225}
226
227impl HttpDate {
228    /// 0001-01-01T00:00:00Z.
229    pub const MIN: Self = Self { secs: MIN_SECS };
230    /// 9999-12-31T23:59:59Z.
231    pub const MAX: Self = Self { secs: MAX_SECS };
232
233    /// The date `secs` seconds after the Unix epoch (negative before 1970).
234    ///
235    /// # Errors
236    ///
237    /// [`ErrorKind::OutOfRange`] outside years 1 to 9999.
238    pub const fn from_unix(secs: i64) -> Result<Self> {
239        if secs < MIN_SECS || secs > MAX_SECS {
240            return err(ErrorKind::OutOfRange, 0);
241        }
242        Ok(Self { secs })
243    }
244
245    /// Seconds since the Unix epoch.
246    #[must_use]
247    pub const fn unix(self) -> i64 {
248        self.secs
249    }
250
251    /// The broken-down UTC time.
252    #[must_use]
253    pub fn civil(self) -> Civil {
254        let days = self.secs.div_euclid(SECS_PER_DAY);
255        let rem = self.secs.rem_euclid(SECS_PER_DAY);
256        let (year, month, day) = civil_from_days(days);
257        Civil {
258            // Years are 1 to 9999 by the type's invariant.
259            year: u16::try_from(year).unwrap_or(0),
260            month,
261            day,
262            hour: (rem / 3600) as u8,
263            minute: (rem % 3600 / 60) as u8,
264            second: (rem % 60) as u8,
265            weekday: weekday_from_days(days),
266        }
267    }
268
269    /// The IMF-fixdate text, `Sun, 06 Nov 1994 08:49:37 GMT`.
270    #[must_use]
271    pub fn format(self) -> [u8; 29] {
272        let c = self.civil();
273        let mut out = *b"Thu, 01 Jan 1970 00:00:00 GMT";
274        let name = DAY_NAMES
275            .get(usize::from(c.weekday).saturating_sub(1))
276            .copied()
277            .unwrap_or(b"Mon");
278        let month = MONTH_NAMES
279            .get(usize::from(c.month).saturating_sub(1))
280            .copied()
281            .unwrap_or(b"Jan");
282        put(&mut out, 0, name);
283        two(&mut out, 5, c.day);
284        put(&mut out, 8, month);
285        two(&mut out, 12, (c.year / 100) as u8);
286        two(&mut out, 14, (c.year % 100) as u8);
287        two(&mut out, 17, c.hour);
288        two(&mut out, 20, c.minute);
289        two(&mut out, 23, c.second);
290        out
291    }
292
293    /// Writes [`format`](Self::format) into `out`.
294    ///
295    /// # Errors
296    ///
297    /// [`WriteError::Full`].
298    pub fn write_to(self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
299        out.put(&self.format())?;
300        Ok(())
301    }
302
303    /// Parses exactly an IMF-fixdate (29 bytes after OWS), the only format
304    /// senders may generate. Needs no clock.
305    ///
306    /// # Errors
307    ///
308    /// [`ErrorKind::InvalidDate`], [`ErrorKind::WeekdayMismatch`],
309    /// [`ErrorKind::OutOfRange`] (year 0000), [`ErrorKind::TrailingData`].
310    pub fn parse_imf_fixdate(value: &[u8]) -> Result<Self> {
311        let (lead, v) = trimmed(value);
312        imf_fixdate(v, &Limits::DEFAULT).map_err(|e| e.shifted(lead))
313    }
314
315    /// Parses an HTTP-date in any of its three formats.
316    ///
317    /// The RFC 850 two-digit year is resolved against `now` (Unix seconds):
318    /// a date more than 50 years after `now` is taken as the most recent past
319    /// year with the same last two digits, and a date 50 years or more before
320    /// `now` moves forward a century, so the year lands in the hundred years
321    /// around `now`. Names are case-sensitive, only `GMT` is accepted, and
322    /// the weekday must agree with the date.
323    ///
324    /// # Errors
325    ///
326    /// [`ErrorKind::InvalidDate`], [`ErrorKind::WeekdayMismatch`],
327    /// [`ErrorKind::OutOfRange`], [`ErrorKind::TrailingData`].
328    pub fn parse_at(value: &[u8], now: i64) -> Result<Self> {
329        Self::parse_detailed_with(value, now, &Limits::DEFAULT).map(|(d, _)| d)
330    }
331
332    /// As [`parse_at`](Self::parse_at), with [`Limits::allow_leap_second`]
333    /// taken from `limits`.
334    ///
335    /// # Errors
336    ///
337    /// As [`parse_at`](Self::parse_at).
338    pub fn parse_at_with(value: &[u8], now: i64, limits: &Limits) -> Result<Self> {
339        Self::parse_detailed_with(value, now, limits).map(|(d, _)| d)
340    }
341
342    /// As [`parse_at`](Self::parse_at), also reporting the format seen.
343    ///
344    /// # Errors
345    ///
346    /// As [`parse_at`](Self::parse_at).
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use parse_net::{DateFormat, HttpDate};
352    ///
353    /// let (_, fmt) = HttpDate::parse_detailed(b"Sunday, 06-Nov-94 08:49:37 GMT", 1_792_540_800)?;
354    /// assert_eq!(fmt, DateFormat::Rfc850);
355    /// # Ok::<(), parse_net::ParseError>(())
356    /// ```
357    pub fn parse_detailed(value: &[u8], now: i64) -> Result<(Self, DateFormat)> {
358        Self::parse_detailed_with(value, now, &Limits::DEFAULT)
359    }
360
361    fn parse_detailed_with(value: &[u8], now: i64, limits: &Limits) -> Result<(Self, DateFormat)> {
362        let (lead, v) = trimmed(value);
363        let result = match (v.get(3), v.iter().position(|&b| b == b',')) {
364            (Some(b','), _) => imf_fixdate(v, limits).map(|d| (d, DateFormat::ImfFixdate)),
365            (Some(b' '), _) => asctime(v, limits).map(|d| (d, DateFormat::Asctime)),
366            (_, Some(_)) => rfc850(v, now, limits).map(|d| (d, DateFormat::Rfc850)),
367            _ => err(ErrorKind::InvalidDate, 0),
368        };
369        result.map_err(|e| e.shifted(lead))
370    }
371
372    /// The current time from the system clock, clamped to the supported
373    /// range.
374    #[cfg(feature = "std")]
375    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
376    #[must_use]
377    pub fn now() -> Self {
378        match Self::from_system_time(std::time::SystemTime::now()) {
379            Ok(d) => d,
380            Err(_) => Self::MAX,
381        }
382    }
383
384    /// The date of a `SystemTime`, truncated to whole seconds toward the
385    /// past.
386    ///
387    /// # Errors
388    ///
389    /// [`ErrorKind::OutOfRange`] outside years 1 to 9999.
390    #[cfg(feature = "std")]
391    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
392    pub fn from_system_time(t: std::time::SystemTime) -> Result<Self> {
393        let secs = match t.duration_since(std::time::UNIX_EPOCH) {
394            Ok(d) => i64::try_from(d.as_secs()).unwrap_or(i64::MAX),
395            Err(before) => {
396                let d = before.duration();
397                let whole = i64::try_from(d.as_secs()).unwrap_or(i64::MAX);
398                let extra = i64::from(d.subsec_nanos() > 0);
399                0i64.saturating_sub(whole).saturating_sub(extra)
400            }
401        };
402        Self::from_unix(secs)
403    }
404
405    /// The date as a `SystemTime`, or `None` when the platform's clock type
406    /// cannot represent it (Windows cannot go before the year 1601).
407    #[cfg(feature = "std")]
408    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
409    #[must_use]
410    pub fn to_system_time(self) -> Option<std::time::SystemTime> {
411        let d = std::time::Duration::from_secs(self.secs.unsigned_abs());
412        if self.secs >= 0 {
413            std::time::UNIX_EPOCH.checked_add(d)
414        } else {
415            std::time::UNIX_EPOCH.checked_sub(d)
416        }
417    }
418}
419
420/// Parses an HTTP-date in any format, resolving RFC 850 years against `now`.
421///
422/// The Tier 1 form for `no_std` callers, who pass the clock in.
423///
424/// # Errors
425///
426/// As [`HttpDate::parse_at`].
427///
428/// # Examples
429///
430/// ```
431/// let d = parse_net::http_date_at(b"Sun, 06 Nov 1994 08:49:37 GMT", 1_792_540_800)?;
432/// assert_eq!(d.unix(), 784_111_777);
433/// # Ok::<(), parse_net::ParseError>(())
434/// ```
435pub fn http_date_at(value: &[u8], now: i64) -> Result<HttpDate> {
436    HttpDate::parse_at(value, now)
437}
438
439/// Parses an HTTP-date in any format, resolving RFC 850 years against the
440/// system clock.
441///
442/// # Errors
443///
444/// As [`HttpDate::parse_at`].
445///
446/// # Examples
447///
448/// ```
449/// let d = parse_net::http_date(b"Sun, 06 Nov 1994 08:49:37 GMT")?;
450/// assert_eq!(d.unix(), 784_111_777);
451/// # Ok::<(), parse_net::ParseError>(())
452/// ```
453#[cfg(feature = "std")]
454#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
455pub fn http_date(value: &[u8]) -> Result<HttpDate> {
456    HttpDate::parse_at(value, HttpDate::now().unix())
457}
458
459fn trimmed(value: &[u8]) -> (usize, &[u8]) {
460    let lead = crate::text::skip_ows(value);
461    (lead, trim_ows(value))
462}
463
464fn put(out: &mut [u8], at: usize, bytes: &[u8]) {
465    if let Some(dst) = out.get_mut(at..at + bytes.len()) {
466        dst.copy_from_slice(bytes);
467    }
468}
469
470fn two(out: &mut [u8], at: usize, v: u8) {
471    put(out, at, &[b'0' + v / 10 % 10, b'0' + v % 10]);
472}
473
474/// The value of `n` ASCII digits at `v[at..]`.
475fn digits(v: &[u8], at: usize, n: usize) -> Result<u32> {
476    let Some(ds) = v.get(at..at + n) else {
477        return err(ErrorKind::InvalidDate, v.len().min(at));
478    };
479    let mut x = 0u32;
480    for (i, &d) in ds.iter().enumerate() {
481        if !d.is_ascii_digit() {
482            return err(ErrorKind::InvalidDate, at + i);
483        }
484        x = x * 10 + u32::from(d - b'0');
485    }
486    Ok(x)
487}
488
489fn expect(v: &[u8], at: usize, lit: &[u8]) -> Result<()> {
490    match v.get(at..at + lit.len()) {
491        Some(s) if s == lit => Ok(()),
492        _ => err(ErrorKind::InvalidDate, at),
493    }
494}
495
496/// The index (0 to 11) of the case-sensitive month name at `v[at..at + 3]`.
497fn month(v: &[u8], at: usize) -> Result<u8> {
498    let name = v.get(at..at + 3).unwrap_or(&[]);
499    match MONTH_NAMES.iter().position(|m| m.as_slice() == name) {
500        // There are 12 months.
501        Some(i) => Ok(i as u8 + 1),
502        None => err(ErrorKind::InvalidDate, at),
503    }
504}
505
506/// The ISO weekday of a short day name at `v[0..3]`.
507fn short_day(v: &[u8]) -> Result<u8> {
508    let name = v.get(..3).unwrap_or(&[]);
509    match DAY_NAMES.iter().position(|d| d.as_slice() == name) {
510        Some(i) => Ok(i as u8 + 1),
511        None => err(ErrorKind::InvalidDate, 0),
512    }
513}
514
515/// `hh:mm:ss` at `v[at..at + 8]`.
516fn time(v: &[u8], at: usize, limits: &Limits) -> Result<i64> {
517    let h = digits(v, at, 2)?;
518    expect(v, at + 2, b":")?;
519    let m = digits(v, at + 3, 2)?;
520    expect(v, at + 5, b":")?;
521    let mut s = digits(v, at + 6, 2)?;
522    if h > 23 {
523        return err(ErrorKind::InvalidDate, at);
524    }
525    if m > 59 {
526        return err(ErrorKind::InvalidDate, at + 3);
527    }
528    if s == 60 && limits.allow_leap_second {
529        s = 59;
530    }
531    if s > 59 {
532        return err(ErrorKind::InvalidDate, at + 6);
533    }
534    Ok(i64::from(h * 3600 + m * 60 + s))
535}
536
537/// Builds the date and checks it exists and matches `weekday`.
538fn assemble(
539    year: u32,
540    month: u8,
541    day: u32,
542    secs: i64,
543    weekday: u8,
544    year_at: usize,
545    day_at: usize,
546) -> Result<HttpDate> {
547    if year == 0 || year > 9999 {
548        return err(ErrorKind::OutOfRange, year_at);
549    }
550    // `year` is 1 to 9999, which fits an i32.
551    let y = year as i32;
552    if day == 0 || day > u32::from(days_in_month(y, month)) {
553        return err(ErrorKind::InvalidDate, day_at);
554    }
555    let days = days_from_civil(y, month, day as u8);
556    if weekday_from_days(days) != weekday {
557        return err(ErrorKind::WeekdayMismatch, 0);
558    }
559    HttpDate::from_unix(days * SECS_PER_DAY + secs)
560}
561
562fn trailing(v: &[u8], len: usize) -> Result<()> {
563    if v.len() > len {
564        return err(ErrorKind::TrailingData, len);
565    }
566    Ok(())
567}
568
569/// `Sun, 06 Nov 1994 08:49:37 GMT`.
570fn imf_fixdate(v: &[u8], limits: &Limits) -> Result<HttpDate> {
571    let weekday = short_day(v)?;
572    expect(v, 3, b", ")?;
573    let day = digits(v, 5, 2)?;
574    expect(v, 7, b" ")?;
575    let m = month(v, 8)?;
576    expect(v, 11, b" ")?;
577    let year = digits(v, 12, 4)?;
578    expect(v, 16, b" ")?;
579    let secs = time(v, 17, limits)?;
580    expect(v, 25, b" GMT")?;
581    trailing(v, 29)?;
582    assemble(year, m, day, secs, weekday, 12, 5)
583}
584
585/// `Sun Nov  6 08:49:37 1994`.
586fn asctime(v: &[u8], limits: &Limits) -> Result<HttpDate> {
587    let weekday = short_day(v)?;
588    expect(v, 3, b" ")?;
589    let m = month(v, 4)?;
590    expect(v, 7, b" ")?;
591    let day = if v.get(8) == Some(&b' ') {
592        digits(v, 9, 1)?
593    } else {
594        digits(v, 8, 2)?
595    };
596    expect(v, 10, b" ")?;
597    let secs = time(v, 11, limits)?;
598    expect(v, 19, b" ")?;
599    let year = digits(v, 20, 4)?;
600    trailing(v, 24)?;
601    assemble(year, m, day, secs, weekday, 20, 8)
602}
603
604/// `Sunday, 06-Nov-94 08:49:37 GMT`.
605fn rfc850(v: &[u8], now: i64, limits: &Limits) -> Result<HttpDate> {
606    let comma = v.iter().position(|&b| b == b',').unwrap_or(0);
607    let name = v.get(..comma).unwrap_or(&[]);
608    let Some(day_index) = LONG_DAY_NAMES.iter().position(|d| *d == name) else {
609        return err(ErrorKind::InvalidDate, 0);
610    };
611    let weekday = day_index as u8 + 1;
612    let at = comma + 2;
613    expect(v, comma, b", ")?;
614    let day = digits(v, at, 2)?;
615    expect(v, at + 2, b"-")?;
616    let m = month(v, at + 3)?;
617    expect(v, at + 6, b"-")?;
618    let yy = digits(v, at + 7, 2)?;
619    expect(v, at + 9, b" ")?;
620    let secs = time(v, at + 10, limits)?;
621    expect(v, at + 18, b" GMT")?;
622    trailing(v, at + 22)?;
623    let year = resolve_two_digit_year(yy, m, day, secs, now);
624    assemble(year, m, day, secs, weekday, at + 7, at)
625}
626
627/// RFC 9110 section 5.6.7: a two-digit year more than 50 years in the future
628/// is the most recent past year with the same last two digits. The year is
629/// first taken in `now`'s century, then moved so it lies after `now` minus
630/// 50 years and no later than `now` plus 50 years (D-037).
631fn resolve_two_digit_year(yy: u32, month: u8, day: u32, secs: i64, now: i64) -> u32 {
632    let now = HttpDate::from_unix(now.clamp(MIN_SECS, MAX_SECS))
633        .unwrap_or(HttpDate::MIN)
634        .civil();
635    let now_key = (
636        u32::from(now.month),
637        u32::from(now.day),
638        i64::from(now.hour) * 3600 + i64::from(now.minute) * 60 + i64::from(now.second),
639    );
640    let key = (u32::from(month), day, secs);
641    let now_year = u32::from(now.year);
642    let mut year = now_year - now_year % 100 + yy;
643    // More than 50 years ahead of now: a century back.
644    if (year, key) > (now_year + 50, now_key) {
645        year = year.saturating_sub(100);
646    } else if (year, key) <= (now_year.saturating_sub(50), now_key) {
647        year += 100;
648    }
649    year
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    const NOW_2026: i64 = 1_792_540_800; // 2026-10-21T00:00:00Z
657
658    fn at(v: &[u8]) -> Result<HttpDate> {
659        HttpDate::parse_at(v, NOW_2026)
660    }
661
662    #[test]
663    fn test_calendar_round_trips_every_day_of_four_centuries() {
664        let start = days_from_civil(1600, 1, 1);
665        for days in start..start + 146_097 {
666            let (y, m, d) = civil_from_days(days);
667            assert_eq!(days_from_civil(y, m, d), days);
668            assert!(d >= 1 && d <= days_in_month(y, m));
669        }
670        assert_eq!(civil_from_days(i64::MAX).0, civil_from_days(DAYS_LIMIT).0);
671        assert_eq!(civil_from_days(i64::MIN).0, civil_from_days(-DAYS_LIMIT).0);
672    }
673
674    #[test]
675    fn test_weekday_known_dates() {
676        assert_eq!(weekday_from_days(days_from_civil(1994, 11, 6)), 7);
677        assert_eq!(weekday_from_days(days_from_civil(2000, 1, 1)), 6);
678        assert_eq!(weekday_from_days(days_from_civil(1, 1, 1)), 1);
679        assert_eq!(weekday_from_days(-1), 3);
680    }
681
682    #[test]
683    fn test_http_date_rfc_examples_all_formats() {
684        let d = HttpDate::from_unix(784_111_777).unwrap();
685        assert_eq!(at(b"Sun, 06 Nov 1994 08:49:37 GMT").unwrap(), d);
686        assert_eq!(at(b"Sunday, 06-Nov-94 08:49:37 GMT").unwrap(), d);
687        assert_eq!(at(b"Sun Nov  6 08:49:37 1994").unwrap(), d);
688        assert_eq!(at(b"  Sun, 06 Nov 1994 08:49:37 GMT \t").unwrap(), d);
689    }
690
691    #[test]
692    fn test_http_date_rejections_table() {
693        use ErrorKind::*;
694        let cases: &[(&[u8], ErrorKind)] = &[
695            (b"Mon, 06 Nov 1994 08:49:37 GMT", WeekdayMismatch),
696            (b"sun, 06 Nov 1994 08:49:37 GMT", InvalidDate),
697            (b"Sun, 06 nov 1994 08:49:37 GMT", InvalidDate),
698            (b"Sun, 06 Nov 1994 08:49:37 UTC", InvalidDate),
699            (b"Sun, 06 Nov 1994 08:49:37 +0000", InvalidDate),
700            (b"Fri, 29 Feb 2023 00:00:00 GMT", InvalidDate),
701            (b"Thu, 31 Apr 2025 00:00:00 GMT", InvalidDate),
702            (b"Sun, 06 Nov 1994 24:00:00 GMT", InvalidDate),
703            (b"Sun, 06 Nov 1994 23:60:00 GMT", InvalidDate),
704            (b"Sun, 06 Nov 1994 23:59:60 GMT", InvalidDate),
705            (b"Sat, 01 Jan 0000 00:00:00 GMT", OutOfRange),
706            (b"Sun, 06 Nov 1994 08:49:37 GMTx", TrailingData),
707            (b"Sun, 06 Nov 1994 08:49:37 GM", InvalidDate),
708            (b"Sun, 6 Nov 1994 08:49:37 GMT", InvalidDate),
709            (b"Sun Nov 6 08:49:37 1994", InvalidDate),
710            (b"Sunday, 06-Nov-1994 08:49:37 GMT", InvalidDate),
711            (b"Sun, 00 Nov 1994 08:49:37 GMT", InvalidDate),
712            (b"", InvalidDate),
713            (b"garbage", InvalidDate),
714            (b"Sun, 06 Nov 1994 08:49:37 GMT, x", TrailingData),
715        ];
716        for (v, kind) in cases {
717            assert_eq!(
718                at(v).unwrap_err().kind(),
719                *kind,
720                "{}",
721                std::string::String::from_utf8_lossy(v)
722            );
723        }
724    }
725
726    #[test]
727    fn test_http_date_leap_year_and_leap_second() {
728        assert!(at(b"Thu, 29 Feb 2024 00:00:00 GMT").is_ok());
729        let v = b"Sun, 06 Nov 1994 08:49:60 GMT";
730        assert!(at(v).is_err());
731        let l = Limits::DEFAULT.allow_leap_second(true);
732        let d = HttpDate::parse_at_with(v, NOW_2026, &l).unwrap();
733        assert_eq!(d.civil().second, 59);
734    }
735
736    #[test]
737    fn test_http_date_range_ends() {
738        assert_eq!(&HttpDate::MAX.format(), b"Fri, 31 Dec 9999 23:59:59 GMT");
739        assert_eq!(&HttpDate::MIN.format(), b"Mon, 01 Jan 0001 00:00:00 GMT");
740        assert_eq!(at(b"Fri, 31 Dec 9999 23:59:59 GMT").unwrap(), HttpDate::MAX);
741        assert_eq!(at(b"Mon, 01 Jan 0001 00:00:00 GMT").unwrap(), HttpDate::MIN);
742        assert!(HttpDate::from_unix(MIN_SECS - 1).is_err());
743        assert!(HttpDate::from_unix(MAX_SECS + 1).is_err());
744        assert_eq!(MIN_SECS, -62_135_596_800);
745        assert_eq!(MAX_SECS, 253_402_300_799);
746    }
747
748    #[test]
749    fn test_rfc850_fifty_year_rule() {
750        let y = |v: &[u8]| at(v).unwrap().civil().year;
751        assert_eq!(y(b"Wednesday, 01-Jan-76 00:00:00 GMT"), 2076);
752        assert_eq!(y(b"Saturday, 01-Jan-77 00:00:00 GMT"), 1977);
753        assert_eq!(y(b"Wednesday, 21-Oct-76 00:00:00 GMT"), 2076);
754        assert_eq!(y(b"Wednesday, 21-Oct-26 00:00:00 GMT"), 2026);
755        assert_eq!(y(b"Tuesday, 01-Jan-80 00:00:00 GMT"), 1980);
756        // At the turn of a century the window still centres on now.
757        let now_2099 = HttpDate::parse_imf_fixdate(b"Thu, 01 Jan 2099 00:00:00 GMT")
758            .unwrap()
759            .unix();
760        let d = HttpDate::parse_at(b"Friday, 01-Jan-00 00:00:00 GMT", now_2099).unwrap();
761        assert_eq!(d.civil().year, 2100);
762        let d = HttpDate::parse_at(b"Saturday, 01-Jan-50 00:00:00 GMT", now_2099).unwrap();
763        assert_eq!(d.civil().year, 2050);
764    }
765
766    #[test]
767    fn test_asctime_day_forms() {
768        assert!(at(b"Wed Nov 16 08:49:37 1994").is_ok());
769        assert_eq!(
770            at(b"Sun Nov  6 08:49:37 1994 ").unwrap().unix(),
771            784_111_777
772        );
773        assert_eq!(
774            at(b"Sun Nov  6 08:49:37 1994x").unwrap_err().kind(),
775            ErrorKind::TrailingData
776        );
777    }
778
779    #[test]
780    fn test_format_parse_round_trip_sampled() {
781        let mut secs = MIN_SECS;
782        while secs < MAX_SECS {
783            let d = HttpDate::from_unix(secs).unwrap();
784            assert_eq!(HttpDate::parse_imf_fixdate(&d.format()).unwrap(), d);
785            secs += 7_919_993; // a prime step covers every weekday and time of day
786        }
787    }
788
789    #[cfg(feature = "std")]
790    #[test]
791    fn test_system_time_conversions() {
792        use std::time::{Duration, UNIX_EPOCH};
793        let t = UNIX_EPOCH + Duration::from_millis(1500);
794        assert_eq!(HttpDate::from_system_time(t).unwrap().unix(), 1);
795        let before = UNIX_EPOCH - Duration::from_millis(1500);
796        assert_eq!(HttpDate::from_system_time(before).unwrap().unix(), -2);
797        let d = HttpDate::from_unix(784_111_777).unwrap();
798        assert_eq!(
799            HttpDate::from_system_time(d.to_system_time().unwrap()).unwrap(),
800            d
801        );
802        assert!(HttpDate::now() > d);
803        assert_eq!(http_date(b"Sun, 06 Nov 1994 08:49:37 GMT").unwrap(), d);
804    }
805}