1use crate::cursor::ManifestCursor;
9use crate::error::CoreError;
10use limnifs_format::{SlabId, SLAB_MAGIC};
11
12pub const DEFAULT_SLAB_MAX_BYTES: u64 = 64 * 1024 * 1024;
15
16pub const SLAB_FORMAT_VERSION: u16 = 1;
22
23pub const SLAB_HEADER_LEN: usize = 56;
25
26pub const EC_DESCRIPTOR_EXTENDED: u8 = 0xFF;
29
30pub const CRYPTO_HINT_EXTENDED: u8 = 0xFF;
33
34#[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 #[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 #[must_use]
53 pub const fn is_sealed(self) -> bool {
54 self.crypto_hint != 0x00 && self.crypto_hint != CRYPTO_HINT_EXTENDED
55 }
56}
57
58pub fn parse_slab_header(cursor: &mut ManifestCursor<'_>) -> Result<SlabHeader, CoreError> {
73 parse_slab_header_with_ceiling(cursor, DEFAULT_SLAB_MAX_BYTES)
74}
75
76pub 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 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 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}