1use crate::cursor::ManifestCursor;
31use crate::drop_record::{parse_drop_record, DropRecord, DROP_RECORD_LEN};
32use crate::error::CoreError;
33use crate::slab::{parse_slab_header, SlabHeader};
34
35#[derive(Debug, Clone)]
42pub struct SlabView<'a> {
43 bytes: &'a [u8],
44 header: SlabHeader,
45 drop_records: Vec<DropRecord>,
46 solid_window_start: usize,
47}
48
49impl SlabView<'_> {
50 #[must_use]
52 pub const fn header(&self) -> SlabHeader {
53 self.header
54 }
55
56 #[must_use]
58 pub fn drop_records(&self) -> &[DropRecord] {
59 &self.drop_records
60 }
61
62 #[must_use]
65 pub const fn solid_window_offset(&self) -> usize {
66 self.solid_window_start
67 }
68
69 #[must_use]
71 pub fn find_record(&self, drop_id: &[u8; 32]) -> Option<&DropRecord> {
72 self.drop_records
73 .iter()
74 .find(|r| r.drop_id.as_bytes() == drop_id)
75 }
76
77 #[must_use]
95 pub fn plaintext_for(&self, drop_id: &[u8; 32]) -> Option<Result<Vec<u8>, CoreError>> {
96 self.plaintext_for_with_dict_lookup(drop_id, &|_| None)
97 }
98
99 #[must_use]
109 pub fn plaintext_for_with_dict_lookup(
110 &self,
111 drop_id: &[u8; 32],
112 dict_lookup: &dyn Fn(u8) -> Option<Vec<u8>>,
113 ) -> Option<Result<Vec<u8>, CoreError>> {
114 let record = self.find_record(drop_id)?;
115 if record.representation.aead != 0x00 {
116 return Some(Err(CoreError::UnsupportedFeature {
117 feature: format!(
118 "drop aead 0x{:02X} (only plaintext/0x00 supported in v0.1)",
119 record.representation.aead
120 ),
121 }));
122 }
123 if record.solid_window_index != 0 {
124 return Some(Err(CoreError::UnsupportedFeature {
125 feature: format!(
126 "solid_window_index {} (only single-window slabs supported in v0.1)",
127 record.solid_window_index
128 ),
129 }));
130 }
131 let offset = usize::try_from(record.offset_in_window).ok()?;
132 let len = usize::try_from(record.len_in_window).ok()?;
133 let start = self.solid_window_start.checked_add(offset)?;
134 let end = start.checked_add(len)?;
135 if end > self.bytes.len() {
136 return Some(Err(CoreError::Corrupt {
137 reason: format!(
138 "drop range [{start}..{end}] extends past slab length {}",
139 self.bytes.len()
140 ),
141 }));
142 }
143 let raw = &self.bytes[start..end];
144 if record.flags & crate::seekable::DROP_FLAG_SEEKABLE != 0 {
145 if record.dict_id != crate::drop_record::NO_DICT {
146 return Some(Err(CoreError::UnsupportedFeature {
147 feature: "seekable drop with trained dictionary (not combinable)".into(),
148 }));
149 }
150 return Some(crate::seekable::decode_seekable(
151 record.representation.codec,
152 raw,
153 record.plaintext_len,
154 ));
155 }
156 if record.dict_id == crate::drop_record::NO_DICT {
157 Some(crate::codec::decompress(
158 record.representation.codec,
159 raw,
160 record.plaintext_len,
161 ))
162 } else {
163 let Some(dict_bytes) = dict_lookup(record.dict_id) else {
166 return Some(Err(CoreError::Corrupt {
167 reason: format!(
168 "drop references dict_id 0x{:02X} but no dictionary_section provided",
169 record.dict_id
170 ),
171 }));
172 };
173 Some(crate::codec::zstd_dict::decompress_with_dict(
174 raw,
175 record.plaintext_len,
176 &dict_bytes,
177 ))
178 }
179 }
180
181 #[must_use]
193 pub fn plaintext_range(
194 &self,
195 drop_id: &[u8; 32],
196 off: u64,
197 len: usize,
198 ) -> Option<Result<Vec<u8>, CoreError>> {
199 let record = self.find_record(drop_id)?;
200 if record.flags & crate::seekable::DROP_FLAG_SEEKABLE != 0 {
201 let (offset, end) = match self.drop_window_bounds(record) {
202 Ok(v) => v,
203 Err(e) => return Some(Err(e)),
204 };
205 let raw = &self.bytes[offset..end];
206 return Some(crate::seekable::decode_seekable_range(
207 record.representation.codec,
208 raw,
209 off,
210 len,
211 ));
212 }
213 let plaintext = self.plaintext_for(drop_id)?;
215 Some(match plaintext {
216 Ok(bytes) => {
217 let total = bytes.len() as u64;
218 if off > total || off + len as u64 > total {
219 Err(CoreError::Corrupt {
220 reason: format!(
221 "drop range [{off}, {}) outside plaintext length {total}",
222 off + len as u64
223 ),
224 })
225 } else {
226 Ok(bytes[off as usize..off as usize + len].to_vec())
227 }
228 }
229 Err(e) => Err(e),
230 })
231 }
232
233 fn drop_window_bounds(&self, record: &DropRecord) -> Result<(usize, usize), CoreError> {
235 let offset = usize::try_from(record.offset_in_window).map_err(|_| CoreError::Corrupt {
236 reason: "drop offset_in_window exceeds usize".into(),
237 })?;
238 let len = usize::try_from(record.len_in_window).map_err(|_| CoreError::Corrupt {
239 reason: "drop len_in_window exceeds usize".into(),
240 })?;
241 let start =
242 self.solid_window_start
243 .checked_add(offset)
244 .ok_or_else(|| CoreError::Corrupt {
245 reason: "drop window start overflows usize".into(),
246 })?;
247 let end = start.checked_add(len).ok_or_else(|| CoreError::Corrupt {
248 reason: "drop window end overflows usize".into(),
249 })?;
250 if end > self.bytes.len() {
251 return Err(CoreError::Corrupt {
252 reason: format!(
253 "drop range [{start}..{end}] extends past slab length {}",
254 self.bytes.len()
255 ),
256 });
257 }
258 Ok((start, end))
259 }
260}
261
262pub fn parse_slab(bytes: &[u8]) -> Result<SlabView<'_>, CoreError> {
276 let mut cursor = ManifestCursor::new(bytes);
277 let header = parse_slab_header(&mut cursor)?;
278 let total_length = usize::try_from(header.total_length).map_err(|_| CoreError::Corrupt {
279 reason: format!("slab total_length {} exceeds usize", header.total_length),
280 })?;
281 if total_length != bytes.len() {
282 return Err(CoreError::Corrupt {
283 reason: format!(
284 "slab total_length {total_length} does not match buffer length {}",
285 bytes.len()
286 ),
287 });
288 }
289
290 let mut drop_records: Vec<DropRecord> = Vec::new();
291 let mut window_len_sum: u64 = 0;
292 loop {
293 let cursor_pos = u64::try_from(cursor.position()).map_err(|_| CoreError::Corrupt {
294 reason: format!("slab cursor position {} exceeds u64", cursor.position()),
295 })?;
296 let remaining_after_cursor =
297 header
298 .total_length
299 .checked_sub(cursor_pos)
300 .ok_or_else(|| CoreError::Corrupt {
301 reason: format!(
302 "slab cursor position {cursor_pos} past total_length {}",
303 header.total_length
304 ),
305 })?;
306 if remaining_after_cursor == window_len_sum {
307 break;
308 }
309 if remaining_after_cursor < window_len_sum {
310 return Err(CoreError::Corrupt {
311 reason: format!(
312 "slab drop records overran solid window: cursor_pos={cursor_pos}, window_sum={window_len_sum}, total_length={}",
313 header.total_length
314 ),
315 });
316 }
317 let trailing = remaining_after_cursor - window_len_sum;
318 if trailing < u64::try_from(DROP_RECORD_LEN).unwrap_or(u64::MAX) {
319 return Err(CoreError::Corrupt {
320 reason: format!(
321 "slab has {trailing} trailing bytes that are neither a full drop record ({DROP_RECORD_LEN}B) nor accounted for by the solid window"
322 ),
323 });
324 }
325 let record = parse_drop_record(&mut cursor, &header)?;
326 window_len_sum = window_len_sum
327 .checked_add(u64::from(record.len_in_window))
328 .ok_or_else(|| CoreError::Corrupt {
329 reason: format!(
330 "slab drop len_in_window sum overflow at record {}",
331 drop_records.len()
332 ),
333 })?;
334 drop_records.push(record);
335 }
336
337 let solid_window_start = cursor.position();
338 Ok(SlabView {
339 bytes,
340 header,
341 drop_records,
342 solid_window_start,
343 })
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::slab::SLAB_HEADER_LEN;
350 use limnifs_format::DropId;
351
352 fn make_slab(drops: &[(&[u8; 32], &[u8])]) -> Vec<u8> {
353 let mut drop_records = Vec::new();
354 let mut solid_window = Vec::new();
355 for (id, plaintext) in drops {
356 let plaintext_len = u32::try_from(plaintext.len()).unwrap();
357 let offset_in_window = u32::try_from(solid_window.len()).unwrap();
358 drop_records.extend_from_slice(*id);
359 drop_records.extend_from_slice(&plaintext_len.to_le_bytes());
360 drop_records.extend_from_slice(&[0x00, 0x00, 0x00]); drop_records.push(0x00); drop_records.extend_from_slice(&offset_in_window.to_le_bytes());
363 drop_records.extend_from_slice(&plaintext_len.to_le_bytes());
364 drop_records.push(crate::drop_record::NO_DICT); drop_records.push(0x00); solid_window.extend_from_slice(plaintext);
367 }
368 let slab_content = [&drop_records[..], &solid_window[..]].concat();
369 let total_length = u64::try_from(SLAB_HEADER_LEN + slab_content.len()).unwrap();
370 let mut bytes = Vec::with_capacity(usize::try_from(total_length).expect("fits usize"));
371 bytes.extend_from_slice(b"LIM1");
372 bytes.extend_from_slice(&1u16.to_le_bytes());
373 bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&[0u8; 32]); bytes.extend_from_slice(&total_length.to_le_bytes());
376 bytes.push(0x00); bytes.push(0x00); bytes.extend_from_slice(&slab_content);
379 bytes
380 }
381
382 #[test]
383 fn parses_empty_slab() {
384 let bytes = make_slab(&[]);
385 let view = parse_slab(&bytes).expect("empty slab parses");
386 assert_eq!(view.drop_records().len(), 0);
387 }
388
389 #[test]
390 fn parses_single_drop() {
391 let id = [0xAA; 32];
392 let plaintext = b"hello world";
393 let bytes = make_slab(&[(&id, plaintext)]);
394 let view = parse_slab(&bytes).expect("single-drop slab parses");
395 assert_eq!(view.drop_records().len(), 1);
396 let got = view
397 .plaintext_for(&id)
398 .expect("drop present")
399 .expect("store codec ok");
400 assert_eq!(got, plaintext);
401 }
402
403 #[test]
404 fn parses_multiple_drops() {
405 let id1 = [0x11; 32];
406 let id2 = [0x22; 32];
407 let id3 = [0x33; 32];
408 let p1 = b"first drop plaintext";
409 let p2 = b"second";
410 let p3 = b"third drop is longer than the others combined";
411 let bytes = make_slab(&[(&id1, p1), (&id2, p2), (&id3, p3)]);
412 let view = parse_slab(&bytes).expect("multi-drop slab parses");
413 assert_eq!(view.drop_records().len(), 3);
414 assert_eq!(
415 view.plaintext_for(&id1)
416 .expect("drop 1 present")
417 .expect("store codec ok"),
418 p1
419 );
420 assert_eq!(
421 view.plaintext_for(&id2)
422 .expect("drop 2 present")
423 .expect("store codec ok"),
424 p2
425 );
426 assert_eq!(
427 view.plaintext_for(&id3)
428 .expect("drop 3 present")
429 .expect("store codec ok"),
430 p3
431 );
432 }
433
434 #[test]
435 fn missing_drop_returns_none() {
436 let id = [0xAA; 32];
437 let bytes = make_slab(&[(&id, b"data")]);
438 let view = parse_slab(&bytes).expect("slab parses");
439 let missing = DropId::from_bytes([0xBB; 32]);
440 assert!(view.plaintext_for(missing.as_bytes()).is_none());
441 }
442
443 #[test]
444 fn rejects_buffer_length_mismatch() {
445 let id = [0xAA; 32];
446 let mut bytes = make_slab(&[(&id, b"data")]);
447 bytes.truncate(bytes.len() - 1);
448 match parse_slab(&bytes) {
449 Err(CoreError::Corrupt { reason }) => {
450 assert!(
451 reason.contains("does not match buffer length"),
452 "got: {reason}"
453 );
454 }
455 other => panic!("expected Corrupt, got {other:?}"),
456 }
457 }
458
459 #[test]
460 fn slab_from_writer_round_trips() {
461 let id1 = [0x11; 32];
464 let id2 = [0x22; 32];
465 let p1 = vec![0xAB; 4096];
466 let p2 = vec![0xCD; 1024];
467 let bytes = make_slab(&[(&id1, &p1), (&id2, &p2)]);
468 let view = parse_slab(&bytes).expect("writer-style slab parses");
469 assert_eq!(view.drop_records().len(), 2);
470 assert_eq!(
471 view.plaintext_for(&id1)
472 .expect("drop 1 present")
473 .expect("ok"),
474 &p1[..]
475 );
476 assert_eq!(
477 view.plaintext_for(&id2)
478 .expect("drop 2 present")
479 .expect("ok"),
480 &p2[..]
481 );
482 }
483}