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