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}