Skip to main content

deep_time/
wire.rs

1use crate::{
2    Drift, Dt, Every, LiteStr, Meridiem, Offset, Scale, Spacetime, TimeParts, TimeRange, Weekday,
3    YmdHmsRich,
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 (17 bytes).
11    pub const WIRE_SIZE: usize = 17;
12
13    /// Serializes this `Dt` into a fixed 17-byte little-endian buffer.
14    ///
15    /// ## Wire Format
16    ///
17    /// - Byte `0`: Version (`WIRE_VERSION`)
18    /// - Bytes `[1..9]`: `sec` as little-endian `i64`
19    /// - Bytes `[9..17]`: `subsec` as little-endian `u64`
20    ///
21    /// This format is stable, portable, and suitable for network transmission,
22    /// file storage, or FFI. The internal representation is always TAI.
23    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
24        let mut buf = [0u8; Self::WIRE_SIZE];
25        buf[0] = Self::WIRE_VERSION;
26        buf[1..9].copy_from_slice(&self.sec.to_le_bytes());
27        buf[9..17].copy_from_slice(&self.attos.to_le_bytes());
28        buf
29    }
30
31    /// Deserializes a `Dt` from exactly 17 bytes of wire data.
32    ///
33    /// Returns `None` if the version byte is unknown.
34    /// Any `subsec` value ≥ 10¹⁸ is automatically normalized using
35    /// [`carry_attos`](Self::carry_attos) so the resulting `Dt`
36    /// is always in canonical form.
37    ///
38    /// ## Security
39    ///
40    /// Safe to call with completely untrusted input. Fixed-size format,
41    /// no allocation, no `unsafe`, and no possibility of code execution.
42    /// Malicious data simply produces a normalized (but still valid) `Dt`.
43    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
44        if bytes.len() != Self::WIRE_SIZE {
45            return None;
46        }
47
48        if bytes[0] != Self::WIRE_VERSION {
49            return None;
50        }
51
52        let sec = i64::from_le_bytes([
53            bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
54        ]);
55        let subsec = u64::from_le_bytes([
56            bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
57        ]);
58
59        Some(Self::new(sec, subsec))
60    }
61}
62
63impl Drift {
64    /// Current wire format version.
65    pub const WIRE_VERSION: u8 = 1;
66
67    /// Size of the canonical wire representation in bytes.
68    pub const WIRE_SIZE: usize = 3 * Dt::WIRE_SIZE; // 3 × 17 = 51
69
70    /// Serializes this `Drift` polynomial into a fixed buffer.
71    ///
72    /// The layout is the concatenation of the three `Dt` fields.
73    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
74        let mut buf = [0u8; Self::WIRE_SIZE];
75        let c = self.constant.to_wire_bytes();
76        let r = self.rate.to_wire_bytes();
77        let a = self.accel.to_wire_bytes();
78
79        buf[0..Dt::WIRE_SIZE].copy_from_slice(&c);
80        buf[Dt::WIRE_SIZE..2 * Dt::WIRE_SIZE].copy_from_slice(&r);
81        buf[2 * Dt::WIRE_SIZE..].copy_from_slice(&a);
82        buf
83    }
84
85    /// Deserializes a `Drift` from exactly `WIRE_SIZE` bytes of wire data.
86    ///
87    /// Returns `None` if any nested `Dt` fails validation or if the version
88    /// byte is unknown.
89    ///
90    /// ## Security
91    ///
92    /// Composes the safety guarantees of
93    /// [`from_wire_bytes`](docs.rs/deep-time/latest/deep_time/struct.Dt.html#method.from_wire_bytes).
94    ///
95    /// Fixed size and layered validation make it safe for untrusted input.
96    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
97        if bytes.len() != Self::WIRE_SIZE {
98            return None;
99        }
100
101        if bytes[0] != Self::WIRE_VERSION {
102            return None;
103        }
104
105        let constant = Dt::from_wire_bytes(&bytes[0..Dt::WIRE_SIZE])?;
106        let rate = Dt::from_wire_bytes(&bytes[Dt::WIRE_SIZE..2 * Dt::WIRE_SIZE])?;
107        let accel = Dt::from_wire_bytes(&bytes[2 * Dt::WIRE_SIZE..])?;
108
109        Some(Self::new(constant, rate, accel))
110    }
111}
112
113impl Spacetime {
114    /// Size of the canonical wire representation in bytes (24 bytes).
115    pub const WIRE_SIZE: usize = 24;
116
117    /// Serializes this `Spacetime` snapshot into a fixed 24-byte buffer.
118    ///
119    /// All fields are stored as little-endian IEEE 754 `f64`.
120    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
121        let mut buf = [0u8; Self::WIRE_SIZE];
122        buf[0..8].copy_from_slice(&self.alpha.to_le_bytes());
123        buf[8..16].copy_from_slice(&self.beta.to_le_bytes());
124        buf[16..24].copy_from_slice(&self.kretschmann.to_le_bytes());
125        buf
126    }
127
128    /// Deserializes a `Spacetime` from exactly 24 bytes.
129    ///
130    /// ## Security
131    ///
132    /// Accepts any `f64` bit pattern (including `NaN`/`Inf`) to match the
133    /// type’s own invariants. Fixed size makes it immune to length-based
134    /// attacks. Safe for untrusted input.
135    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
136        if bytes.len() != Self::WIRE_SIZE {
137            return None;
138        }
139        let alpha = f64::from_le_bytes([
140            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
141        ]);
142        let beta = f64::from_le_bytes([
143            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
144        ]);
145        let kretschmann = f64::from_le_bytes([
146            bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
147        ]);
148        Some(Self {
149            alpha,
150            beta,
151            kretschmann,
152        })
153    }
154}
155
156impl Every {
157    /// Size of the canonical wire representation in bytes (33 bytes).
158    pub const WIRE_SIZE: usize = Dt::WIRE_SIZE + Dt::WIRE_SIZE;
159
160    /// Serializes this `Every` builder into a fixed 33-byte buffer.
161    ///
162    /// The layout is simply the concatenation of `start` (17 bytes) and `step` (16 bytes).
163    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
164        let mut buf = [0u8; Self::WIRE_SIZE];
165        let start = self.start.to_wire_bytes();
166        let step = self.step.to_wire_bytes();
167        buf[0..17].copy_from_slice(&start);
168        buf[17..33].copy_from_slice(&step);
169        buf
170    }
171
172    /// Deserializes an `Every` builder from exactly 33 bytes.
173    ///
174    /// ## Security
175    ///
176    /// Safe for untrusted input. Fixed size with strict validation
177    /// of the inner `Dt` and `Dt`.
178    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
179        if bytes.len() != Self::WIRE_SIZE {
180            return None;
181        }
182        let start = Dt::from_wire_bytes(&bytes[0..17])?;
183        let step = Dt::from_wire_bytes(&bytes[17..33])?;
184        Some(Self { start, step })
185    }
186}
187
188impl TimeRange {
189    /// Current wire format version.
190    pub const WIRE_VERSION: u8 = 1;
191
192    /// Size of the canonical wire representation in bytes.
193    /// Only the logical definition is stored (runtime state is not serialized).
194    pub const WIRE_SIZE: usize = 1 + 2 * Dt::WIRE_SIZE + Dt::WIRE_SIZE + 1;
195
196    /// Serializes this `TimeRange` into a fixed buffer.
197    ///
198    /// Only the logical definition is stored:
199    /// - `start` + `end` + `step` + `inclusive` flag
200    ///
201    /// Runtime iterator state (`current`, `finished`) is **not** serialized.
202    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
203        let mut buf = [0u8; Self::WIRE_SIZE];
204        buf[0] = Self::WIRE_VERSION;
205
206        let start = self.start.to_wire_bytes();
207        let end = self.end.to_wire_bytes();
208        let step = self.step.to_wire_bytes();
209
210        let tp_size = Dt::WIRE_SIZE;
211        let span_size = Dt::WIRE_SIZE;
212
213        buf[1..1 + tp_size].copy_from_slice(&start);
214        buf[1 + tp_size..1 + 2 * tp_size].copy_from_slice(&end);
215        buf[1 + 2 * tp_size..1 + 2 * tp_size + span_size].copy_from_slice(&step);
216        buf[1 + 2 * tp_size + span_size] = if self.inclusive { 1 } else { 0 };
217
218        buf
219    }
220
221    /// Deserializes a `TimeRange` from exactly `WIRE_SIZE` bytes.
222    ///
223    /// The iterator is reconstructed in its initial state
224    /// (`current = start`, `finished = false`).
225    ///
226    /// Returns `None` if the version is unknown or any component is invalid.
227    ///
228    /// ## Security
229    ///
230    /// Safe for untrusted input. Fixed size with layered validation
231    /// of all inner types. No runtime iterator state is accepted from the wire.
232    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
233        if bytes.len() != Self::WIRE_SIZE {
234            return None;
235        }
236
237        if bytes[0] != Self::WIRE_VERSION {
238            return None;
239        }
240
241        let tp_size = Dt::WIRE_SIZE;
242        let span_size = Dt::WIRE_SIZE;
243
244        let start = Dt::from_wire_bytes(&bytes[1..1 + tp_size])?;
245        let end = Dt::from_wire_bytes(&bytes[1 + tp_size..1 + 2 * tp_size])?;
246        let step = Dt::from_wire_bytes(&bytes[1 + 2 * tp_size..1 + 2 * tp_size + span_size])?;
247        let inclusive = bytes[1 + 2 * tp_size + span_size] != 0;
248
249        Some(Self::new(start, end, step, inclusive))
250    }
251}
252
253impl YmdHmsRich {
254    /// Current wire format version.
255    pub const WIRE_VERSION: u8 = 1;
256
257    /// Size of the canonical wire representation in bytes (159 bytes).
258    pub const WIRE_SIZE: usize = 159;
259
260    /// Serializes this `YmdHmsRich` into a fixed 159-byte buffer.
261    ///
262    /// ## Wire Format (Version 1)
263    ///
264    /// - Byte `0`: Version (`WIRE_VERSION`)
265    /// - Bytes `1..17`: `unix_attosec` (`i128`)
266    /// - Bytes `17..25`: `yr` (`i64`)
267    /// - Bytes `25..30`: `mo`, `day`, `hr`, `min`, `sec` (`u8` × 5)
268    /// - Bytes `30..38`: `attos` (`u64`)
269    /// - Bytes `38..46`: `iso_yr` (`i64`)
270    /// - Bytes `46..48`: `iso_wk` + `iso_wkday` (`u8` × 2)
271    /// - Bytes `48..50`: `day_of_yr` (`u16`)
272    /// - Byte `50`: `wkday` (`u8`)
273    /// - Bytes `51..53`: `wk_of_yr_sun` + `wk_of_yr_mon` (`u8` × 2)
274    /// - Bytes `53..58`: `offset_sec` (tag byte + `i32`)
275    /// - Bytes `58..108`: `tz` (tag byte + `LiteStr<49>`)
276    /// - Bytes `108..158`: `tz_abbrev` (tag byte + `LiteStr<49>`)
277    /// - Byte `158`: `scale` (1 byte via `to_wire_byte`)
278    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
279        let mut buf = [0u8; Self::WIRE_SIZE];
280        buf[0] = Self::WIRE_VERSION;
281        let mut offset = 1usize;
282
283        // unix_attosec (16 bytes)
284        buf[offset..offset + 16].copy_from_slice(&self.unix_attosec.to_le_bytes());
285        offset += 16;
286
287        // yr (8 bytes)
288        buf[offset..offset + 8].copy_from_slice(&self.yr.to_le_bytes());
289        offset += 8;
290
291        // mo, day, hr, min, sec (5 bytes)
292        buf[offset] = self.mo;
293        offset += 1;
294        buf[offset] = self.day;
295        offset += 1;
296        buf[offset] = self.hr;
297        offset += 1;
298        buf[offset] = self.min;
299        offset += 1;
300        buf[offset] = self.sec;
301        offset += 1;
302
303        // attos (8 bytes)
304        buf[offset..offset + 8].copy_from_slice(&self.attos.to_le_bytes());
305        offset += 8;
306
307        // iso_yr (8 bytes)
308        buf[offset..offset + 8].copy_from_slice(&self.iso_yr.to_le_bytes());
309        offset += 8;
310
311        // iso_wk + iso_wkday (2 bytes)
312        buf[offset] = self.iso_wk;
313        offset += 1;
314        buf[offset] = self.iso_wkday.to_wire_byte();
315        offset += 1;
316
317        // day_of_yr (2 bytes)
318        buf[offset..offset + 2].copy_from_slice(&self.day_of_yr.to_le_bytes());
319        offset += 2;
320
321        // wkday (1 byte)
322        buf[offset] = self.wkday;
323        offset += 1;
324
325        // wk_of_yr_sun + wk_of_yr_mon (2 bytes)
326        buf[offset] = self.wk_of_yr_sun;
327        offset += 1;
328        buf[offset] = self.wk_of_yr_mon;
329        offset += 1;
330
331        // offset_sec (Option<i32>) — 5 bytes
332        if let Some(val) = self.offset_sec {
333            buf[offset] = 1;
334            buf[offset + 1..offset + 5].copy_from_slice(&val.to_le_bytes());
335        } else {
336            buf[offset] = 0;
337        }
338        offset += 5;
339
340        // tz (Option<LiteStr<49>>) — 50 bytes
341        if let Some(tz) = &self.tz {
342            buf[offset] = 1;
343            let tz_bytes = tz.to_bytes();
344            buf[offset + 1..offset + 1 + LiteStr::<49>::SIZE].copy_from_slice(&tz_bytes);
345        } else {
346            buf[offset] = 0;
347        }
348        offset += 1 + LiteStr::<49>::SIZE;
349
350        // tz_abbrev (Option<LiteStr<49>>) — 50 bytes
351        if let Some(abbrev) = &self.tz_abbrev {
352            buf[offset] = 1;
353            let abbrev_bytes = abbrev.to_bytes();
354            buf[offset + 1..offset + 1 + LiteStr::<49>::SIZE].copy_from_slice(&abbrev_bytes);
355        } else {
356            buf[offset] = 0;
357        }
358        offset += 1 + LiteStr::<49>::SIZE;
359
360        // scale (1 byte)
361        buf[offset] = self.scale.to_wire_byte();
362
363        buf
364    }
365
366    /// Deserializes a `YmdHmsRich` from exactly 159 bytes of wire data.
367    ///
368    /// Returns `None` if the version is unknown or any field is invalid.
369    ///
370    /// ## Security
371    ///
372    /// Safe for untrusted input. Fixed-size format with strict validation.
373    /// No allocation or `unsafe` code used.
374    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
375        if bytes.len() != Self::WIRE_SIZE {
376            return None;
377        }
378        if bytes[0] != Self::WIRE_VERSION {
379            return None;
380        }
381
382        let mut offset = 1usize;
383
384        // unix_attosec (16 bytes)
385        let unix_attosec = i128::from_le_bytes(bytes[offset..offset + 16].try_into().ok()?);
386        offset += 16;
387
388        // yr (8 bytes)
389        let yr = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
390        offset += 8;
391
392        // mo, day, hr, min, sec (5 bytes)
393        let mo = bytes[offset];
394        offset += 1;
395        let day = bytes[offset];
396        offset += 1;
397        let hr = bytes[offset];
398        offset += 1;
399        let min = bytes[offset];
400        offset += 1;
401        let sec = bytes[offset];
402        offset += 1;
403
404        // attos (8 bytes)
405        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
406        offset += 8;
407
408        // iso_yr (8 bytes)
409        let iso_yr = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
410        offset += 8;
411
412        // iso_wk + iso_wkday (2 bytes)
413        let iso_wk = bytes[offset];
414        offset += 1;
415        let iso_wkday = Weekday::from_wire_byte(bytes[offset])?;
416        offset += 1;
417
418        // day_of_yr (2 bytes)
419        let day_of_yr = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
420        offset += 2;
421
422        // wkday (1 byte)
423        let wkday = bytes[offset];
424        offset += 1;
425
426        // wk_of_yr_sun + wk_of_yr_mon (2 bytes)
427        let wk_of_yr_sun = bytes[offset];
428        offset += 1;
429        let wk_of_yr_mon = bytes[offset];
430        offset += 1;
431
432        // offset_sec (Option<i32>) — 5 bytes
433        let offset_sec = if bytes[offset] == 1 {
434            Some(i32::from_le_bytes(
435                bytes[offset + 1..offset + 5].try_into().ok()?,
436            ))
437        } else {
438            None
439        };
440        offset += 5;
441
442        // tz (Option<LiteStr<49>>) — 50 bytes
443        let tz = if bytes[offset] == 1 {
444            LiteStr::<49>::from_bytes(&bytes[offset + 1..offset + 1 + LiteStr::<49>::SIZE]).ok()
445        } else {
446            None
447        };
448        offset += 1 + LiteStr::<49>::SIZE;
449
450        // tz_abbrev (Option<LiteStr<49>>) — 50 bytes
451        let tz_abbrev = if bytes[offset] == 1 {
452            LiteStr::<49>::from_bytes(&bytes[offset + 1..offset + 1 + LiteStr::<49>::SIZE]).ok()
453        } else {
454            None
455        };
456        offset += 1 + LiteStr::<49>::SIZE;
457
458        // scale (1 byte)
459        let scale = Scale::from_u8(bytes[offset]);
460
461        Some(Self {
462            unix_attosec,
463            yr,
464            mo,
465            day,
466            hr,
467            min,
468            sec,
469            attos,
470            iso_yr,
471            iso_wk,
472            iso_wkday,
473            day_of_yr,
474            wkday,
475            wk_of_yr_sun,
476            wk_of_yr_mon,
477            offset_sec,
478            tz,
479            tz_abbrev,
480            scale,
481        })
482    }
483}
484
485impl Meridiem {
486    pub const WIRE_SIZE: usize = 1;
487
488    #[inline]
489    pub const fn to_wire_byte(self) -> u8 {
490        match self {
491            Meridiem::AM => 0,
492            Meridiem::PM => 1,
493        }
494    }
495
496    #[inline]
497    pub const fn from_wire_byte(b: u8) -> Option<Self> {
498        match b {
499            0 => Some(Meridiem::AM),
500            1 => Some(Meridiem::PM),
501            _ => None,
502        }
503    }
504}
505
506impl Offset {
507    pub const WIRE_SIZE: usize = 5; // tag (1) + i32 (4)
508
509    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
510        let mut buf = [0u8; Self::WIRE_SIZE];
511        match self {
512            Offset::Utc => buf[0] = 0,
513            Offset::None => buf[0] = 1,
514            Offset::Fixed(offset) => {
515                buf[0] = 2;
516                buf[1..5].copy_from_slice(&offset.to_le_bytes());
517            }
518        }
519        buf
520    }
521
522    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
523        if bytes.len() != Self::WIRE_SIZE {
524            return None;
525        }
526        match bytes[0] {
527            0 => Some(Offset::Utc),
528            1 => Some(Offset::None),
529            2 => {
530                let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
531                Some(Offset::Fixed(offset))
532            }
533            _ => None,
534        }
535    }
536}
537
538impl Weekday {
539    pub const WIRE_SIZE: usize = 1;
540
541    #[inline]
542    pub const fn to_wire_byte(self) -> u8 {
543        self.wk_sun()
544    }
545
546    #[inline]
547    pub const fn from_wire_byte(b: u8) -> Option<Self> {
548        Self::from_sunday_zero_offset(b)
549    }
550}
551
552impl TimeParts {
553    /// Current wire format version.
554    pub const WIRE_VERSION: u8 = 1;
555
556    /// Total size of the wire representation (120 bytes).
557    pub const WIRE_SIZE: usize = 120;
558
559    /// Serializes `TimeParts` into a fixed 120-byte buffer.
560    ///
561    /// Layout:
562    /// - Byte 0: Version (`WIRE_VERSION`)
563    /// - Bytes 1..120: Data (119 bytes)
564    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
565        let mut buf = [0u8; Self::WIRE_SIZE];
566        buf[0] = Self::WIRE_VERSION;
567
568        let mut offset = 1usize;
569
570        // year (sentinel = i64::MIN)
571        let year = self.yr.unwrap_or(i64::MIN);
572        buf[offset..offset + 8].copy_from_slice(&year.to_le_bytes());
573        offset += 8;
574
575        // month
576        buf[offset] = self.mo.unwrap_or(u8::MAX);
577        offset += 1;
578
579        // day
580        buf[offset] = self.day.unwrap_or(u8::MAX);
581        offset += 1;
582
583        // hour
584        buf[offset] = self.hr.unwrap_or(u8::MAX);
585        offset += 1;
586
587        // minute
588        buf[offset] = self.min.unwrap_or(u8::MAX);
589        offset += 1;
590
591        // second
592        buf[offset] = self.sec.unwrap_or(u8::MAX);
593        offset += 1;
594
595        // attos
596        let attos = self.attos.unwrap_or(u64::MAX);
597        buf[offset..offset + 8].copy_from_slice(&attos.to_le_bytes());
598        offset += 8;
599
600        // offset (5 bytes)
601        let offset_bytes = self.offset.unwrap_or_default().to_wire_bytes();
602        buf[offset..offset + 5].copy_from_slice(&offset_bytes);
603        offset += 5;
604
605        // iana_name (49 bytes)
606        if let Some(name) = &self.iana_name {
607            let name_bytes = name.to_bytes();
608            buf[offset..offset + 49].copy_from_slice(&name_bytes);
609        }
610        offset += 49;
611
612        // is_leap_second
613        buf[offset] = if self.is_leap_sec { 1 } else { 0 };
614        offset += 1;
615
616        // scale
617        buf[offset] = self.scale as u8;
618        offset += 1;
619
620        // weekday
621        buf[offset] = self.wkday.map_or(255, |w| w.to_wire_byte());
622        offset += 1;
623
624        // day_of_year
625        let doy = self.day_of_yr.unwrap_or(u16::MAX);
626        buf[offset..offset + 2].copy_from_slice(&doy.to_le_bytes());
627        offset += 2;
628
629        // iso_week_year
630        let iso_y = self.iso_wk_yr.unwrap_or(i64::MIN);
631        buf[offset..offset + 8].copy_from_slice(&iso_y.to_le_bytes());
632        offset += 8;
633
634        // iso_week
635        buf[offset] = self.iso_wk.unwrap_or(u8::MAX);
636        offset += 1;
637
638        // week_sun
639        buf[offset] = self.wk_sun.unwrap_or(u8::MAX);
640        offset += 1;
641
642        // week_mon
643        buf[offset] = self.wk_mon.unwrap_or(u8::MAX);
644        offset += 1;
645
646        // meridiem
647        buf[offset] = self.meridiem.map_or(255, |m| m.to_wire_byte());
648        offset += 1;
649
650        // unix_timestamp_seconds
651        let unix = self.unix_timestamp_seconds.unwrap_or(i64::MIN);
652        buf[offset..offset + 8].copy_from_slice(&unix.to_le_bytes());
653
654        buf
655    }
656
657    /// Deserializes `TimeParts` from exactly 120 bytes.
658    ///
659    /// Returns `None` if the version byte is unknown or the data is invalid.
660    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
661        if bytes.len() != Self::WIRE_SIZE {
662            return None;
663        }
664        if bytes[0] != Self::WIRE_VERSION {
665            return None;
666        }
667
668        let mut dc = TimeParts::default();
669        let mut offset = 1usize;
670
671        // year (8 bytes)
672        let year = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
673        if year != i64::MIN {
674            dc.yr = Some(year);
675        }
676        offset += 8;
677
678        // month (1 byte)
679        let m = bytes[offset];
680        if m != u8::MAX {
681            dc.mo = Some(m);
682        }
683        offset += 1;
684
685        // day (1 byte)
686        let d = bytes[offset];
687        if d != u8::MAX {
688            dc.day = Some(d);
689        }
690        offset += 1;
691
692        // hour (1 byte)
693        let h = bytes[offset];
694        if h != u8::MAX {
695            dc.hr = Some(h);
696        }
697        offset += 1;
698
699        // minute (1 byte)
700        let min = bytes[offset];
701        if min != u8::MAX {
702            dc.min = Some(min);
703        }
704        offset += 1;
705
706        // second (1 byte)
707        let sec = bytes[offset];
708        if sec != u8::MAX {
709            dc.sec = Some(sec);
710        }
711        offset += 1;
712
713        // attos (8 bytes)
714        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
715        if attos != u64::MAX {
716            dc.attos = Some(attos);
717        }
718        offset += 8;
719
720        // offset (5 bytes) — already nice
721        if let Some(offset) = Offset::from_wire_bytes(&bytes[offset..offset + 5]) {
722            dc.offset = Some(offset);
723        }
724        offset += 5;
725
726        // iana_name (49 bytes) — already nice
727        let iana_bytes = &bytes[offset..offset + 49];
728        if let Some(name) = LiteStr::<49>::from_bytes(iana_bytes).ok()
729            && !name.len() == 0
730        {
731            dc.iana_name = Some(name);
732        }
733        offset += 49;
734
735        // is_leap_second (1 byte)
736        dc.is_leap_sec = bytes[offset] != 0;
737        offset += 1;
738
739        // scale (1 byte)
740        dc.scale = Scale::from_u8(bytes[offset]);
741        offset += 1;
742
743        // weekday (1 byte)
744        let wd_byte = bytes[offset];
745        if wd_byte != 255
746            && let Some(wd) = Weekday::from_wire_byte(wd_byte)
747        {
748            dc.wkday = Some(wd);
749        }
750        offset += 1;
751
752        // day_of_year (2 bytes)
753        let doy = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
754        if doy != u16::MAX {
755            dc.day_of_yr = Some(doy);
756        }
757        offset += 2;
758
759        // iso_week_year (8 bytes)
760        let iso_y = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
761        if iso_y != i64::MIN {
762            dc.iso_wk_yr = Some(iso_y);
763        }
764        offset += 8;
765
766        // iso_week (1 byte)
767        let iw = bytes[offset];
768        if iw != u8::MAX {
769            dc.iso_wk = Some(iw);
770        }
771        offset += 1;
772
773        // week_sun (1 byte)
774        let ws = bytes[offset];
775        if ws != u8::MAX {
776            dc.wk_sun = Some(ws);
777        }
778        offset += 1;
779
780        // week_mon (1 byte)
781        let wm = bytes[offset];
782        if wm != u8::MAX {
783            dc.wk_mon = Some(wm);
784        }
785        offset += 1;
786
787        // meridiem (1 byte)
788        let mer_byte = bytes[offset];
789        if mer_byte != 255
790            && let Some(m) = Meridiem::from_wire_byte(mer_byte)
791        {
792            dc.meridiem = Some(m);
793        }
794
795        offset += 1;
796
797        // unix_timestamp_seconds (8 bytes)
798        let unix = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
799        if unix != i64::MIN {
800            dc.unix_timestamp_seconds = Some(unix);
801        }
802
803        Some(dc)
804    }
805}