Skip to main content

http_net/
date.rs

1//! The `Date` field: IMF-fixdate formatting and a per-second cache.
2//!
3//! The core has no clock (it is sans-IO and `no_std`): the caller supplies
4//! the time as Unix seconds. The blocking adapter wraps the system clock in
5//! `blocking::now_date`. The formatting grammar is RFC 9110 section 5.6.7:
6//! the civil calendar (days to year, month, day, and weekday) is
7//! `parse-net`'s, the 29-byte layout and the cache stay here (dev/PLAN.md,
8//! D-59). parse-net's own `HttpDate::format` writes the same bytes but is not
9//! a `const fn`, and `HttpDate::from_unix_seconds` and `DateCache::new` are.
10
11use core::fmt;
12
13/// The last second IMF-fixdate can show: 9999-12-31T23:59:59Z.
14const MAX_SECONDS: u64 = 253_402_300_799;
15
16/// A formatted `Date` value: an IMF-fixdate such as
17/// `Sun, 06 Nov 1994 08:49:37 GMT` (always 29 bytes).
18///
19/// # Examples
20///
21/// ```
22/// use http_net::HttpDate;
23///
24/// let d = HttpDate::from_unix_seconds(784_111_777);
25/// assert_eq!(d.as_str(), "Sun, 06 Nov 1994 08:49:37 GMT");
26/// assert_eq!(d.as_bytes().len(), 29);
27/// ```
28#[derive(Clone, Copy, PartialEq, Eq, Hash)]
29pub struct HttpDate([u8; 29]);
30
31impl HttpDate {
32    /// Formats `secs` seconds after the Unix epoch. Times after the year
33    /// 9999 are shown as its last second, the latest the format can hold.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use http_net::HttpDate;
39    ///
40    /// assert_eq!(HttpDate::from_unix_seconds(0).as_str(), "Thu, 01 Jan 1970 00:00:00 GMT");
41    /// assert_eq!(HttpDate::from_unix_seconds(u64::MAX).as_str(), "Fri, 31 Dec 9999 23:59:59 GMT");
42    /// ```
43    #[must_use]
44    pub const fn from_unix_seconds(secs: u64) -> Self {
45        // ISO 8601 order, as parse-net's `weekday_from_days` counts (1 = Monday).
46        const WEEKDAYS: [&[u8; 3]; 7] = [b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat", b"Sun"];
47        const MONTHS: [&[u8; 3]; 12] = [
48            b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
49            b"Dec",
50        ];
51        let secs = if secs > MAX_SECONDS {
52            MAX_SECONDS
53        } else {
54            secs
55        };
56        // At most 2,932,896 days after the clamp, so the cast is exact.
57        let days = (secs / 86_400) as i64;
58        let rem = secs % 86_400;
59        let (year, month, day) = parse_net::date::civil_from_days(days);
60        let weekday = parse_net::date::weekday_from_days(days);
61        let mut out = *b"Thu, 01 Jan 1970 00:00:00 GMT";
62        copy3(&mut out, 0, WEEKDAYS[(weekday - 1) as usize]);
63        two_digits(&mut out, 5, day as u64);
64        copy3(&mut out, 8, MONTHS[(month - 1) as usize]);
65        // Years 1970 to 9999 after the clamp.
66        two_digits(&mut out, 12, year as u64 / 100);
67        two_digits(&mut out, 14, year as u64 % 100);
68        two_digits(&mut out, 17, rem / 3600);
69        two_digits(&mut out, 20, rem % 3600 / 60);
70        two_digits(&mut out, 23, rem % 60);
71        Self(out)
72    }
73
74    /// The 29 bytes of the value.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// assert!(http_net::HttpDate::from_unix_seconds(0).as_bytes().ends_with(b" GMT"));
80    /// ```
81    #[must_use]
82    pub const fn as_bytes(&self) -> &[u8; 29] {
83        &self.0
84    }
85
86    /// The value as text.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// assert!(http_net::HttpDate::from_unix_seconds(0).as_str().starts_with("Thu, "));
92    /// ```
93    #[must_use]
94    pub fn as_str(&self) -> &str {
95        // Built only from ASCII digits, names, and separators.
96        core::str::from_utf8(&self.0).unwrap_or("")
97    }
98}
99
100impl fmt::Debug for HttpDate {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_tuple("HttpDate").field(&self.as_str()).finish()
103    }
104}
105
106impl fmt::Display for HttpDate {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.write_str(self.as_str())
109    }
110}
111
112/// A `Date` value reformatted at most once per second.
113///
114/// A server writes `date` on every response; formatting it once per second
115/// instead of once per response saves the calendar arithmetic on all but the
116/// first response of each second. Keep one per thread or per connection
117/// driver: it is plain data, not shared state.
118///
119/// # Examples
120///
121/// ```
122/// use http_net::DateCache;
123///
124/// let mut cache = DateCache::new();
125/// assert_eq!(cache.update(784_111_777).as_str(), "Sun, 06 Nov 1994 08:49:37 GMT");
126/// // Same second: the stored value, no formatting.
127/// assert_eq!(cache.update(784_111_777).as_str(), "Sun, 06 Nov 1994 08:49:37 GMT");
128/// assert_eq!(cache.update(784_111_778).as_str(), "Sun, 06 Nov 1994 08:49:38 GMT");
129/// ```
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct DateCache {
132    /// The second `date` shows; `None` before the first update.
133    second: Option<u64>,
134    date: HttpDate,
135}
136
137impl Default for DateCache {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl DateCache {
144    /// An empty cache; [`current`](Self::current) shows the epoch until the
145    /// first [`update`](Self::update).
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// let cache = http_net::DateCache::new();
151    /// assert_eq!(cache.current().as_str(), "Thu, 01 Jan 1970 00:00:00 GMT");
152    /// ```
153    #[must_use]
154    pub const fn new() -> Self {
155        Self {
156            second: None,
157            date: HttpDate::from_unix_seconds(0),
158        }
159    }
160
161    /// The value for `unix_seconds`, reformatted only when the second
162    /// differs from the last update.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// let mut cache = http_net::DateCache::new();
168    /// assert!(cache.update(1_000_000_000).as_str().contains("2001"));
169    /// ```
170    pub fn update(&mut self, unix_seconds: u64) -> &HttpDate {
171        if self.second != Some(unix_seconds) {
172            self.date = HttpDate::from_unix_seconds(unix_seconds);
173            self.second = Some(unix_seconds);
174        }
175        &self.date
176    }
177
178    /// The value from the last update.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// let mut cache = http_net::DateCache::new();
184    /// let _ = cache.update(86_400);
185    /// assert_eq!(cache.current().as_str(), "Fri, 02 Jan 1970 00:00:00 GMT");
186    /// ```
187    #[must_use]
188    pub const fn current(&self) -> &HttpDate {
189        &self.date
190    }
191}
192
193const fn copy3(dst: &mut [u8; 29], at: usize, src: &[u8; 3]) {
194    dst[at] = src[0];
195    dst[at + 1] = src[1];
196    dst[at + 2] = src[2];
197}
198
199const fn two_digits(dst: &mut [u8; 29], at: usize, v: u64) {
200    dst[at] = b'0' + (v / 10 % 10) as u8;
201    dst[at + 1] = b'0' + (v % 10) as u8;
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_http_date_known_values() {
210        for (secs, text) in [
211            (0, "Thu, 01 Jan 1970 00:00:00 GMT"),
212            (784_111_777, "Sun, 06 Nov 1994 08:49:37 GMT"),
213            (951_782_400, "Tue, 29 Feb 2000 00:00:00 GMT"),
214            (1_791_072_000, "Sun, 04 Oct 2026 00:00:00 GMT"),
215            (4_107_542_400, "Mon, 01 Mar 2100 00:00:00 GMT"),
216            (MAX_SECONDS, "Fri, 31 Dec 9999 23:59:59 GMT"),
217            (MAX_SECONDS + 1, "Fri, 31 Dec 9999 23:59:59 GMT"),
218        ] {
219            assert_eq!(HttpDate::from_unix_seconds(secs).as_str(), text, "{secs}");
220        }
221    }
222
223    /// The calendar matches a day-by-day walk from the epoch, so no month or
224    /// leap-year boundary between 1970 and 2400 is off by one.
225    #[test]
226    fn test_civil_from_days_matches_a_day_by_day_walk() {
227        let (mut y, mut m, mut d) = (1970u64, 1u64, 1u64);
228        for days in 0..157_000u64 {
229            let (py, pm, pd) = parse_net::date::civil_from_days(days as i64);
230            assert_eq!((py as u64, pm as u64, pd as u64), (y, m, d), "day {days}");
231            let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
232            let len = match m {
233                2 if leap => 29,
234                2 => 28,
235                4 | 6 | 9 | 11 => 30,
236                _ => 31,
237            };
238            d += 1;
239            if d > len {
240                d = 1;
241                m += 1;
242                if m > 12 {
243                    m = 1;
244                    y += 1;
245                }
246            }
247        }
248    }
249
250    /// The http-net 0.7 formatter, copied verbatim as the reference for the
251    /// section 7.9 equality test.
252    mod old {
253        use super::super::{MAX_SECONDS, copy3, two_digits};
254
255        pub(super) const fn from_unix_seconds(secs: u64) -> [u8; 29] {
256            const WEEKDAYS: [&[u8; 3]; 7] =
257                [b"Thu", b"Fri", b"Sat", b"Sun", b"Mon", b"Tue", b"Wed"];
258            const MONTHS: [&[u8; 3]; 12] = [
259                b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct",
260                b"Nov", b"Dec",
261            ];
262            let secs = if secs > MAX_SECONDS {
263                MAX_SECONDS
264            } else {
265                secs
266            };
267            let days = secs / 86_400;
268            let rem = secs % 86_400;
269            let (year, month, day) = civil_from_days(days);
270            let mut out = *b"Thu, 01 Jan 1970 00:00:00 GMT";
271            copy3(&mut out, 0, WEEKDAYS[(days % 7) as usize]);
272            two_digits(&mut out, 5, day);
273            copy3(&mut out, 8, MONTHS[(month - 1) as usize]);
274            two_digits(&mut out, 12, year / 100);
275            two_digits(&mut out, 14, year % 100);
276            two_digits(&mut out, 17, rem / 3600);
277            two_digits(&mut out, 20, rem % 3600 / 60);
278            two_digits(&mut out, 23, rem % 60);
279            out
280        }
281
282        /// Days since 1970-01-01 to (year, month 1..=12, day 1..=31), after Howard
283        /// Hinnant's `civil_from_days`, for dates on or after the epoch.
284        pub(super) const fn civil_from_days(days: u64) -> (u64, u64, u64) {
285            let z = days + 719_468;
286            let era = z / 146_097;
287            let doe = z - era * 146_097;
288            let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
289            let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
290            let mp = (5 * doy + 2) / 153;
291            let day = doy - (153 * mp + 2) / 5 + 1;
292            let month = if mp < 10 { mp + 3 } else { mp - 9 };
293            let year = yoe + era * 400 + (month <= 2) as u64;
294            (year, month, day)
295        }
296    }
297
298    /// Every day from 1970 to 9999, at a second that moves through every
299    /// hour, minute, and second value: the new formatter writes the bytes
300    /// the 0.7 one wrote, and parse-net's `HttpDate::format` agrees.
301    #[test]
302    fn test_formatter_matches_07_on_every_day_to_9999() {
303        let last_day = MAX_SECONDS / 86_400;
304        for day in 0..=last_day {
305            let secs = day * 86_400 + (day * 7_919) % 86_400;
306            let new = *HttpDate::from_unix_seconds(secs).as_bytes();
307            assert_eq!(new, old::from_unix_seconds(secs), "{secs}");
308            let pn = parse_net::HttpDate::from_unix(secs as i64).map(|d| d.format());
309            assert_eq!(pn, Ok(new), "{secs}");
310        }
311        for secs in [
312            0,
313            1,
314            86_399,
315            MAX_SECONDS - 1,
316            MAX_SECONDS,
317            MAX_SECONDS + 1,
318            u64::MAX / 2,
319            u64::MAX,
320        ] {
321            assert_eq!(
322                *HttpDate::from_unix_seconds(secs).as_bytes(),
323                old::from_unix_seconds(secs),
324                "{secs}"
325            );
326        }
327        for secs in 0..86_400 * 2 {
328            assert_eq!(
329                *HttpDate::from_unix_seconds(secs).as_bytes(),
330                old::from_unix_seconds(secs),
331                "{secs}"
332            );
333        }
334    }
335
336    #[test]
337    fn test_date_cache_reformats_only_on_a_new_second() {
338        let mut cache = DateCache::new();
339        let a = *cache.update(10);
340        assert_eq!(cache.second, Some(10));
341        assert_eq!(*cache.update(10), a);
342        let b = *cache.update(11);
343        assert_ne!(a, b);
344        // Going back in time is a new second too.
345        assert_eq!(*cache.update(10), a);
346        assert_eq!(cache.current(), &a);
347    }
348}