segb/lib.rs
1//! `segb-core` — panic-free reader for Apple SEGB container files.
2//!
3//! # What is SEGB?
4//!
5//! SEGB is Apple's container format used by the **Biome** subsystem on macOS
6//! and iOS to store user-activity streams. Each Biome stream (e.g.
7//! `~/Library/Biome/streams/restricted/App.MenuItem/local`) is a SEGB file
8//! whose records carry a state flag, one or two timestamps, and a raw protobuf
9//! payload.
10//!
11//! Two variants exist:
12//!
13//! | Variant | Magic location | Header size | Alignment |
14//! |---------|--------------------|-------------|-----------|
15//! | SEGB v1 | Last 4 bytes of header (offset 52–55) | 56 bytes | 8 bytes |
16//! | SEGB v2 | First 4 bytes (offset 0–3) | 32 bytes | 4 bytes |
17//!
18//! # Quick start
19//!
20//! ```rust,no_run
21//! use std::fs::File;
22//! use std::io::BufReader;
23//! use segb::{read_segb, SegbRecord, menuitem::decode_app_menu_item};
24//!
25//! let f = File::open("/path/to/App.MenuItem/local").unwrap();
26//! let mut r = BufReader::new(f);
27//! for record in read_segb(&mut r).unwrap() {
28//! let item = decode_app_menu_item(record.payload(), record.timestamp_unix()).unwrap();
29//! println!("{:?} → {:?}", item.application, item.menu_item);
30//! }
31//! ```
32//!
33//! # References
34//!
35//! - ccl-segb (Alex Caithness / CCL Solutions):
36//! <https://github.com/cclgroupltd/ccl-segb>
37//! - Unit 42 research (Palo Alto Networks, 2026):
38//! <https://unit42.paloaltonetworks.com/new-macos-artifact-discovered/>
39//! - forensicnomicon catalog entry `macos_biome_app_menuitem`
40
41#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
42
43pub mod common;
44pub mod error;
45pub mod menuitem;
46pub mod proto;
47pub mod segb1;
48pub mod segb2;
49
50use std::io::{Read, Seek};
51
52pub use common::EntryState;
53pub use error::{Result, SegbError};
54
55// Re-exports for convenience.
56pub use segb1::SegbV1Record;
57pub use segb2::SegbV2Record;
58
59/// A version-neutral SEGB record, produced by [`read_segb`].
60#[derive(Debug, Clone)]
61pub enum SegbRecord {
62 /// A record from a SEGB v1 file.
63 V1(SegbV1Record),
64 /// A record from a SEGB v2 file.
65 V2(SegbV2Record),
66}
67
68impl SegbRecord {
69 /// The logical state of this record.
70 pub fn state(&self) -> EntryState {
71 match self {
72 Self::V1(r) => r.state,
73 Self::V2(r) => r.state,
74 }
75 }
76
77 /// The primary timestamp as Unix seconds (seconds since 1970-01-01T00:00:00Z).
78 ///
79 /// - For v1: `timestamp1`.
80 /// - For v2: the trailer `creation` timestamp.
81 ///
82 /// Returns `None` if the stored `f64` is not finite.
83 pub fn timestamp_unix(&self) -> Option<f64> {
84 match self {
85 Self::V1(r) => r.timestamp1_unix,
86 Self::V2(r) => r.timestamp_unix,
87 }
88 }
89
90 /// The raw protobuf (or other format) payload bytes.
91 pub fn payload(&self) -> &[u8] {
92 match self {
93 Self::V1(r) => &r.payload,
94 Self::V2(r) => &r.payload,
95 }
96 }
97
98 /// File offset of the first byte of [`Self::payload`].
99 pub fn data_offset(&self) -> u64 {
100 match self {
101 Self::V1(r) => r.data_offset,
102 Self::V2(r) => r.data_offset,
103 }
104 }
105
106 /// The CRC-32 stored in the record header.
107 pub fn stored_crc32(&self) -> u32 {
108 match self {
109 Self::V1(r) => r.stored_crc32,
110 Self::V2(r) => r.stored_crc32,
111 }
112 }
113
114 /// The CRC-32 computed over the payload bytes as read.
115 pub fn computed_crc32(&self) -> u32 {
116 match self {
117 Self::V1(r) => r.computed_crc32,
118 Self::V2(r) => r.computed_crc32,
119 }
120 }
121
122 /// Returns `true` if stored and computed CRC-32 values match.
123 pub fn crc_ok(&self) -> bool {
124 self.stored_crc32() == self.computed_crc32()
125 }
126}
127
128/// Detect the SEGB variant in `r` and read all records.
129///
130/// The stream is rewound to position 0 before detection. This is the primary
131/// entry point for callers that do not know which variant to expect.
132///
133/// # Errors
134///
135/// Returns `Err` if the stream is neither a valid SEGB v1 nor a valid SEGB v2
136/// file, or if a lower-level parse error occurs.
137pub fn read_segb<R: Read + Seek>(r: &mut R) -> Result<Vec<SegbRecord>> {
138 r.seek(std::io::SeekFrom::Start(0))?;
139
140 if segb1::is_segb_v1(r) {
141 r.seek(std::io::SeekFrom::Start(0))?;
142 let records = segb1::read_v1(r)?;
143 return Ok(records.into_iter().map(SegbRecord::V1).collect());
144 }
145
146 r.seek(std::io::SeekFrom::Start(0))?;
147 if segb2::is_segb_v2(r) {
148 r.seek(std::io::SeekFrom::Start(0))?;
149 let records = segb2::read_v2(r)?;
150 return Ok(records.into_iter().map(SegbRecord::V2).collect());
151 }
152
153 Err(SegbError::BadMagic {
154 found: "not SEGB v1 or v2".to_owned(),
155 })
156}