Skip to main content

segb/
common.rs

1//! Shared types used by both SEGB v1 and v2 decoders.
2//!
3//! # References
4//!
5//! `ccl_segb/ccl_segb_common.py` — Alex Caithness / CCL Solutions
6//! <https://github.com/cclgroupltd/ccl-segb/blob/main/ccl_segb/ccl_segb_common.py>
7
8use crate::error::{Result, SegbError};
9
10/// The 4-byte ASCII magic shared by both SEGB variants.
11pub const MAGIC: &[u8; 4] = b"SEGB";
12
13/// Mac Absolute Time (Cocoa / `CFAbsoluteTime`) epoch — 2001-01-01T00:00:00Z.
14///
15/// Timestamps in SEGB records are stored as `f64` seconds since this epoch.
16/// Source: `ccl_segb_common.py:COCOA_EPOCH`.
17pub const COCOA_EPOCH_UNIX_SECS: f64 = 978_307_200.0;
18
19/// The state of a SEGB record.
20///
21/// Values are sourced from `ccl_segb_common.py:EntryState`:
22///
23/// | Value | Name    | Meaning                            |
24/// |-------|---------|------------------------------------|
25/// | 1     | Written | Normal, live record                |
26/// | 3     | Deleted | Logically deleted record           |
27/// | 4     | Unknown | Placeholder / empty record (v2)    |
28///
29/// `Unknown` (4) in SEGB v2 marks a slot the code skips; it is exposed here
30/// so callers can observe it without the reader silently dropping the entry.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub enum EntryState {
34    /// Normal, live record. Value = 1.
35    Written,
36    /// Logically deleted record. Value = 3.
37    Deleted,
38    /// Empty placeholder (primarily SEGB v2). Value = 4.
39    Unknown,
40}
41
42impl EntryState {
43    /// Decode from the raw `i32` on-disk value.
44    pub fn from_raw(v: i32) -> Result<Self> {
45        match v {
46            1 => Ok(Self::Written),
47            3 => Ok(Self::Deleted),
48            4 => Ok(Self::Unknown),
49            _ => Err(SegbError::UnknownState(v)),
50        }
51    }
52
53    /// Return `true` if this is a live (`Written`) record.
54    #[inline]
55    pub fn is_live(self) -> bool {
56        self == Self::Written
57    }
58}
59
60/// Convert a Cocoa `f64` timestamp (seconds since 2001-01-01) to seconds
61/// since the Unix epoch (1970-01-01). Returns `None` if the value is not
62/// finite (NaN / ±Inf).
63///
64/// Source: `ccl_segb_common.py:decode_cocoa_time`.
65#[inline]
66pub fn cocoa_to_unix_secs(cocoa: f64) -> Option<f64> {
67    if cocoa.is_finite() {
68        Some(cocoa + COCOA_EPOCH_UNIX_SECS)
69    } else {
70        None
71    }
72}
73
74/// Read a little-endian `i32` from `data[off..off+4]`.
75/// Returns 0 if the slice is shorter than required (bounds-safe).
76#[inline]
77pub(crate) fn le_i32(data: &[u8], off: usize) -> i32 {
78    let mut b = [0u8; 4];
79    if let Some(s) = data.get(off..off + 4) {
80        b.copy_from_slice(s);
81    }
82    i32::from_le_bytes(b)
83}
84
85/// Read a little-endian `u32` from `data[off..off+4]`.
86/// Returns 0 if the slice is shorter than required (bounds-safe).
87#[inline]
88pub(crate) fn le_u32(data: &[u8], off: usize) -> u32 {
89    let mut b = [0u8; 4];
90    if let Some(s) = data.get(off..off + 4) {
91        b.copy_from_slice(s);
92    }
93    u32::from_le_bytes(b)
94}
95
96/// Read a little-endian `f64` from `data[off..off+8]`.
97/// Returns `f64::NAN` if the slice is shorter than required (bounds-safe).
98#[inline]
99pub(crate) fn le_f64(data: &[u8], off: usize) -> f64 {
100    let mut b = [0u8; 8];
101    if let Some(s) = data.get(off..off + 8) {
102        b.copy_from_slice(s);
103    } else {
104        return f64::NAN;
105    }
106    f64::from_le_bytes(b)
107}
108
109/// Read a little-endian `i64` (cast from `i32` pair) — used for `entries_count`
110/// in the SEGB v2 header where the Python struct uses `"i"` (signed 32-bit).
111#[inline]
112pub(crate) fn le_i32_at(data: &[u8], off: usize) -> i32 {
113    le_i32(data, off)
114}