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::None => buf[0] = 0,
284            Offset::Fixed(offset) => {
285                buf[0] = 1;
286                buf[1..5].copy_from_slice(&offset.to_le_bytes());
287            }
288        }
289        buf
290    }
291
292    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
293        if bytes.len() != Self::WIRE_SIZE {
294            return None;
295        }
296        match bytes[0] {
297            0 => Some(Offset::None),
298            1 => {
299                let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
300                Some(Offset::Fixed(offset))
301            }
302            _ => None,
303        }
304    }
305}
306
307impl Weekday {
308    pub const WIRE_SIZE: usize = 1;
309
310    #[inline]
311    pub const fn to_wire_byte(self) -> u8 {
312        self.wkday_sun_0_based()
313    }
314
315    #[inline]
316    pub const fn from_wire_byte(b: u8) -> Option<Self> {
317        Self::from_sunday_0_based(b)
318    }
319}
320
321impl TimeParts {
322    /// Current wire format version.
323    pub const WIRE_VERSION: u8 = 1;
324
325    /// Total size of the wire representation (119 bytes).
326    pub const WIRE_SIZE: usize = 119;
327
328    /// Serializes `TimeParts` into a fixed 119-byte buffer.
329    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
330        let mut buf = [0u8; Self::WIRE_SIZE];
331        buf[0] = Self::WIRE_VERSION;
332
333        let mut offset = 1usize;
334
335        // year (sentinel = i64::MIN)
336        let year = self.yr.unwrap_or(i64::MIN);
337        buf[offset..offset + 8].copy_from_slice(&year.to_le_bytes());
338        offset += 8;
339
340        // month
341        buf[offset] = self.mo.unwrap_or(u8::MAX);
342        offset += 1;
343
344        // day
345        buf[offset] = self.day.unwrap_or(u8::MAX);
346        offset += 1;
347
348        // hour
349        buf[offset] = self.hr;
350        offset += 1;
351
352        // minute
353        buf[offset] = self.min;
354        offset += 1;
355
356        // second
357        buf[offset] = self.sec;
358        offset += 1;
359
360        // attos
361        let attos = self.attos;
362        buf[offset..offset + 8].copy_from_slice(&attos.to_le_bytes());
363        offset += 8;
364
365        // offset (5 bytes)
366        let offset_bytes = self.offset.unwrap_or_default().to_wire_bytes();
367        buf[offset..offset + 5].copy_from_slice(&offset_bytes);
368        offset += 5;
369
370        // iana_name (49 bytes)
371        if let Some(name) = &self.iana_name {
372            let name_bytes = name.bytes;
373            buf[offset..offset + 49].copy_from_slice(&name_bytes);
374        }
375        offset += 49;
376
377        // scale
378        buf[offset] = self.scale as u8;
379        offset += 1;
380
381        // weekday
382        buf[offset] = self.wkday.map_or(255, |w| w.to_wire_byte());
383        offset += 1;
384
385        // day_of_year
386        let doy = self.day_of_yr.unwrap_or(u16::MAX);
387        buf[offset..offset + 2].copy_from_slice(&doy.to_le_bytes());
388        offset += 2;
389
390        // iso_week_year
391        let iso_y = self.iso_wk_yr.unwrap_or(i64::MIN);
392        buf[offset..offset + 8].copy_from_slice(&iso_y.to_le_bytes());
393        offset += 8;
394
395        // iso_week
396        buf[offset] = self.iso_wk.unwrap_or(u8::MAX);
397        offset += 1;
398
399        // week_sun
400        buf[offset] = self.wk_sun.unwrap_or(u8::MAX);
401        offset += 1;
402
403        // week_mon
404        buf[offset] = self.wk_mon.unwrap_or(u8::MAX);
405        offset += 1;
406
407        // meridiem
408        buf[offset] = self.meridiem.map_or(255, |m| m.to_wire_byte());
409        offset += 1;
410
411        // unix_timestamp_seconds
412        let unix = self.unix_timestamp_seconds.unwrap_or(i64::MIN);
413        buf[offset..offset + 8].copy_from_slice(&unix.to_le_bytes());
414
415        buf
416    }
417
418    /// Deserializes `TimeParts` from exactly 119 bytes.
419    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
420        if bytes.len() != Self::WIRE_SIZE {
421            return None;
422        }
423        if bytes[0] != Self::WIRE_VERSION {
424            return None;
425        }
426
427        let mut dc = TimeParts::default();
428        let mut offset = 1usize;
429
430        // year (8 bytes)
431        let year = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
432        if year != i64::MIN {
433            dc.yr = Some(year);
434        }
435        offset += 8;
436
437        // month (1 byte)
438        let m = bytes[offset];
439        if m != u8::MAX {
440            dc.mo = Some(m);
441        }
442        offset += 1;
443
444        // day (1 byte)
445        let d = bytes[offset];
446        if d != u8::MAX {
447            dc.day = Some(d);
448        }
449        offset += 1;
450
451        // hour (1 byte)
452        dc.hr = bytes[offset];
453        offset += 1;
454
455        // minute (1 byte)
456        dc.min = bytes[offset];
457        offset += 1;
458
459        // second (1 byte)
460        dc.sec = bytes[offset];
461        offset += 1;
462
463        // attos (8 bytes)
464        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
465        dc.attos = attos;
466        offset += 8;
467
468        // offset (5 bytes)
469        if let Some(off) = Offset::from_wire_bytes(&bytes[offset..offset + 5]) {
470            dc.offset = Some(off);
471        }
472        offset += 5;
473
474        // iana_name (49 bytes)
475        let iana_bytes = &bytes[offset..offset + 49];
476        let name = LiteStr::<49>::from_bytes(iana_bytes);
477        if !name.as_bytes().is_empty() {
478            dc.iana_name = Some(name);
479        }
480        offset += 49;
481
482        // scale (1 byte)
483        dc.scale = Scale::from_u8(bytes[offset]);
484        offset += 1;
485
486        // weekday (1 byte)
487        let wd_byte = bytes[offset];
488        if wd_byte != 255
489            && let Some(wd) = Weekday::from_wire_byte(wd_byte)
490        {
491            dc.wkday = Some(wd);
492        }
493        offset += 1;
494
495        // day_of_year (2 bytes)
496        let doy = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
497        if doy != u16::MAX {
498            dc.day_of_yr = Some(doy);
499        }
500        offset += 2;
501
502        // iso_week_year (8 bytes)
503        let iso_y = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
504        if iso_y != i64::MIN {
505            dc.iso_wk_yr = Some(iso_y);
506        }
507        offset += 8;
508
509        // iso_week (1 byte)
510        let iw = bytes[offset];
511        if iw != u8::MAX {
512            dc.iso_wk = Some(iw);
513        }
514        offset += 1;
515
516        // week_sun (1 byte)
517        let ws = bytes[offset];
518        if ws != u8::MAX {
519            dc.wk_sun = Some(ws);
520        }
521        offset += 1;
522
523        // week_mon (1 byte)
524        let wm = bytes[offset];
525        if wm != u8::MAX {
526            dc.wk_mon = Some(wm);
527        }
528        offset += 1;
529
530        // meridiem (1 byte)
531        let mer_byte = bytes[offset];
532        if mer_byte != 255
533            && let Some(m) = Meridiem::from_wire_byte(mer_byte)
534        {
535            dc.meridiem = Some(m);
536        }
537        offset += 1;
538
539        // unix_timestamp_seconds (8 bytes)
540        let unix = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
541        if unix != i64::MIN {
542            dc.unix_timestamp_seconds = Some(unix);
543        }
544
545        Some(dc)
546    }
547}