Skip to main content

deep_time/
wire.rs

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