Skip to main content

xitca_http/
date.rs

1//! low resolution async date time for reduced syscall for generating http date time.
2
3use core::{
4    cell::RefCell,
5    fmt::{self, Write},
6    ops::Deref,
7    time::Duration,
8};
9
10use std::{rc::Rc, time::SystemTime};
11
12use tokio::{
13    task::JoinHandle,
14    time::{Instant, interval},
15};
16
17use crate::http::header::HeaderValue;
18
19// The length of byte representation of HttpDate
20const DATE_VALUE_LENGTH: usize = 29;
21
22const DAY: [&[u8; 3]; 7] = [b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"];
23
24const MONTH: [&[u8; 3]; 12] = [
25    b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov", b"Dec",
26];
27
28/// the fixed bytes of an IMF-fixdate. `Display` overwrites only the variable fields, so
29/// the separators and the trailing `GMT` never have to be written at runtime.
30const TEMPLATE: [u8; DATE_VALUE_LENGTH] = *b"Xxx, 00 Xxx 0000 00:00:00 GMT";
31
32/// write `n` as two ASCII digits at offset `OFF`. `n` is in range at every call site.
33///
34/// taking the whole buffer as a fixed size array turns an out of range `OFF` into a
35/// compile error instead of a runtime panic.
36const fn two_digits<const OFF: usize>(buf: &mut [u8; DATE_VALUE_LENGTH], n: u32) {
37    const {
38        assert!(
39            OFF + 1 < DATE_VALUE_LENGTH,
40            "two_digits would write past the date buffer"
41        )
42    }
43    buf[OFF] = b'0' + (n / 10) as u8;
44    buf[OFF + 1] = b'0' + (n % 10) as u8;
45}
46
47/// [IMF-fixdate] formatting of a [SystemTime].
48///
49/// a server only ever emits this one format and never parses a date, so the full date
50/// handling of a dedicated crate is not needed for it.
51///
52/// [IMF-fixdate]: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7
53struct HttpDate {
54    /// seconds relative to the unix epoch. negative for dates before 1970.
55    secs: i64,
56}
57
58impl From<SystemTime> for HttpDate {
59    fn from(time: SystemTime) -> Self {
60        let secs = match time.duration_since(SystemTime::UNIX_EPOCH) {
61            Ok(dur) => dur.as_secs() as i64,
62            Err(e) => -(e.duration().as_secs() as i64),
63        };
64        Self { secs }
65    }
66}
67
68impl fmt::Display for HttpDate {
69    /// always writes exactly [DATE_VALUE_LENGTH] bytes.
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        // euclidean division keeps the time of day positive for pre epoch timestamps.
72        let days = self.secs.div_euclid(86400);
73        let rem = self.secs.rem_euclid(86400);
74
75        // Howard Hinnant's civil_from_days. the year is shifted to start in march so the
76        // leap day lands at the end of it.
77        let z = days + 719468;
78        let era = z.div_euclid(146097);
79        let doe = z - era * 146097;
80        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
81        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
82        let mp = (5 * doy + 2) / 153;
83        let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
84        let month = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
85        let year = yoe + era * 400 + if month <= 2 { 1 } else { 0 };
86
87        // 1970-01-01 was a thursday, which is index 4 of DAY.
88        let week_day = DAY[(days + 4).rem_euclid(7) as usize];
89
90        let mut buf = TEMPLATE;
91
92        buf[..3].copy_from_slice(week_day);
93        two_digits::<5>(&mut buf, day);
94        buf[8..11].copy_from_slice(MONTH[month as usize - 1]);
95
96        // a year outside four digits is not representable. clamping keeps the output
97        // length fixed, which callers copy into fixed size buffers.
98        let year = year.clamp(0, 9999) as u32;
99        two_digits::<12>(&mut buf, year / 100);
100        two_digits::<14>(&mut buf, year % 100);
101        two_digits::<17>(&mut buf, (rem / 3600) as u32);
102        two_digits::<20>(&mut buf, (rem % 3600 / 60) as u32);
103        two_digits::<23>(&mut buf, (rem % 60) as u32);
104
105        // every byte written above is ASCII, and the whole buffer is written in one call
106        // because `DateTimeState` copies it into a fixed size array.
107        f.write_str(core::str::from_utf8(&buf).expect("date buffer is always ASCII"))
108    }
109}
110
111/// Trait for getting current date/time.
112///
113/// This is usually used by a low resolution of timer to reduce frequent syscall to OS.
114pub trait DateTime {
115    /// The size hint of slice by Self::date method.
116    const DATE_SIZE_HINT: usize = DATE_VALUE_LENGTH;
117
118    /// closure would receive byte slice representation of [HttpDate].
119    fn with_date<F, O>(&self, f: F) -> O
120    where
121        F: FnOnce(&[u8]) -> O;
122
123    fn with_date_header<F, O>(&self, f: F) -> O
124    where
125        F: FnOnce(&HeaderValue) -> O;
126
127    fn now(&self) -> Instant;
128}
129
130/// Struct with Date update periodically at 500 milliseconds interval.
131pub struct DateTimeService {
132    state: Rc<RefCell<DateTimeState>>,
133    handle: JoinHandle<()>,
134}
135
136impl Drop for DateTimeService {
137    fn drop(&mut self) {
138        // stop the timer update async task on drop.
139        self.handle.abort();
140    }
141}
142
143impl Default for DateTimeService {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl DateTimeService {
150    pub fn new() -> Self {
151        // shared date and timer for Date and update async task.
152        let state = Rc::new(RefCell::new(DateTimeState::default()));
153        let state_clone = Rc::clone(&state);
154        // spawn an async task sleep for 1 sec and update date in a loop.
155        // handle is used to stop the task on Date drop.
156        let handle = tokio::task::spawn_local(async move {
157            let mut interval = interval(Duration::from_millis(500));
158            loop {
159                let _ = interval.tick().await;
160                *state_clone.borrow_mut() = DateTimeState::default();
161            }
162        });
163
164        Self { state, handle }
165    }
166
167    #[inline]
168    pub fn get(&self) -> &DateTimeHandle {
169        self.state.deref()
170    }
171}
172
173pub(crate) type DateTimeHandle = RefCell<DateTimeState>;
174
175/// struct contains byte representation of [HttpDate] and [Instant].
176#[derive(Clone)]
177pub struct DateTimeState {
178    pub date: [u8; DATE_VALUE_LENGTH],
179    pub date_header: HeaderValue,
180    pub now: Instant,
181}
182
183impl Default for DateTimeState {
184    fn default() -> Self {
185        let mut date = Self {
186            date: [0; DATE_VALUE_LENGTH],
187            date_header: HeaderValue::from_static(""),
188            now: Instant::now(),
189        };
190        let _ = write!(date, "{}", HttpDate::from(SystemTime::now()));
191        date.date_header = HeaderValue::from_bytes(&date.date).unwrap();
192        date
193    }
194}
195
196impl Write for DateTimeState {
197    fn write_str(&mut self, s: &str) -> fmt::Result {
198        self.date[..].copy_from_slice(s.as_bytes());
199        Ok(())
200    }
201}
202
203impl DateTime for DateTimeHandle {
204    // TODO: remove this allow
205    #[inline]
206    fn with_date<F, O>(&self, f: F) -> O
207    where
208        F: FnOnce(&[u8]) -> O,
209    {
210        let date = self.borrow();
211        f(&date.date[..])
212    }
213
214    #[inline]
215    fn with_date_header<F, O>(&self, f: F) -> O
216    where
217        F: FnOnce(&HeaderValue) -> O,
218    {
219        let date = self.borrow();
220        f(&date.date_header)
221    }
222
223    #[inline(always)]
224    fn now(&self) -> Instant {
225        self.borrow().now
226    }
227}
228
229/// Time handler powered by plain OS system time. useful for testing purpose.
230pub struct SystemTimeDateTimeHandler;
231
232impl DateTime for SystemTimeDateTimeHandler {
233    // TODO: remove this allow
234    #[allow(dead_code)]
235    fn with_date<F, O>(&self, f: F) -> O
236    where
237        F: FnOnce(&[u8]) -> O,
238    {
239        let date = HttpDate::from(SystemTime::now()).to_string();
240        f(date.as_bytes())
241    }
242
243    #[allow(dead_code)]
244    fn with_date_header<F, O>(&self, f: F) -> O
245    where
246        F: FnOnce(&HeaderValue) -> O,
247    {
248        self.with_date(|date| {
249            let val = HeaderValue::from_bytes(date).unwrap();
250            f(&val)
251        })
252    }
253
254    fn now(&self) -> Instant {
255        Instant::now()
256    }
257}
258
259#[cfg(test)]
260mod test {
261    use super::*;
262
263    #[test]
264    fn imf_fixdate_vectors() {
265        let format = |secs: i64| HttpDate { secs }.to_string();
266
267        // the reference timestamp from RFC9110 section 5.6.7.
268        assert_eq!(format(784_111_777), "Sun, 06 Nov 1994 08:49:37 GMT");
269        assert_eq!(format(0), "Thu, 01 Jan 1970 00:00:00 GMT");
270        assert_eq!(format(2_147_483_647), "Tue, 19 Jan 2038 03:14:07 GMT");
271        assert_eq!(format(4_102_444_800), "Fri, 01 Jan 2100 00:00:00 GMT");
272        // 2000 is a leap year, so 29 Feb exists.
273        assert_eq!(format(951_782_400), "Tue, 29 Feb 2000 00:00:00 GMT");
274        assert_eq!(format(-1), "Wed, 31 Dec 1969 23:59:59 GMT");
275    }
276
277    /// `DateTimeState::write_str` copies into a fixed size array, so a formatted date that
278    /// is not exactly [DATE_VALUE_LENGTH] bytes would panic at runtime.
279    #[test]
280    fn formatted_length_is_always_the_date_value_length() {
281        // roughly every 11 hours from 1970 to 2118, plus every second of one day.
282        let steps = (0..100_000).map(|i| i * 40_000).chain(0..86_400);
283        for secs in steps {
284            let date = HttpDate { secs }.to_string();
285            assert_eq!(date.len(), DATE_VALUE_LENGTH, "{date}");
286        }
287    }
288
289    #[test]
290    fn date_time_state_default_is_a_valid_header() {
291        let state = DateTimeState::default();
292        assert_eq!(state.date.len(), DATE_VALUE_LENGTH);
293        assert_eq!(state.date_header.as_bytes(), &state.date[..]);
294        assert!(state.date.ends_with(b" GMT"));
295    }
296}