1use core::fmt;
12
13const MAX_SECONDS: u64 = 253_402_300_799;
15
16#[derive(Clone, Copy, PartialEq, Eq, Hash)]
29pub struct HttpDate([u8; 29]);
30
31impl HttpDate {
32 #[must_use]
44 pub const fn from_unix_seconds(secs: u64) -> Self {
45 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 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 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 #[must_use]
82 pub const fn as_bytes(&self) -> &[u8; 29] {
83 &self.0
84 }
85
86 #[must_use]
94 pub fn as_str(&self) -> &str {
95 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct DateCache {
132 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 #[must_use]
154 pub const fn new() -> Self {
155 Self {
156 second: None,
157 date: HttpDate::from_unix_seconds(0),
158 }
159 }
160
161 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 #[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 #[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 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 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 #[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 assert_eq!(*cache.update(10), a);
346 assert_eq!(cache.current(), &a);
347 }
348}