Skip to main content

limnifs_core/
slab.rs

1//! Slab header (spec §3.2, `bit-level/30-slab-header.md`).
2//!
3//! The 56-byte fixed-size prefix at offset 0 of every slab in the
4//! drop store. Magic `LIM1`, u16 LE `format_version`, `SlabId`
5//! (ordinal + hash), u64 LE `total_length`, u8 `ec_descriptor`, u8
6//! `crypto_hint`.
7
8use crate::cursor::ManifestCursor;
9use crate::error::CoreError;
10use limnifs_format::{SlabId, SLAB_MAGIC};
11
12/// Default slab-size ceiling per [§3.1](https://github.com/limnifs/spec/blob/main/wire-format/21-drop-store.md):
13/// slabs MUST be ≤ 64 MiB unless the manifest overrides.
14pub const DEFAULT_SLAB_MAX_BYTES: u64 = 64 * 1024 * 1024;
15
16/// Current (and only) slab layout version (matches `format_version`
17/// byte 4..6). Alpha software: there is no version history — this
18/// layout (50-byte drop records with trailing `flags` byte, seekable
19/// containers per `crate::seekable`) IS the format. Any other value
20/// fails closed.
21pub const SLAB_FORMAT_VERSION: u16 = 1;
22
23/// Width of the fixed slab header.
24pub const SLAB_HEADER_LEN: usize = 56;
25
26/// Sentinel value for `ec_descriptor` indicating an extended (post-v1)
27/// descriptor follows. Readers in v1 reject with `UnsupportedFeature`.
28pub const EC_DESCRIPTOR_EXTENDED: u8 = 0xFF;
29
30/// Sentinel value for `crypto_hint` indicating an extended (post-v1)
31/// hint follows. Readers in v1 reject with `UnsupportedFeature`.
32pub const CRYPTO_HINT_EXTENDED: u8 = 0xFF;
33
34/// Parsed slab header.
35#[derive(Copy, Clone, Debug, Eq, PartialEq)]
36pub struct SlabHeader {
37    pub format_version: u16,
38    pub slab_id: SlabId,
39    pub total_length: u64,
40    pub ec_descriptor: u8,
41    pub crypto_hint: u8,
42}
43
44impl SlabHeader {
45    /// True iff this slab carries Reed-Solomon parity shards.
46    #[must_use]
47    pub const fn has_erasure_coding(self) -> bool {
48        self.ec_descriptor != 0x00 && self.ec_descriptor != EC_DESCRIPTOR_EXTENDED
49    }
50
51    /// True iff this slab's payload is sealed with an AEAD.
52    #[must_use]
53    pub const fn is_sealed(self) -> bool {
54        self.crypto_hint != 0x00 && self.crypto_hint != CRYPTO_HINT_EXTENDED
55    }
56}
57
58/// Parse a slab header from the cursor's current position.
59///
60/// Advances the cursor by [`SLAB_HEADER_LEN`] bytes on success.
61/// Validates magic, format version, `total_length` floor (must be at
62/// least the header width), and rejects extended descriptors/hints
63/// (`0xFF`) with [`CoreError::UnsupportedFeature`].
64///
65/// # Errors
66///
67/// - [`CoreError::BadMagic`] if the first 4 bytes are not `LIM1`.
68/// - [`CoreError::TooShort`] if the cursor has fewer than 56 bytes.
69/// - [`CoreError::UnsupportedFeature`] if `format_version` is not 1,
70///   or if `ec_descriptor == 0xFF`, or if `crypto_hint == 0xFF`.
71/// - [`CoreError::Corrupt`] if `total_length < SLAB_HEADER_LEN`.
72pub fn parse_slab_header(cursor: &mut ManifestCursor<'_>) -> Result<SlabHeader, CoreError> {
73    parse_slab_header_with_ceiling(cursor, DEFAULT_SLAB_MAX_BYTES)
74}
75
76/// Same as [`parse_slab_header`] but lets the caller supply a
77/// `max_total_length` overriding the 64 MiB default. Used by readers
78/// that have parsed a manifest with a non-default slab-size parameter.
79///
80/// # Errors
81///
82/// Inherits all errors from [`parse_slab_header`], and additionally
83/// returns [`CoreError::Corrupt`] when `total_length > max_total_length`.
84pub fn parse_slab_header_with_ceiling(
85    cursor: &mut ManifestCursor<'_>,
86    max_total_length: u64,
87) -> Result<SlabHeader, CoreError> {
88    let magic = cursor.read_magic()?;
89    if magic != SLAB_MAGIC {
90        // BadMagic's Display says "manifest"/"LMFS"; spell out the
91        // slab magic here so the message matches what was parsed.
92        return Err(CoreError::Corrupt {
93            reason: format!(
94                "bad slab magic: expected LIM1 ({:x?}), found {:x?}",
95                SLAB_MAGIC, magic
96            ),
97        });
98    }
99    let format_version = cursor.read_u16_le()?;
100    if format_version != SLAB_FORMAT_VERSION {
101        return Err(CoreError::UnsupportedFeature {
102            feature: format!(
103                "slab format_version {format_version} (supported: {SLAB_FORMAT_VERSION})"
104            ),
105        });
106    }
107    let ordinal = cursor.read_u64_le()?;
108    let hash = cursor.read_n(32)?;
109    let mut hash_array = [0u8; 32];
110    hash_array.copy_from_slice(hash);
111    let slab_id = SlabId::new(ordinal, hash_array);
112    let total_length = cursor.read_u64_le()?;
113    if total_length < u64::try_from(SLAB_HEADER_LEN).unwrap_or(u64::MAX) {
114        return Err(CoreError::Corrupt {
115            reason: format!(
116                "slab total_length {total_length} is less than header width {SLAB_HEADER_LEN}"
117            ),
118        });
119    }
120    if total_length > max_total_length {
121        return Err(CoreError::Corrupt {
122            reason: format!(
123                "slab total_length {total_length} exceeds configured ceiling {max_total_length}"
124            ),
125        });
126    }
127    let ec_descriptor = cursor.read_u8()?;
128    if ec_descriptor == EC_DESCRIPTOR_EXTENDED {
129        return Err(CoreError::UnsupportedFeature {
130            feature: "slab ec_descriptor 0xFF (extended descriptor, post-v1)".into(),
131        });
132    }
133    let crypto_hint = cursor.read_u8()?;
134    if crypto_hint == CRYPTO_HINT_EXTENDED {
135        return Err(CoreError::UnsupportedFeature {
136            feature: "slab crypto_hint 0xFF (extended hint, post-v1)".into(),
137        });
138    }
139    Ok(SlabHeader {
140        format_version,
141        slab_id,
142        total_length,
143        ec_descriptor,
144        crypto_hint,
145    })
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn make_slab_header_bytes(
153        format_version: u16,
154        slab_id: SlabId,
155        total_length: u64,
156        ec_descriptor: u8,
157        crypto_hint: u8,
158    ) -> [u8; SLAB_HEADER_LEN] {
159        let mut bytes = [0u8; SLAB_HEADER_LEN];
160        bytes[..4].copy_from_slice(&SLAB_MAGIC);
161        bytes[4..6].copy_from_slice(&format_version.to_le_bytes());
162        let slab_id_bytes = slab_id.to_bytes();
163        bytes[6..46].copy_from_slice(&slab_id_bytes);
164        bytes[46..54].copy_from_slice(&total_length.to_le_bytes());
165        bytes[54] = ec_descriptor;
166        bytes[55] = crypto_hint;
167        bytes
168    }
169
170    fn sample_slab_id() -> SlabId {
171        SlabId::new(7, [0xAA; 32])
172    }
173
174    #[test]
175    fn parses_current_plaintext_header() {
176        let bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0x00, 0x00);
177        let mut cursor = ManifestCursor::new(&bytes);
178        let header = parse_slab_header(&mut cursor).expect("current header parses");
179        assert_eq!(header.format_version, 1);
180        assert_eq!(header.slab_id, sample_slab_id());
181        assert_eq!(header.total_length, 4096);
182        assert_eq!(header.ec_descriptor, 0x00);
183        assert_eq!(header.crypto_hint, 0x00);
184        assert!(!header.has_erasure_coding());
185        assert!(!header.is_sealed());
186        assert_eq!(cursor.position(), SLAB_HEADER_LEN);
187    }
188
189    #[test]
190    fn parses_ec_enabled_sealed_header() {
191        let bytes = make_slab_header_bytes(1, sample_slab_id(), 16_384, 0x01, 0x01);
192        let mut cursor = ManifestCursor::new(&bytes);
193        let header = parse_slab_header(&mut cursor).expect("EC + sealed parses");
194        assert!(header.has_erasure_coding());
195        assert!(header.is_sealed());
196    }
197
198    #[test]
199    fn rejects_bad_magic() {
200        let mut bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0, 0);
201        bytes[0] = b'X';
202        let mut cursor = ManifestCursor::new(&bytes);
203        match parse_slab_header(&mut cursor) {
204            Err(CoreError::Corrupt { reason }) => {
205                assert!(reason.contains("bad slab magic"), "got {reason}");
206            }
207            other => panic!("expected Corrupt, got {other:?}"),
208        }
209    }
210
211    #[test]
212    fn rejects_unknown_format_version() {
213        let bytes = make_slab_header_bytes(7, sample_slab_id(), 4096, 0, 0);
214        let mut cursor = ManifestCursor::new(&bytes);
215        match parse_slab_header(&mut cursor) {
216            Err(CoreError::UnsupportedFeature { feature }) => {
217                assert!(feature.contains("format_version 7"), "got: {feature}");
218            }
219            other => panic!("expected UnsupportedFeature, got {other:?}"),
220        }
221    }
222
223    #[test]
224    fn rejects_total_length_below_header_width() {
225        let bytes = make_slab_header_bytes(1, sample_slab_id(), 32, 0, 0);
226        let mut cursor = ManifestCursor::new(&bytes);
227        match parse_slab_header(&mut cursor) {
228            Err(CoreError::Corrupt { reason }) => {
229                assert!(reason.contains("header width"), "got: {reason}");
230            }
231            other => panic!("expected Corrupt, got {other:?}"),
232        }
233    }
234
235    #[test]
236    fn rejects_total_length_above_default_ceiling() {
237        let bytes = make_slab_header_bytes(1, sample_slab_id(), DEFAULT_SLAB_MAX_BYTES + 1, 0, 0);
238        let mut cursor = ManifestCursor::new(&bytes);
239        match parse_slab_header(&mut cursor) {
240            Err(CoreError::Corrupt { reason }) => {
241                assert!(reason.contains("ceiling"), "got: {reason}");
242            }
243            other => panic!("expected Corrupt, got {other:?}"),
244        }
245    }
246
247    #[test]
248    fn custom_ceiling_accepts_oversized_slab() {
249        let bytes = make_slab_header_bytes(1, sample_slab_id(), DEFAULT_SLAB_MAX_BYTES + 1, 0, 0);
250        let mut cursor = ManifestCursor::new(&bytes);
251        let header = parse_slab_header_with_ceiling(&mut cursor, DEFAULT_SLAB_MAX_BYTES * 2)
252            .expect("custom ceiling accepts");
253        assert_eq!(header.total_length, DEFAULT_SLAB_MAX_BYTES + 1);
254    }
255
256    #[test]
257    fn rejects_extended_ec_descriptor() {
258        let bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0xFF, 0);
259        let mut cursor = ManifestCursor::new(&bytes);
260        match parse_slab_header(&mut cursor) {
261            Err(CoreError::UnsupportedFeature { feature }) => {
262                assert!(feature.contains("ec_descriptor"), "got: {feature}");
263            }
264            other => panic!("expected UnsupportedFeature, got {other:?}"),
265        }
266    }
267
268    #[test]
269    fn rejects_extended_crypto_hint() {
270        let bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0, 0xFF);
271        let mut cursor = ManifestCursor::new(&bytes);
272        match parse_slab_header(&mut cursor) {
273            Err(CoreError::UnsupportedFeature { feature }) => {
274                assert!(feature.contains("crypto_hint"), "got: {feature}");
275            }
276            other => panic!("expected UnsupportedFeature, got {other:?}"),
277        }
278    }
279
280    #[test]
281    fn rejects_truncated_header() {
282        // Valid magic + valid format_version, but the slab_id and
283        // remaining fields do not fit. Cursor returns TooShort when
284        // it reaches the missing bytes.
285        let mut bytes = [0u8; 50];
286        bytes[..4].copy_from_slice(&SLAB_MAGIC);
287        bytes[4..6].copy_from_slice(&SLAB_FORMAT_VERSION.to_le_bytes());
288        let mut cursor = ManifestCursor::new(&bytes);
289        match parse_slab_header(&mut cursor) {
290            Err(CoreError::TooShort { .. }) => {}
291            other => panic!("expected TooShort, got {other:?}"),
292        }
293    }
294
295    #[test]
296    fn round_trip_via_constructor() {
297        let header = SlabHeader {
298            format_version: SLAB_FORMAT_VERSION,
299            slab_id: sample_slab_id(),
300            total_length: 8192,
301            ec_descriptor: 0x01,
302            crypto_hint: 0x01,
303        };
304        let bytes = make_slab_header_bytes(
305            header.format_version,
306            header.slab_id,
307            header.total_length,
308            header.ec_descriptor,
309            header.crypto_hint,
310        );
311        let mut cursor = ManifestCursor::new(&bytes);
312        let reparsed = parse_slab_header(&mut cursor).expect("roundtrip");
313        assert_eq!(reparsed, header);
314    }
315}