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 (120 bytes).
328    pub const WIRE_SIZE: usize = 120;
329
330    /// Serializes `TimeParts` into a fixed 120-byte buffer.
331    ///
332    /// Layout:
333    /// - Byte 0: Version (`WIRE_VERSION`)
334    /// - Bytes 1..120: Data (119 bytes)
335    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
336        let mut buf = [0u8; Self::WIRE_SIZE];
337        buf[0] = Self::WIRE_VERSION;
338
339        let mut offset = 1usize;
340
341        // year (sentinel = i64::MIN)
342        let year = self.yr.unwrap_or(i64::MIN);
343        buf[offset..offset + 8].copy_from_slice(&year.to_le_bytes());
344        offset += 8;
345
346        // month
347        buf[offset] = self.mo.unwrap_or(u8::MAX);
348        offset += 1;
349
350        // day
351        buf[offset] = self.day.unwrap_or(u8::MAX);
352        offset += 1;
353
354        // hour
355        buf[offset] = self.hr.unwrap_or(u8::MAX);
356        offset += 1;
357
358        // minute
359        buf[offset] = self.min.unwrap_or(u8::MAX);
360        offset += 1;
361
362        // second
363        buf[offset] = self.sec.unwrap_or(u8::MAX);
364        offset += 1;
365
366        // attos
367        let attos = self.attos.unwrap_or(u64::MAX);
368        buf[offset..offset + 8].copy_from_slice(&attos.to_le_bytes());
369        offset += 8;
370
371        // offset (5 bytes)
372        let offset_bytes = self.offset.unwrap_or_default().to_wire_bytes();
373        buf[offset..offset + 5].copy_from_slice(&offset_bytes);
374        offset += 5;
375
376        // iana_name (49 bytes)
377        if let Some(name) = &self.iana_name {
378            let name_bytes = name.bytes;
379            buf[offset..offset + 49].copy_from_slice(&name_bytes);
380        }
381        offset += 49;
382
383        // is_leap_second
384        buf[offset] = if self.is_leap_sec { 1 } else { 0 };
385        offset += 1;
386
387        // scale
388        buf[offset] = self.scale as u8;
389        offset += 1;
390
391        // weekday
392        buf[offset] = self.wkday.map_or(255, |w| w.to_wire_byte());
393        offset += 1;
394
395        // day_of_year
396        let doy = self.day_of_yr.unwrap_or(u16::MAX);
397        buf[offset..offset + 2].copy_from_slice(&doy.to_le_bytes());
398        offset += 2;
399
400        // iso_week_year
401        let iso_y = self.iso_wk_yr.unwrap_or(i64::MIN);
402        buf[offset..offset + 8].copy_from_slice(&iso_y.to_le_bytes());
403        offset += 8;
404
405        // iso_week
406        buf[offset] = self.iso_wk.unwrap_or(u8::MAX);
407        offset += 1;
408
409        // week_sun
410        buf[offset] = self.wk_sun.unwrap_or(u8::MAX);
411        offset += 1;
412
413        // week_mon
414        buf[offset] = self.wk_mon.unwrap_or(u8::MAX);
415        offset += 1;
416
417        // meridiem
418        buf[offset] = self.meridiem.map_or(255, |m| m.to_wire_byte());
419        offset += 1;
420
421        // unix_timestamp_seconds
422        let unix = self.unix_timestamp_seconds.unwrap_or(i64::MIN);
423        buf[offset..offset + 8].copy_from_slice(&unix.to_le_bytes());
424
425        buf
426    }
427
428    /// Deserializes `TimeParts` from exactly 120 bytes.
429    ///
430    /// Returns `None` if the version byte is unknown or the data is invalid.
431    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
432        if bytes.len() != Self::WIRE_SIZE {
433            return None;
434        }
435        if bytes[0] != Self::WIRE_VERSION {
436            return None;
437        }
438
439        let mut dc = TimeParts::default();
440        let mut offset = 1usize;
441
442        // year (8 bytes)
443        let year = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
444        if year != i64::MIN {
445            dc.yr = Some(year);
446        }
447        offset += 8;
448
449        // month (1 byte)
450        let m = bytes[offset];
451        if m != u8::MAX {
452            dc.mo = Some(m);
453        }
454        offset += 1;
455
456        // day (1 byte)
457        let d = bytes[offset];
458        if d != u8::MAX {
459            dc.day = Some(d);
460        }
461        offset += 1;
462
463        // hour (1 byte)
464        let h = bytes[offset];
465        if h != u8::MAX {
466            dc.hr = Some(h);
467        }
468        offset += 1;
469
470        // minute (1 byte)
471        let min = bytes[offset];
472        if min != u8::MAX {
473            dc.min = Some(min);
474        }
475        offset += 1;
476
477        // second (1 byte)
478        let sec = bytes[offset];
479        if sec != u8::MAX {
480            dc.sec = Some(sec);
481        }
482        offset += 1;
483
484        // attos (8 bytes)
485        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
486        if attos != u64::MAX {
487            dc.attos = Some(attos);
488        }
489        offset += 8;
490
491        // offset (5 bytes) — already nice
492        if let Some(offset) = Offset::from_wire_bytes(&bytes[offset..offset + 5]) {
493            dc.offset = Some(offset);
494        }
495        offset += 5;
496
497        // iana_name (49 bytes) — already nice
498        let iana_bytes = &bytes[offset..offset + 49];
499        let name = LiteStr::<49>::from_bytes(iana_bytes);
500        if !name.as_bytes().len() == 0 {
501            dc.iana_name = Some(name);
502        }
503        offset += 49;
504
505        // is_leap_second (1 byte)
506        dc.is_leap_sec = bytes[offset] != 0;
507        offset += 1;
508
509        // scale (1 byte)
510        dc.scale = Scale::from_u8(bytes[offset]);
511        offset += 1;
512
513        // weekday (1 byte)
514        let wd_byte = bytes[offset];
515        if wd_byte != 255
516            && let Some(wd) = Weekday::from_wire_byte(wd_byte)
517        {
518            dc.wkday = Some(wd);
519        }
520        offset += 1;
521
522        // day_of_year (2 bytes)
523        let doy = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
524        if doy != u16::MAX {
525            dc.day_of_yr = Some(doy);
526        }
527        offset += 2;
528
529        // iso_week_year (8 bytes)
530        let iso_y = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
531        if iso_y != i64::MIN {
532            dc.iso_wk_yr = Some(iso_y);
533        }
534        offset += 8;
535
536        // iso_week (1 byte)
537        let iw = bytes[offset];
538        if iw != u8::MAX {
539            dc.iso_wk = Some(iw);
540        }
541        offset += 1;
542
543        // week_sun (1 byte)
544        let ws = bytes[offset];
545        if ws != u8::MAX {
546            dc.wk_sun = Some(ws);
547        }
548        offset += 1;
549
550        // week_mon (1 byte)
551        let wm = bytes[offset];
552        if wm != u8::MAX {
553            dc.wk_mon = Some(wm);
554        }
555        offset += 1;
556
557        // meridiem (1 byte)
558        let mer_byte = bytes[offset];
559        if mer_byte != 255
560            && let Some(m) = Meridiem::from_wire_byte(mer_byte)
561        {
562            dc.meridiem = Some(m);
563        }
564
565        offset += 1;
566
567        // unix_timestamp_seconds (8 bytes)
568        let unix = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
569        if unix != i64::MIN {
570            dc.unix_timestamp_seconds = Some(unix);
571        }
572
573        Some(dc)
574    }
575}