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.
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 YmdHmsRich {
258    /// Current wire format version.
259    pub const WIRE_VERSION: u8 = 1;
260
261    /// Size of the canonical wire representation in bytes (159 bytes).
262    pub const WIRE_SIZE: usize = 159;
263
264    /// Serializes this `YmdHmsRich` into a fixed 159-byte buffer.
265    ///
266    /// ## Wire Format (Version 1)
267    ///
268    /// - Byte `0`: Version (`WIRE_VERSION`)
269    /// - Bytes `1..17`: `unix_attosec` (`i128`)
270    /// - Bytes `17..25`: `yr` (`i64`)
271    /// - Bytes `25..30`: `mo`, `day`, `hr`, `min`, `sec` (`u8` × 5)
272    /// - Bytes `30..38`: `attos` (`u64`)
273    /// - Bytes `38..46`: `iso_yr` (`i64`)
274    /// - Bytes `46..48`: `iso_wk` + `iso_wkday` (`u8` × 2)
275    /// - Bytes `48..50`: `day_of_yr` (`u16`)
276    /// - Byte `50`: `wkday` (`u8`)
277    /// - Bytes `51..53`: `wk_of_yr_sun` + `wk_of_yr_mon` (`u8` × 2)
278    /// - Bytes `53..58`: `offset_sec` (tag byte + `i32`)
279    /// - Bytes `58..108`: `tz` (tag byte + `LiteStr<49>`)
280    /// - Bytes `108..158`: `tz_abbrev` (tag byte + `LiteStr<49>`)
281    /// - Byte `158`: `scale` (1 byte via `to_wire_byte`)
282    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
283        let mut buf = [0u8; Self::WIRE_SIZE];
284        buf[0] = Self::WIRE_VERSION;
285        let mut offset = 1usize;
286
287        // unix_attosec (16 bytes)
288        buf[offset..offset + 16].copy_from_slice(&self.unix_attosec.to_le_bytes());
289        offset += 16;
290
291        // yr (8 bytes)
292        buf[offset..offset + 8].copy_from_slice(&self.yr.to_le_bytes());
293        offset += 8;
294
295        // mo, day, hr, min, sec (5 bytes)
296        buf[offset] = self.mo;
297        offset += 1;
298        buf[offset] = self.day;
299        offset += 1;
300        buf[offset] = self.hr;
301        offset += 1;
302        buf[offset] = self.min;
303        offset += 1;
304        buf[offset] = self.sec;
305        offset += 1;
306
307        // attos (8 bytes)
308        buf[offset..offset + 8].copy_from_slice(&self.attos.to_le_bytes());
309        offset += 8;
310
311        // iso_yr (8 bytes)
312        buf[offset..offset + 8].copy_from_slice(&self.iso_yr.to_le_bytes());
313        offset += 8;
314
315        // iso_wk + iso_wkday (2 bytes)
316        buf[offset] = self.iso_wk;
317        offset += 1;
318        buf[offset] = self.iso_wkday.to_wire_byte();
319        offset += 1;
320
321        // day_of_yr (2 bytes)
322        buf[offset..offset + 2].copy_from_slice(&self.day_of_yr.to_le_bytes());
323        offset += 2;
324
325        // wkday (1 byte)
326        buf[offset] = self.wkday;
327        offset += 1;
328
329        // wk_of_yr_sun + wk_of_yr_mon (2 bytes)
330        buf[offset] = self.wk_of_yr_sun;
331        offset += 1;
332        buf[offset] = self.wk_of_yr_mon;
333        offset += 1;
334
335        // offset_sec (Option<i32>) — 5 bytes
336        if let Some(val) = self.offset_sec {
337            buf[offset] = 1;
338            buf[offset + 1..offset + 5].copy_from_slice(&val.to_le_bytes());
339        } else {
340            buf[offset] = 0;
341        }
342        offset += 5;
343
344        // tz (Option<LiteStr<49>>) — 50 bytes
345        if let Some(tz) = &self.tz {
346            buf[offset] = 1;
347            let tz_bytes = tz.to_bytes();
348            buf[offset + 1..offset + 1 + LiteStr::<49>::SIZE].copy_from_slice(&tz_bytes);
349        } else {
350            buf[offset] = 0;
351        }
352        offset += 1 + LiteStr::<49>::SIZE;
353
354        // tz_abbrev (Option<LiteStr<49>>) — 50 bytes
355        if let Some(abbrev) = &self.tz_abbrev {
356            buf[offset] = 1;
357            let abbrev_bytes = abbrev.to_bytes();
358            buf[offset + 1..offset + 1 + LiteStr::<49>::SIZE].copy_from_slice(&abbrev_bytes);
359        } else {
360            buf[offset] = 0;
361        }
362        offset += 1 + LiteStr::<49>::SIZE;
363
364        // scale (1 byte)
365        buf[offset] = self.scale.to_u8();
366
367        buf
368    }
369
370    /// Deserializes a `YmdHmsRich` from exactly 159 bytes of wire data.
371    ///
372    /// Returns `None` if the version is unknown or any field is invalid.
373    ///
374    /// ## Security
375    ///
376    /// Safe for untrusted input. Fixed-size format with strict validation.
377    /// No allocation or `unsafe` code used.
378    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
379        if bytes.len() != Self::WIRE_SIZE {
380            return None;
381        }
382        if bytes[0] != Self::WIRE_VERSION {
383            return None;
384        }
385
386        let mut offset = 1usize;
387
388        // unix_attosec (16 bytes)
389        let unix_attosec = i128::from_le_bytes(bytes[offset..offset + 16].try_into().ok()?);
390        offset += 16;
391
392        // yr (8 bytes)
393        let yr = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
394        offset += 8;
395
396        // mo, day, hr, min, sec (5 bytes)
397        let mo = bytes[offset];
398        offset += 1;
399        let day = bytes[offset];
400        offset += 1;
401        let hr = bytes[offset];
402        offset += 1;
403        let min = bytes[offset];
404        offset += 1;
405        let sec = bytes[offset];
406        offset += 1;
407
408        // attos (8 bytes)
409        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
410        offset += 8;
411
412        // iso_yr (8 bytes)
413        let iso_yr = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
414        offset += 8;
415
416        // iso_wk + iso_wkday (2 bytes)
417        let iso_wk = bytes[offset];
418        offset += 1;
419        let iso_wkday = Weekday::from_wire_byte(bytes[offset])?;
420        offset += 1;
421
422        // day_of_yr (2 bytes)
423        let day_of_yr = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
424        offset += 2;
425
426        // wkday (1 byte)
427        let wkday = bytes[offset];
428        offset += 1;
429
430        // wk_of_yr_sun + wk_of_yr_mon (2 bytes)
431        let wk_of_yr_sun = bytes[offset];
432        offset += 1;
433        let wk_of_yr_mon = bytes[offset];
434        offset += 1;
435
436        // offset_sec (Option<i32>) — 5 bytes
437        let offset_sec = if bytes[offset] == 1 {
438            Some(i32::from_le_bytes(
439                bytes[offset + 1..offset + 5].try_into().ok()?,
440            ))
441        } else {
442            None
443        };
444        offset += 5;
445
446        // tz (Option<LiteStr<49>>) — 50 bytes
447        let tz = if bytes[offset] == 1 {
448            LiteStr::<49>::from_bytes(&bytes[offset + 1..offset + 1 + LiteStr::<49>::SIZE]).ok()
449        } else {
450            None
451        };
452        offset += 1 + LiteStr::<49>::SIZE;
453
454        // tz_abbrev (Option<LiteStr<49>>) — 50 bytes
455        let tz_abbrev = if bytes[offset] == 1 {
456            LiteStr::<49>::from_bytes(&bytes[offset + 1..offset + 1 + LiteStr::<49>::SIZE]).ok()
457        } else {
458            None
459        };
460        offset += 1 + LiteStr::<49>::SIZE;
461
462        // scale (1 byte)
463        let scale = Scale::from_u8(bytes[offset]);
464
465        Some(Self {
466            unix_attosec,
467            yr,
468            mo,
469            day,
470            hr,
471            min,
472            sec,
473            attos,
474            iso_yr,
475            iso_wk,
476            iso_wkday,
477            day_of_yr,
478            wkday,
479            wk_of_yr_sun,
480            wk_of_yr_mon,
481            offset_sec,
482            tz,
483            tz_abbrev,
484            scale,
485        })
486    }
487}
488
489impl Meridiem {
490    pub const WIRE_SIZE: usize = 1;
491
492    #[inline]
493    pub const fn to_wire_byte(self) -> u8 {
494        match self {
495            Meridiem::AM => 0,
496            Meridiem::PM => 1,
497        }
498    }
499
500    #[inline]
501    pub const fn from_wire_byte(b: u8) -> Option<Self> {
502        match b {
503            0 => Some(Meridiem::AM),
504            1 => Some(Meridiem::PM),
505            _ => None,
506        }
507    }
508}
509
510impl Offset {
511    pub const WIRE_SIZE: usize = 5; // tag (1) + i32 (4)
512
513    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
514        let mut buf = [0u8; Self::WIRE_SIZE];
515        match self {
516            Offset::Utc => buf[0] = 0,
517            Offset::None => buf[0] = 1,
518            Offset::Fixed(offset) => {
519                buf[0] = 2;
520                buf[1..5].copy_from_slice(&offset.to_le_bytes());
521            }
522        }
523        buf
524    }
525
526    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
527        if bytes.len() != Self::WIRE_SIZE {
528            return None;
529        }
530        match bytes[0] {
531            0 => Some(Offset::Utc),
532            1 => Some(Offset::None),
533            2 => {
534                let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
535                Some(Offset::Fixed(offset))
536            }
537            _ => None,
538        }
539    }
540}
541
542impl Weekday {
543    pub const WIRE_SIZE: usize = 1;
544
545    #[inline]
546    pub const fn to_wire_byte(self) -> u8 {
547        self.wk_sun()
548    }
549
550    #[inline]
551    pub const fn from_wire_byte(b: u8) -> Option<Self> {
552        Self::from_sunday_zero_offset(b)
553    }
554}
555
556impl TimeParts {
557    /// Current wire format version.
558    pub const WIRE_VERSION: u8 = 1;
559
560    /// Total size of the wire representation (120 bytes).
561    pub const WIRE_SIZE: usize = 120;
562
563    /// Serializes `TimeParts` into a fixed 120-byte buffer.
564    ///
565    /// Layout:
566    /// - Byte 0: Version (`WIRE_VERSION`)
567    /// - Bytes 1..120: Data (119 bytes)
568    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
569        let mut buf = [0u8; Self::WIRE_SIZE];
570        buf[0] = Self::WIRE_VERSION;
571
572        let mut offset = 1usize;
573
574        // year (sentinel = i64::MIN)
575        let year = self.yr.unwrap_or(i64::MIN);
576        buf[offset..offset + 8].copy_from_slice(&year.to_le_bytes());
577        offset += 8;
578
579        // month
580        buf[offset] = self.mo.unwrap_or(u8::MAX);
581        offset += 1;
582
583        // day
584        buf[offset] = self.day.unwrap_or(u8::MAX);
585        offset += 1;
586
587        // hour
588        buf[offset] = self.hr.unwrap_or(u8::MAX);
589        offset += 1;
590
591        // minute
592        buf[offset] = self.min.unwrap_or(u8::MAX);
593        offset += 1;
594
595        // second
596        buf[offset] = self.sec.unwrap_or(u8::MAX);
597        offset += 1;
598
599        // attos
600        let attos = self.attos.unwrap_or(u64::MAX);
601        buf[offset..offset + 8].copy_from_slice(&attos.to_le_bytes());
602        offset += 8;
603
604        // offset (5 bytes)
605        let offset_bytes = self.offset.unwrap_or_default().to_wire_bytes();
606        buf[offset..offset + 5].copy_from_slice(&offset_bytes);
607        offset += 5;
608
609        // iana_name (49 bytes)
610        if let Some(name) = &self.iana_name {
611            let name_bytes = name.to_bytes();
612            buf[offset..offset + 49].copy_from_slice(&name_bytes);
613        }
614        offset += 49;
615
616        // is_leap_second
617        buf[offset] = if self.is_leap_sec { 1 } else { 0 };
618        offset += 1;
619
620        // scale
621        buf[offset] = self.scale as u8;
622        offset += 1;
623
624        // weekday
625        buf[offset] = self.wkday.map_or(255, |w| w.to_wire_byte());
626        offset += 1;
627
628        // day_of_year
629        let doy = self.day_of_yr.unwrap_or(u16::MAX);
630        buf[offset..offset + 2].copy_from_slice(&doy.to_le_bytes());
631        offset += 2;
632
633        // iso_week_year
634        let iso_y = self.iso_wk_yr.unwrap_or(i64::MIN);
635        buf[offset..offset + 8].copy_from_slice(&iso_y.to_le_bytes());
636        offset += 8;
637
638        // iso_week
639        buf[offset] = self.iso_wk.unwrap_or(u8::MAX);
640        offset += 1;
641
642        // week_sun
643        buf[offset] = self.wk_sun.unwrap_or(u8::MAX);
644        offset += 1;
645
646        // week_mon
647        buf[offset] = self.wk_mon.unwrap_or(u8::MAX);
648        offset += 1;
649
650        // meridiem
651        buf[offset] = self.meridiem.map_or(255, |m| m.to_wire_byte());
652        offset += 1;
653
654        // unix_timestamp_seconds
655        let unix = self.unix_timestamp_seconds.unwrap_or(i64::MIN);
656        buf[offset..offset + 8].copy_from_slice(&unix.to_le_bytes());
657
658        buf
659    }
660
661    /// Deserializes `TimeParts` from exactly 120 bytes.
662    ///
663    /// Returns `None` if the version byte is unknown or the data is invalid.
664    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
665        if bytes.len() != Self::WIRE_SIZE {
666            return None;
667        }
668        if bytes[0] != Self::WIRE_VERSION {
669            return None;
670        }
671
672        let mut dc = TimeParts::default();
673        let mut offset = 1usize;
674
675        // year (8 bytes)
676        let year = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
677        if year != i64::MIN {
678            dc.yr = Some(year);
679        }
680        offset += 8;
681
682        // month (1 byte)
683        let m = bytes[offset];
684        if m != u8::MAX {
685            dc.mo = Some(m);
686        }
687        offset += 1;
688
689        // day (1 byte)
690        let d = bytes[offset];
691        if d != u8::MAX {
692            dc.day = Some(d);
693        }
694        offset += 1;
695
696        // hour (1 byte)
697        let h = bytes[offset];
698        if h != u8::MAX {
699            dc.hr = Some(h);
700        }
701        offset += 1;
702
703        // minute (1 byte)
704        let min = bytes[offset];
705        if min != u8::MAX {
706            dc.min = Some(min);
707        }
708        offset += 1;
709
710        // second (1 byte)
711        let sec = bytes[offset];
712        if sec != u8::MAX {
713            dc.sec = Some(sec);
714        }
715        offset += 1;
716
717        // attos (8 bytes)
718        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
719        if attos != u64::MAX {
720            dc.attos = Some(attos);
721        }
722        offset += 8;
723
724        // offset (5 bytes) — already nice
725        if let Some(offset) = Offset::from_wire_bytes(&bytes[offset..offset + 5]) {
726            dc.offset = Some(offset);
727        }
728        offset += 5;
729
730        // iana_name (49 bytes) — already nice
731        let iana_bytes = &bytes[offset..offset + 49];
732        if let Some(name) = LiteStr::<49>::from_bytes(iana_bytes).ok()
733            && !name.len() == 0
734        {
735            dc.iana_name = Some(name);
736        }
737        offset += 49;
738
739        // is_leap_second (1 byte)
740        dc.is_leap_sec = bytes[offset] != 0;
741        offset += 1;
742
743        // scale (1 byte)
744        dc.scale = Scale::from_u8(bytes[offset]);
745        offset += 1;
746
747        // weekday (1 byte)
748        let wd_byte = bytes[offset];
749        if wd_byte != 255
750            && let Some(wd) = Weekday::from_wire_byte(wd_byte)
751        {
752            dc.wkday = Some(wd);
753        }
754        offset += 1;
755
756        // day_of_year (2 bytes)
757        let doy = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
758        if doy != u16::MAX {
759            dc.day_of_yr = Some(doy);
760        }
761        offset += 2;
762
763        // iso_week_year (8 bytes)
764        let iso_y = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
765        if iso_y != i64::MIN {
766            dc.iso_wk_yr = Some(iso_y);
767        }
768        offset += 8;
769
770        // iso_week (1 byte)
771        let iw = bytes[offset];
772        if iw != u8::MAX {
773            dc.iso_wk = Some(iw);
774        }
775        offset += 1;
776
777        // week_sun (1 byte)
778        let ws = bytes[offset];
779        if ws != u8::MAX {
780            dc.wk_sun = Some(ws);
781        }
782        offset += 1;
783
784        // week_mon (1 byte)
785        let wm = bytes[offset];
786        if wm != u8::MAX {
787            dc.wk_mon = Some(wm);
788        }
789        offset += 1;
790
791        // meridiem (1 byte)
792        let mer_byte = bytes[offset];
793        if mer_byte != 255
794            && let Some(m) = Meridiem::from_wire_byte(mer_byte)
795        {
796            dc.meridiem = Some(m);
797        }
798
799        offset += 1;
800
801        // unix_timestamp_seconds (8 bytes)
802        let unix = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
803        if unix != i64::MIN {
804            dc.unix_timestamp_seconds = Some(unix);
805        }
806
807        Some(dc)
808    }
809}