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