Skip to main content

deep_time/
wire.rs

1use crate::{
2    Drift, Dt, Every, LiteStr, Meridiem, Offset, Scale, Spacetime, TimeParts, TimeRange, Weekday,
3};
4
5impl Dt {
6    /// Current wire format version.
7    pub const WIRE_VERSION: u8 = 1;
8
9    /// Size of the canonical wire representation in bytes.
10    pub const WIRE_SIZE: usize = 19;
11
12    /// Serializes this `Dt` into a fixed 18-byte little-endian buffer using the
13    /// `attos: i128` + `scale: Scale` representation.
14    ///
15    /// ## Wire Format
16    ///
17    /// - Byte `0`: Version (`WIRE_VERSION`)
18    /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
19    /// - Byte `17`: scale as `u8` (enum discriminant)
20    /// - Byte `18`: target as `u8` (enum discriminant)
21    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
22        let mut buf = [0u8; Self::WIRE_SIZE];
23        buf[0] = Self::WIRE_VERSION;
24        buf[1..17].copy_from_slice(&self.attos.to_le_bytes());
25        buf[17] = self.target as u8;
26        buf
27    }
28
29    /// Deserializes a [`Dt`] from exactly 18 bytes of wire data.
30    ///
31    /// Returns `None` if the version byte is unknown, the length is wrong,
32    /// or the scale byte is not a valid `Scale` variant.
33    ///
34    /// ## Wire Format
35    ///
36    /// - Byte `0`: Version (`WIRE_VERSION`)
37    /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
38    /// - Byte `17`: scale as `u8` (enum discriminant)
39    /// - Byte `18`: target as `u8` (enum discriminant)
40    ///
41    /// ## Security
42    ///
43    /// Safe to call with completely untrusted input. Fixed-size format,
44    /// no allocation, no `unsafe`, and no possibility of code execution.
45    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
46        if bytes.len() != Self::WIRE_SIZE {
47            return None;
48        }
49
50        if bytes[0] != Self::WIRE_VERSION {
51            return None;
52        }
53
54        let attos = i128::from_le_bytes([
55            bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
56            bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
57        ]);
58
59        let scale = Scale::from_u8(bytes[17]);
60        let target = Scale::from_u8(bytes[18]);
61
62        Some(Dt::new(attos, scale, target))
63    }
64}
65
66impl Drift {
67    /// Current wire format version.
68    pub const WIRE_VERSION: u8 = 1;
69
70    /// Size of the canonical wire representation in bytes.
71    pub const WIRE_SIZE: usize = 3 * Dt::WIRE_SIZE; // 3 × 17 = 51
72
73    /// Serializes this `Drift` polynomial into a fixed buffer.
74    ///
75    /// The layout is the concatenation of the three `Dt` fields.
76    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
77        let mut buf = [0u8; Self::WIRE_SIZE];
78        let c = self.constant.to_wire_bytes();
79        let r = self.rate.to_wire_bytes();
80        let a = self.accel.to_wire_bytes();
81
82        buf[0..Dt::WIRE_SIZE].copy_from_slice(&c);
83        buf[Dt::WIRE_SIZE..2 * Dt::WIRE_SIZE].copy_from_slice(&r);
84        buf[2 * Dt::WIRE_SIZE..].copy_from_slice(&a);
85        buf
86    }
87
88    /// Deserializes a `Drift` from exactly `WIRE_SIZE` bytes of wire data.
89    ///
90    /// Returns `None` if any nested `Dt` fails validation or if the version
91    /// byte is unknown.
92    ///
93    /// ## Security
94    ///
95    /// Composes the safety guarantees of
96    /// [`from_wire_bytes`](docs.rs/deep-time/latest/deep_time/struct.Dt.html#method.from_wire_bytes).
97    ///
98    /// Fixed size and layered validation make it safe for untrusted input.
99    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
100        if bytes.len() != Self::WIRE_SIZE {
101            return None;
102        }
103
104        if bytes[0] != Self::WIRE_VERSION {
105            return None;
106        }
107
108        let constant = Dt::from_wire_bytes(&bytes[0..Dt::WIRE_SIZE])?;
109        let rate = Dt::from_wire_bytes(&bytes[Dt::WIRE_SIZE..2 * Dt::WIRE_SIZE])?;
110        let accel = Dt::from_wire_bytes(&bytes[2 * Dt::WIRE_SIZE..])?;
111
112        Some(Self::new(constant, rate, accel))
113    }
114}
115
116impl Spacetime {
117    /// Size of the canonical wire representation in bytes (24 bytes).
118    pub const WIRE_SIZE: usize = 24;
119
120    /// Serializes this `Spacetime` snapshot into a fixed 24-byte buffer.
121    ///
122    /// All fields are stored as little-endian IEEE 754 `f64`.
123    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
124        let mut buf = [0u8; Self::WIRE_SIZE];
125        buf[0..8].copy_from_slice(&self.alpha.to_le_bytes());
126        buf[8..16].copy_from_slice(&self.beta.to_le_bytes());
127        buf[16..24].copy_from_slice(&self.kretschmann.to_le_bytes());
128        buf
129    }
130
131    /// Deserializes a `Spacetime` from exactly 24 bytes.
132    ///
133    /// ## Security
134    ///
135    /// Accepts any `f64` bit pattern (including `NaN`/`Inf`) to match the
136    /// type’s own invariants. Fixed size makes it immune to length-based
137    /// attacks. Safe for untrusted input.
138    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
139        if bytes.len() != Self::WIRE_SIZE {
140            return None;
141        }
142        let alpha = f64::from_le_bytes([
143            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
144        ]);
145        let beta = f64::from_le_bytes([
146            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
147        ]);
148        let kretschmann = f64::from_le_bytes([
149            bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
150        ]);
151        Some(Self {
152            alpha,
153            beta,
154            kretschmann,
155        })
156    }
157}
158
159impl Every {
160    /// Size of the canonical wire representation in bytes (33 bytes).
161    pub const WIRE_SIZE: usize = Dt::WIRE_SIZE + Dt::WIRE_SIZE;
162
163    /// Serializes this `Every` builder into a fixed 33-byte buffer.
164    ///
165    /// The layout is simply the concatenation of `start` (17 bytes) and `step` (16 bytes).
166    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
167        let mut buf = [0u8; Self::WIRE_SIZE];
168        let start = self.start.to_wire_bytes();
169        let step = self.step.to_wire_bytes();
170        buf[0..17].copy_from_slice(&start);
171        buf[17..33].copy_from_slice(&step);
172        buf
173    }
174
175    /// Deserializes an `Every` builder from exactly 33 bytes.
176    ///
177    /// ## Security
178    ///
179    /// Safe for untrusted input. Fixed size with strict validation
180    /// of the inner `Dt` and `Dt`.
181    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
182        if bytes.len() != Self::WIRE_SIZE {
183            return None;
184        }
185        let start = Dt::from_wire_bytes(&bytes[0..17])?;
186        let step = Dt::from_wire_bytes(&bytes[17..33])?;
187        Some(Self { start, step })
188    }
189}
190
191impl TimeRange {
192    /// Current wire format version.
193    pub const WIRE_VERSION: u8 = 1;
194
195    /// Size of the canonical wire representation in bytes.
196    /// Only the logical definition is stored (runtime state is not serialized).
197    pub const WIRE_SIZE: usize = 1 + 2 * Dt::WIRE_SIZE + Dt::WIRE_SIZE + 1;
198
199    /// Serializes this `TimeRange` into a fixed buffer.
200    ///
201    /// Only the logical definition is stored:
202    /// - `start` + `end` + `step` + `inclusive` flag
203    ///
204    /// Runtime iterator state (`current`, `finished`) is **not** serialized.
205    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
206        let mut buf = [0u8; Self::WIRE_SIZE];
207        buf[0] = Self::WIRE_VERSION;
208
209        let start = self.start.to_wire_bytes();
210        let end = self.end.to_wire_bytes();
211        let step = self.step.to_wire_bytes();
212
213        let tp_size = Dt::WIRE_SIZE;
214        let span_size = Dt::WIRE_SIZE;
215
216        buf[1..1 + tp_size].copy_from_slice(&start);
217        buf[1 + tp_size..1 + 2 * tp_size].copy_from_slice(&end);
218        buf[1 + 2 * tp_size..1 + 2 * tp_size + span_size].copy_from_slice(&step);
219        buf[1 + 2 * tp_size + span_size] = if self.inclusive { 1 } else { 0 };
220
221        buf
222    }
223
224    /// Deserializes a `TimeRange` from exactly `WIRE_SIZE` bytes.
225    ///
226    /// The iterator is reconstructed in its initial state
227    /// (`current = start`, `finished = false`).
228    ///
229    /// Returns `None` if the version is unknown or any component is invalid.
230    ///
231    /// ## Security
232    ///
233    /// Safe for untrusted input. Fixed size with layered validation
234    /// of all inner types. No runtime iterator state is accepted from the wire.
235    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
236        if bytes.len() != Self::WIRE_SIZE {
237            return None;
238        }
239
240        if bytes[0] != Self::WIRE_VERSION {
241            return None;
242        }
243
244        let tp_size = Dt::WIRE_SIZE;
245        let span_size = Dt::WIRE_SIZE;
246
247        let start = Dt::from_wire_bytes(&bytes[1..1 + tp_size])?;
248        let end = Dt::from_wire_bytes(&bytes[1 + tp_size..1 + 2 * tp_size])?;
249        let step = Dt::from_wire_bytes(&bytes[1 + 2 * tp_size..1 + 2 * tp_size + span_size])?;
250        let inclusive = bytes[1 + 2 * tp_size + span_size] != 0;
251
252        Some(Self::new(start, end, step, inclusive))
253    }
254}
255
256impl Meridiem {
257    pub const WIRE_SIZE: usize = 1;
258
259    #[inline]
260    pub const fn to_wire_byte(self) -> u8 {
261        match self {
262            Meridiem::AM => 0,
263            Meridiem::PM => 1,
264        }
265    }
266
267    #[inline]
268    pub const fn from_wire_byte(b: u8) -> Option<Self> {
269        match b {
270            0 => Some(Meridiem::AM),
271            1 => Some(Meridiem::PM),
272            _ => None,
273        }
274    }
275}
276
277impl Offset {
278    pub const WIRE_SIZE: usize = 5; // tag (1) + i32 (4)
279
280    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
281        let mut buf = [0u8; Self::WIRE_SIZE];
282        match self {
283            Offset::Utc => buf[0] = 0,
284            Offset::None => buf[0] = 1,
285            Offset::Fixed(offset) => {
286                buf[0] = 2;
287                buf[1..5].copy_from_slice(&offset.to_le_bytes());
288            }
289        }
290        buf
291    }
292
293    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
294        if bytes.len() != Self::WIRE_SIZE {
295            return None;
296        }
297        match bytes[0] {
298            0 => Some(Offset::Utc),
299            1 => Some(Offset::None),
300            2 => {
301                let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
302                Some(Offset::Fixed(offset))
303            }
304            _ => None,
305        }
306    }
307}
308
309impl Weekday {
310    pub const WIRE_SIZE: usize = 1;
311
312    #[inline]
313    pub const fn to_wire_byte(self) -> u8 {
314        self.wkday_sun_0_based()
315    }
316
317    #[inline]
318    pub const fn from_wire_byte(b: u8) -> Option<Self> {
319        Self::from_sunday_0_based(b)
320    }
321}
322
323impl TimeParts {
324    /// Current wire format version.
325    pub const WIRE_VERSION: u8 = 1;
326
327    /// Total size of the wire representation (119 bytes).
328    pub const WIRE_SIZE: usize = 119;
329
330    /// Serializes `TimeParts` into a fixed 119-byte buffer.
331    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
332        let mut buf = [0u8; Self::WIRE_SIZE];
333        buf[0] = Self::WIRE_VERSION;
334
335        let mut offset = 1usize;
336
337        // year (sentinel = i64::MIN)
338        let year = self.yr.unwrap_or(i64::MIN);
339        buf[offset..offset + 8].copy_from_slice(&year.to_le_bytes());
340        offset += 8;
341
342        // month
343        buf[offset] = self.mo.unwrap_or(u8::MAX);
344        offset += 1;
345
346        // day
347        buf[offset] = self.day.unwrap_or(u8::MAX);
348        offset += 1;
349
350        // hour
351        buf[offset] = self.hr;
352        offset += 1;
353
354        // minute
355        buf[offset] = self.min;
356        offset += 1;
357
358        // second
359        buf[offset] = self.sec;
360        offset += 1;
361
362        // attos
363        let attos = self.attos;
364        buf[offset..offset + 8].copy_from_slice(&attos.to_le_bytes());
365        offset += 8;
366
367        // offset (5 bytes)
368        let offset_bytes = self.offset.unwrap_or_default().to_wire_bytes();
369        buf[offset..offset + 5].copy_from_slice(&offset_bytes);
370        offset += 5;
371
372        // iana_name (49 bytes)
373        if let Some(name) = &self.iana_name {
374            let name_bytes = name.bytes;
375            buf[offset..offset + 49].copy_from_slice(&name_bytes);
376        }
377        offset += 49;
378
379        // scale
380        buf[offset] = self.scale as u8;
381        offset += 1;
382
383        // weekday
384        buf[offset] = self.wkday.map_or(255, |w| w.to_wire_byte());
385        offset += 1;
386
387        // day_of_year
388        let doy = self.day_of_yr.unwrap_or(u16::MAX);
389        buf[offset..offset + 2].copy_from_slice(&doy.to_le_bytes());
390        offset += 2;
391
392        // iso_week_year
393        let iso_y = self.iso_wk_yr.unwrap_or(i64::MIN);
394        buf[offset..offset + 8].copy_from_slice(&iso_y.to_le_bytes());
395        offset += 8;
396
397        // iso_week
398        buf[offset] = self.iso_wk.unwrap_or(u8::MAX);
399        offset += 1;
400
401        // week_sun
402        buf[offset] = self.wk_sun.unwrap_or(u8::MAX);
403        offset += 1;
404
405        // week_mon
406        buf[offset] = self.wk_mon.unwrap_or(u8::MAX);
407        offset += 1;
408
409        // meridiem
410        buf[offset] = self.meridiem.map_or(255, |m| m.to_wire_byte());
411        offset += 1;
412
413        // unix_timestamp_seconds
414        let unix = self.unix_timestamp_seconds.unwrap_or(i64::MIN);
415        buf[offset..offset + 8].copy_from_slice(&unix.to_le_bytes());
416
417        buf
418    }
419
420    /// Deserializes `TimeParts` from exactly 119 bytes.
421    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
422        if bytes.len() != Self::WIRE_SIZE {
423            return None;
424        }
425        if bytes[0] != Self::WIRE_VERSION {
426            return None;
427        }
428
429        let mut dc = TimeParts::default();
430        let mut offset = 1usize;
431
432        // year (8 bytes)
433        let year = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
434        if year != i64::MIN {
435            dc.yr = Some(year);
436        }
437        offset += 8;
438
439        // month (1 byte)
440        let m = bytes[offset];
441        if m != u8::MAX {
442            dc.mo = Some(m);
443        }
444        offset += 1;
445
446        // day (1 byte)
447        let d = bytes[offset];
448        if d != u8::MAX {
449            dc.day = Some(d);
450        }
451        offset += 1;
452
453        // hour (1 byte)
454        dc.hr = bytes[offset];
455        offset += 1;
456
457        // minute (1 byte)
458        dc.min = bytes[offset];
459        offset += 1;
460
461        // second (1 byte)
462        dc.sec = bytes[offset];
463        offset += 1;
464
465        // attos (8 bytes)
466        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
467        dc.attos = attos;
468        offset += 8;
469
470        // offset (5 bytes)
471        if let Some(off) = Offset::from_wire_bytes(&bytes[offset..offset + 5]) {
472            dc.offset = Some(off);
473        }
474        offset += 5;
475
476        // iana_name (49 bytes)
477        let iana_bytes = &bytes[offset..offset + 49];
478        let name = LiteStr::<49>::from_bytes(iana_bytes);
479        if !name.as_bytes().is_empty() {
480            dc.iana_name = Some(name);
481        }
482        offset += 49;
483
484        // scale (1 byte)
485        dc.scale = Scale::from_u8(bytes[offset]);
486        offset += 1;
487
488        // weekday (1 byte)
489        let wd_byte = bytes[offset];
490        if wd_byte != 255
491            && let Some(wd) = Weekday::from_wire_byte(wd_byte)
492        {
493            dc.wkday = Some(wd);
494        }
495        offset += 1;
496
497        // day_of_year (2 bytes)
498        let doy = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
499        if doy != u16::MAX {
500            dc.day_of_yr = Some(doy);
501        }
502        offset += 2;
503
504        // iso_week_year (8 bytes)
505        let iso_y = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
506        if iso_y != i64::MIN {
507            dc.iso_wk_yr = Some(iso_y);
508        }
509        offset += 8;
510
511        // iso_week (1 byte)
512        let iw = bytes[offset];
513        if iw != u8::MAX {
514            dc.iso_wk = Some(iw);
515        }
516        offset += 1;
517
518        // week_sun (1 byte)
519        let ws = bytes[offset];
520        if ws != u8::MAX {
521            dc.wk_sun = Some(ws);
522        }
523        offset += 1;
524
525        // week_mon (1 byte)
526        let wm = bytes[offset];
527        if wm != u8::MAX {
528            dc.wk_mon = Some(wm);
529        }
530        offset += 1;
531
532        // meridiem (1 byte)
533        let mer_byte = bytes[offset];
534        if mer_byte != 255
535            && let Some(m) = Meridiem::from_wire_byte(mer_byte)
536        {
537            dc.meridiem = Some(m);
538        }
539        offset += 1;
540
541        // unix_timestamp_seconds (8 bytes)
542        let unix = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
543        if unix != i64::MIN {
544            dc.unix_timestamp_seconds = Some(unix);
545        }
546
547        Some(dc)
548    }
549}