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//! it moves to `parse-net` once http-net depends on it, and the cache stays
7//! here.
8
9use core::fmt;
10
11/// The last second IMF-fixdate can show: 9999-12-31T23:59:59Z.
12const MAX_SECONDS: u64 = 253_402_300_799;
13
14/// A formatted `Date` value: an IMF-fixdate such as
15/// `Sun, 06 Nov 1994 08:49:37 GMT` (always 29 bytes).
16///
17/// # Examples
18///
19/// ```
20/// use http_net::HttpDate;
21///
22/// let d = HttpDate::from_unix_seconds(784_111_777);
23/// assert_eq!(d.as_str(), "Sun, 06 Nov 1994 08:49:37 GMT");
24/// assert_eq!(d.as_bytes().len(), 29);
25/// ```
26#[derive(Clone, Copy, PartialEq, Eq, Hash)]
27pub struct HttpDate([u8; 29]);
28
29impl HttpDate {
30    /// Formats `secs` seconds after the Unix epoch. Times after the year
31    /// 9999 are shown as its last second, the latest the format can hold.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use http_net::HttpDate;
37    ///
38    /// assert_eq!(HttpDate::from_unix_seconds(0).as_str(), "Thu, 01 Jan 1970 00:00:00 GMT");
39    /// assert_eq!(HttpDate::from_unix_seconds(u64::MAX).as_str(), "Fri, 31 Dec 9999 23:59:59 GMT");
40    /// ```
41    #[must_use]
42    pub const fn from_unix_seconds(secs: u64) -> Self {
43        const WEEKDAYS: [&[u8; 3]; 7] = [b"Thu", b"Fri", b"Sat", b"Sun", b"Mon", b"Tue", b"Wed"];
44        const MONTHS: [&[u8; 3]; 12] = [
45            b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
46            b"Dec",
47        ];
48        let secs = if secs > MAX_SECONDS {
49            MAX_SECONDS
50        } else {
51            secs
52        };
53        let days = secs / 86_400;
54        let rem = secs % 86_400;
55        let (year, month, day) = civil_from_days(days);
56        let mut out = *b"Thu, 01 Jan 1970 00:00:00 GMT";
57        copy3(&mut out, 0, WEEKDAYS[(days % 7) as usize]);
58        two_digits(&mut out, 5, day);
59        copy3(&mut out, 8, MONTHS[(month - 1) as usize]);
60        two_digits(&mut out, 12, year / 100);
61        two_digits(&mut out, 14, year % 100);
62        two_digits(&mut out, 17, rem / 3600);
63        two_digits(&mut out, 20, rem % 3600 / 60);
64        two_digits(&mut out, 23, rem % 60);
65        Self(out)
66    }
67
68    /// The 29 bytes of the value.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// assert!(http_net::HttpDate::from_unix_seconds(0).as_bytes().ends_with(b" GMT"));
74    /// ```
75    #[must_use]
76    pub const fn as_bytes(&self) -> &[u8; 29] {
77        &self.0
78    }
79
80    /// The value as text.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// assert!(http_net::HttpDate::from_unix_seconds(0).as_str().starts_with("Thu, "));
86    /// ```
87    #[must_use]
88    pub fn as_str(&self) -> &str {
89        // Built only from ASCII digits, names, and separators.
90        core::str::from_utf8(&self.0).unwrap_or("")
91    }
92}
93
94impl fmt::Debug for HttpDate {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.debug_tuple("HttpDate").field(&self.as_str()).finish()
97    }
98}
99
100impl fmt::Display for HttpDate {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(self.as_str())
103    }
104}
105
106/// A `Date` value reformatted at most once per second.
107///
108/// A server writes `date` on every response; formatting it once per second
109/// instead of once per response saves the calendar arithmetic on all but the
110/// first response of each second. Keep one per thread or per connection
111/// driver: it is plain data, not shared state.
112///
113/// # Examples
114///
115/// ```
116/// use http_net::DateCache;
117///
118/// let mut cache = DateCache::new();
119/// assert_eq!(cache.update(784_111_777).as_str(), "Sun, 06 Nov 1994 08:49:37 GMT");
120/// // Same second: the stored value, no formatting.
121/// assert_eq!(cache.update(784_111_777).as_str(), "Sun, 06 Nov 1994 08:49:37 GMT");
122/// assert_eq!(cache.update(784_111_778).as_str(), "Sun, 06 Nov 1994 08:49:38 GMT");
123/// ```
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub struct DateCache {
126    /// The second `date` shows; `None` before the first update.
127    second: Option<u64>,
128    date: HttpDate,
129}
130
131impl Default for DateCache {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl DateCache {
138    /// An empty cache; [`current`](Self::current) shows the epoch until the
139    /// first [`update`](Self::update).
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// let cache = http_net::DateCache::new();
145    /// assert_eq!(cache.current().as_str(), "Thu, 01 Jan 1970 00:00:00 GMT");
146    /// ```
147    #[must_use]
148    pub const fn new() -> Self {
149        Self {
150            second: None,
151            date: HttpDate::from_unix_seconds(0),
152        }
153    }
154
155    /// The value for `unix_seconds`, reformatted only when the second
156    /// differs from the last update.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// let mut cache = http_net::DateCache::new();
162    /// assert!(cache.update(1_000_000_000).as_str().contains("2001"));
163    /// ```
164    pub fn update(&mut self, unix_seconds: u64) -> &HttpDate {
165        if self.second != Some(unix_seconds) {
166            self.date = HttpDate::from_unix_seconds(unix_seconds);
167            self.second = Some(unix_seconds);
168        }
169        &self.date
170    }
171
172    /// The value from the last update.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// let mut cache = http_net::DateCache::new();
178    /// let _ = cache.update(86_400);
179    /// assert_eq!(cache.current().as_str(), "Fri, 02 Jan 1970 00:00:00 GMT");
180    /// ```
181    #[must_use]
182    pub const fn current(&self) -> &HttpDate {
183        &self.date
184    }
185}
186
187const fn copy3(dst: &mut [u8; 29], at: usize, src: &[u8; 3]) {
188    dst[at] = src[0];
189    dst[at + 1] = src[1];
190    dst[at + 2] = src[2];
191}
192
193const fn two_digits(dst: &mut [u8; 29], at: usize, v: u64) {
194    dst[at] = b'0' + (v / 10 % 10) as u8;
195    dst[at + 1] = b'0' + (v % 10) as u8;
196}
197
198/// Days since 1970-01-01 to (year, month 1..=12, day 1..=31), after Howard
199/// Hinnant's `civil_from_days`, for dates on or after the epoch.
200const fn civil_from_days(days: u64) -> (u64, u64, u64) {
201    let z = days + 719_468;
202    let era = z / 146_097;
203    let doe = z - era * 146_097;
204    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
205    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
206    let mp = (5 * doy + 2) / 153;
207    let day = doy - (153 * mp + 2) / 5 + 1;
208    let month = if mp < 10 { mp + 3 } else { mp - 9 };
209    let year = yoe + era * 400 + (month <= 2) as u64;
210    (year, month, day)
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_http_date_known_values() {
219        for (secs, text) in [
220            (0, "Thu, 01 Jan 1970 00:00:00 GMT"),
221            (784_111_777, "Sun, 06 Nov 1994 08:49:37 GMT"),
222            (951_782_400, "Tue, 29 Feb 2000 00:00:00 GMT"),
223            (1_791_072_000, "Sun, 04 Oct 2026 00:00:00 GMT"),
224            (4_107_542_400, "Mon, 01 Mar 2100 00:00:00 GMT"),
225            (MAX_SECONDS, "Fri, 31 Dec 9999 23:59:59 GMT"),
226            (MAX_SECONDS + 1, "Fri, 31 Dec 9999 23:59:59 GMT"),
227        ] {
228            assert_eq!(HttpDate::from_unix_seconds(secs).as_str(), text, "{secs}");
229        }
230    }
231
232    /// The calendar matches a day-by-day walk from the epoch, so no month or
233    /// leap-year boundary between 1970 and 2400 is off by one.
234    #[test]
235    fn test_civil_from_days_matches_a_day_by_day_walk() {
236        let (mut y, mut m, mut d) = (1970u64, 1u64, 1u64);
237        for days in 0..157_000u64 {
238            assert_eq!(civil_from_days(days), (y, m, d), "day {days}");
239            let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
240            let len = match m {
241                2 if leap => 29,
242                2 => 28,
243                4 | 6 | 9 | 11 => 30,
244                _ => 31,
245            };
246            d += 1;
247            if d > len {
248                d = 1;
249                m += 1;
250                if m > 12 {
251                    m = 1;
252                    y += 1;
253                }
254            }
255        }
256    }
257
258    #[test]
259    fn test_date_cache_reformats_only_on_a_new_second() {
260        let mut cache = DateCache::new();
261        let a = *cache.update(10);
262        assert_eq!(cache.second, Some(10));
263        assert_eq!(*cache.update(10), a);
264        let b = *cache.update(11);
265        assert_ne!(a, b);
266        // Going back in time is a new second too.
267        assert_eq!(*cache.update(10), a);
268        assert_eq!(cache.current(), &a);
269    }
270}