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}