Skip to main content

deep_time/
wire.rs

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