1#![allow(
6 clippy::cast_lossless,
7 clippy::cast_possible_truncation,
8 clippy::cast_possible_wrap,
9 clippy::doc_markdown,
10 clippy::items_after_statements,
11 clippy::similar_names,
12 clippy::unreadable_literal
13)]
14
15use alloc::format;
88use alloc::string::String;
89use alloc::vec;
90use alloc::vec::Vec;
91use core::fmt;
92
93use spg_crypto::crc32::crc32;
94
95use crate::bloom::{BloomError, BloomFilter};
96
97pub const SEGMENT_MAGIC: [u8; 8] = *b"SPGSEG\x01\x00";
101
102pub const SEGMENT_MAGIC_V2: [u8; 8] = *b"SPGSEG\x02\x00";
113
114pub const SEGMENT_MAGIC_V3: [u8; 8] = *b"SPGSEG\x03\x00";
123
124pub const SEGMENT_MAGIC_V4: [u8; 8] = *b"SPGSEG\x04\x00";
129
130pub const BRIN_SIDECAR_MAGIC: [u8; 4] = *b"BRIN";
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct BrinSummary {
141 pub page_index: u32,
142 pub min_key: u64,
143 pub max_key: u64,
144}
145pub(crate) const SEGMENT_V2_HEADER_LEN: usize = 8 + 1 + 4;
146pub const SEGMENT_COMPRESS_ALGO_NONE: u8 = 0;
147pub const SEGMENT_COMPRESS_ALGO_LZSS: u8 = 1;
148
149pub const SEGMENT_PAGE_BYTES: u32 = 4096;
154
155const HEADER_FIXED_LEN: usize = 8 + 4 + 4 + 4 + 8 + 8 + 4; const FOOTER_LEN: usize = 4;
163
164#[derive(Debug)]
167pub enum SegmentError {
168 TooShort {
169 got: usize,
170 need: usize,
171 },
172 BadMagic {
173 got: [u8; 8],
174 },
175 BadShape(String),
176 BadCrc {
177 expected: u32,
178 got: u32,
179 },
180 BloomError(BloomError),
181 UnsortedKey {
182 prev: u64,
183 next: u64,
184 },
185 KeyNotInPage {
186 key: u64,
187 },
188 PageOutOfRange {
190 got: u32,
191 num_pages: u32,
192 },
193 CompressionDecodeFailed(String),
197 UnknownCompressionAlgo(u8),
201}
202
203impl fmt::Display for SegmentError {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 match self {
206 Self::TooShort { got, need } => write!(
207 f,
208 "segment: too short, got {got} bytes, need at least {need}"
209 ),
210 Self::BadMagic { got } => {
211 write!(f, "segment: bad magic {got:?}, expected {SEGMENT_MAGIC:?}")
212 }
213 Self::BadShape(s) => write!(f, "segment: bad shape: {s}"),
214 Self::BadCrc { expected, got } => write!(
215 f,
216 "segment: crc mismatch, expected 0x{expected:08x}, got 0x{got:08x}"
217 ),
218 Self::BloomError(e) => write!(f, "segment: bloom decode failed: {e}"),
219 Self::UnsortedKey { prev, next } => write!(
220 f,
221 "segment: writer received unsorted keys (prev={prev}, next={next}); \
222 the segment contract requires ascending u64 keys"
223 ),
224 Self::KeyNotInPage { key } => {
225 write!(f, "segment: key {key} not found in target page")
226 }
227 Self::PageOutOfRange { got, num_pages } => write!(
228 f,
229 "segment: page index {got} out of range, num_pages = {num_pages}"
230 ),
231 Self::CompressionDecodeFailed(s) => {
232 write!(f, "segment v2 envelope: LZSS decompress failed: {s}")
233 }
234 Self::UnknownCompressionAlgo(b) => write!(
235 f,
236 "segment v2 envelope: unknown compression algo byte {b:#04x}"
237 ),
238 }
239 }
240}
241
242impl From<BloomError> for SegmentError {
243 fn from(e: BloomError) -> Self {
244 Self::BloomError(e)
245 }
246}
247
248#[derive(Debug, Clone)]
253pub struct SegmentMeta {
254 pub num_rows: u64,
255 pub num_pages: u32,
256 pub page_size_bytes: u32,
257 pub min_pk: u64,
258 pub max_pk: u64,
259 pub total_bytes: usize,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269struct PageIndexEntry {
270 first_pk: u64,
271 file_offset: u32,
272}
273
274#[allow(clippy::too_many_lines)]
294pub fn encode_segment<I>(
295 rows: I,
296 bloom_target_fp: f64,
297 page_size_bytes: u32,
298) -> Result<(Vec<u8>, SegmentMeta), SegmentError>
299where
300 I: ExactSizeIterator<Item = (u64, Vec<u8>)>,
301{
302 if !(256..=65_536).contains(&page_size_bytes) {
303 return Err(SegmentError::BadShape(format!(
304 "page_size_bytes {page_size_bytes} must be in [256, 65536]"
305 )));
306 }
307 let num_rows_hint = rows.len();
308 if num_rows_hint == 0 {
309 return Err(SegmentError::BadShape(
310 "encode_segment: at least one row required".into(),
311 ));
312 }
313 let mut bloom = BloomFilter::with_target_fp_rate(num_rows_hint, bloom_target_fp);
319 let mut pages: Vec<Vec<u8>> = Vec::new();
320 let mut pages_bytes_total: usize = 0;
324 let mut page_index: Vec<PageIndexEntry> = Vec::new();
325 let mut row_bytes_in_page: Vec<Vec<u8>> = Vec::new();
326 let mut first_pk_in_page: Option<u64> = None;
327 let mut last_key: Option<u64> = None;
328 let mut min_pk: Option<u64> = None;
329 let mut max_pk: u64 = 0;
330 let mut total_rows: u64 = 0;
331 for (key, payload) in rows {
332 if let Some(prev) = last_key
333 && key <= prev
334 {
335 return Err(SegmentError::UnsortedKey { prev, next: key });
336 }
337 last_key = Some(key);
338 if min_pk.is_none() {
339 min_pk = Some(key);
340 }
341 max_pk = key;
342 total_rows = total_rows.wrapping_add(1);
343 bloom.insert(&key.to_le_bytes());
344 let mut row_bytes = Vec::with_capacity(12 + payload.len());
346 row_bytes.extend_from_slice(&key.to_le_bytes());
347 let plen = u32::try_from(payload.len()).map_err(|_| {
348 SegmentError::BadShape(format!(
349 "row payload too large: {} bytes > u32::MAX",
350 payload.len()
351 ))
352 })?;
353 row_bytes.extend_from_slice(&plen.to_le_bytes());
354 row_bytes.extend_from_slice(&payload);
355 let proposed_num_rows = row_bytes_in_page.len() + 1;
361 let proposed_offsets_bytes = proposed_num_rows * 4;
362 let proposed_rows_bytes: usize =
363 row_bytes_in_page.iter().map(Vec::len).sum::<usize>() + row_bytes.len();
364 let proposed_size = 4 + proposed_offsets_bytes + proposed_rows_bytes;
365 if proposed_size > page_size_bytes as usize {
366 if !row_bytes_in_page.is_empty() {
368 let page_file_offset =
369 u32::try_from(pages_bytes_total).expect("pages region fits u32");
370 page_index.push(PageIndexEntry {
371 first_pk: first_pk_in_page.expect("page is non-empty"),
372 file_offset: page_file_offset,
373 });
374 let finalised = serialise_page(&row_bytes_in_page, page_size_bytes as usize);
375 pages_bytes_total += finalised.len();
376 pages.push(finalised);
377 row_bytes_in_page.clear();
378 first_pk_in_page = None;
379 }
380 if 4 + 4 + row_bytes.len() > page_size_bytes as usize {
388 let page_file_offset =
389 u32::try_from(pages_bytes_total).expect("pages region fits u32");
390 page_index.push(PageIndexEntry {
391 first_pk: key,
392 file_offset: page_file_offset,
393 });
394 let natural = 4 + 4 + row_bytes.len();
395 let jumbo = serialise_page(core::slice::from_ref(&row_bytes), natural);
396 pages_bytes_total += jumbo.len();
397 pages.push(jumbo);
398 continue;
399 }
400 }
401 if first_pk_in_page.is_none() {
403 first_pk_in_page = Some(key);
404 }
405 row_bytes_in_page.push(row_bytes);
406 }
407 if !row_bytes_in_page.is_empty() {
410 let page_file_offset = u32::try_from(pages_bytes_total).expect("pages region fits u32");
411 page_index.push(PageIndexEntry {
412 first_pk: first_pk_in_page.expect("trailing page is non-empty"),
413 file_offset: page_file_offset,
414 });
415 let final_page = serialise_page(&row_bytes_in_page, page_size_bytes as usize);
416 pages_bytes_total += final_page.len();
417 pages.push(final_page);
418 }
419 let num_pages = u32::try_from(pages.len()).map_err(|_| {
420 SegmentError::BadShape(format!(
421 "segment has {} pages, exceeds u32::MAX",
422 pages.len()
423 ))
424 })?;
425 let num_rows = total_rows;
426 let num_rows_u32 = u32::try_from(num_rows)
427 .map_err(|_| SegmentError::BadShape(format!("num_rows {num_rows} exceeds u32::MAX")))?;
428 let min_pk = min_pk.expect("non-empty rows");
429 let bloom_bytes = bloom.to_bytes();
432 let page_index_bytes = encode_page_index(&page_index);
433 let mut out = Vec::with_capacity(
435 HEADER_FIXED_LEN
436 + 4
437 + bloom_bytes.len()
438 + 4
439 + page_index_bytes.len()
440 + pages_bytes_total
441 + FOOTER_LEN,
442 );
443 out.extend_from_slice(&SEGMENT_MAGIC_V4);
448 let body_start = out.len();
449 out.extend_from_slice(&num_rows_u32.to_le_bytes());
450 out.extend_from_slice(&num_pages.to_le_bytes());
451 out.extend_from_slice(&page_size_bytes.to_le_bytes());
452 out.extend_from_slice(&min_pk.to_le_bytes());
453 out.extend_from_slice(&max_pk.to_le_bytes());
454 out.extend_from_slice(
455 &u32::try_from(bloom_bytes.len())
456 .expect("bloom < 4 GiB")
457 .to_le_bytes(),
458 );
459 out.extend_from_slice(&bloom_bytes);
460 out.extend_from_slice(
461 &u32::try_from(page_index_bytes.len())
462 .expect("page index < 4 GiB")
463 .to_le_bytes(),
464 );
465 out.extend_from_slice(&page_index_bytes);
466 for page in &pages {
467 debug_assert!(
472 page.len() == page_size_bytes as usize || page.len() > page_size_bytes as usize,
473 "page neither fixed-size nor jumbo: {} vs {page_size_bytes}",
474 page.len()
475 );
476 out.extend_from_slice(page);
477 }
478 let crc = crc32(&out[body_start..]);
482 out.extend_from_slice(&crc.to_le_bytes());
483 let meta = SegmentMeta {
484 num_rows,
485 num_pages,
486 page_size_bytes,
487 min_pk,
488 max_pk,
489 total_bytes: out.len(),
490 };
491 Ok((out, meta))
492}
493
494fn serialise_page(row_bytes: &[Vec<u8>], page_size_bytes: usize) -> Vec<u8> {
501 let num_rows = u32::try_from(row_bytes.len()).expect("row count fits u32");
502 let offsets_section_bytes = num_rows as usize * 4;
503 let header_total = 4 + offsets_section_bytes;
504 let mut page = Vec::with_capacity(page_size_bytes);
505 page.extend_from_slice(&num_rows.to_le_bytes());
506 page.resize(header_total, 0);
509 let mut offsets = Vec::with_capacity(row_bytes.len());
511 for row in row_bytes {
512 offsets.push(u32::try_from(page.len()).expect("page < 4 GiB"));
513 page.extend_from_slice(row);
514 }
515 for (i, off) in offsets.iter().enumerate() {
517 let pos = 4 + i * 4;
518 page[pos..pos + 4].copy_from_slice(&off.to_le_bytes());
519 }
520 debug_assert!(
521 page.len() <= page_size_bytes,
522 "page overflow: {} > {page_size_bytes}",
523 page.len()
524 );
525 page.resize(page_size_bytes, 0);
526 page
527}
528
529fn encode_page_index(index: &[PageIndexEntry]) -> Vec<u8> {
532 let mut out = Vec::with_capacity(4 + index.len() * 12);
533 out.extend_from_slice(
534 &u32::try_from(index.len())
535 .expect("page count fits u32")
536 .to_le_bytes(),
537 );
538 for entry in index {
539 out.extend_from_slice(&entry.first_pk.to_le_bytes());
540 out.extend_from_slice(&entry.file_offset.to_le_bytes());
541 }
542 out
543}
544
545fn parse_page_index(input: &[u8]) -> Result<Vec<PageIndexEntry>, SegmentError> {
546 if input.len() < 4 {
547 return Err(SegmentError::BadShape(
548 "page index: too short for count prefix".into(),
549 ));
550 }
551 let count = u32::from_le_bytes([input[0], input[1], input[2], input[3]]) as usize;
552 let expected = 4 + count * 12;
553 if input.len() != expected {
554 return Err(SegmentError::BadShape(format!(
555 "page index: input is {} bytes, expected {} for count {count}",
556 input.len(),
557 expected
558 )));
559 }
560 let mut out = Vec::with_capacity(count);
561 for i in 0..count {
562 let off = 4 + i * 12;
563 let first_pk = u64::from_le_bytes([
564 input[off],
565 input[off + 1],
566 input[off + 2],
567 input[off + 3],
568 input[off + 4],
569 input[off + 5],
570 input[off + 6],
571 input[off + 7],
572 ]);
573 let file_offset = u32::from_le_bytes([
574 input[off + 8],
575 input[off + 9],
576 input[off + 10],
577 input[off + 11],
578 ]);
579 out.push(PageIndexEntry {
580 first_pk,
581 file_offset,
582 });
583 }
584 Ok(out)
585}
586
587#[derive(Debug, Clone)]
595struct SegmentMetadata {
596 meta: SegmentMeta,
597 bloom: BloomFilter,
598 page_index: Vec<PageIndexEntry>,
599 codec_version: u8,
603 pages_start_offset: usize,
607}
608
609fn parse_segment_metadata(bytes: &[u8]) -> Result<SegmentMetadata, SegmentError> {
615 if bytes.len() < HEADER_FIXED_LEN + FOOTER_LEN {
616 return Err(SegmentError::TooShort {
617 got: bytes.len(),
618 need: HEADER_FIXED_LEN + FOOTER_LEN,
619 });
620 }
621 let mut magic = [0u8; 8];
622 magic.copy_from_slice(&bytes[..8]);
623 let codec_version: u8 = if magic == SEGMENT_MAGIC_V4 {
624 47
625 } else if magic == SEGMENT_MAGIC_V3 {
626 46
627 } else if magic == SEGMENT_MAGIC {
628 0
629 } else {
630 return Err(SegmentError::BadMagic { got: magic });
631 };
632 let num_rows = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
634 let num_pages = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
635 let page_size_bytes = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
636 let min_pk = u64::from_le_bytes([
637 bytes[20], bytes[21], bytes[22], bytes[23], bytes[24], bytes[25], bytes[26], bytes[27],
638 ]);
639 let max_pk = u64::from_le_bytes([
640 bytes[28], bytes[29], bytes[30], bytes[31], bytes[32], bytes[33], bytes[34], bytes[35],
641 ]);
642 let bloom_len = u32::from_le_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]) as usize;
643 let bloom_offset = HEADER_FIXED_LEN;
644 if bytes.len() < bloom_offset + bloom_len + 4 {
645 return Err(SegmentError::TooShort {
646 got: bytes.len(),
647 need: bloom_offset + bloom_len + 4,
648 });
649 }
650 let bloom = BloomFilter::from_bytes(&bytes[bloom_offset..bloom_offset + bloom_len])?;
651 let page_index_len_off = bloom_offset + bloom_len;
652 let page_index_len = u32::from_le_bytes([
653 bytes[page_index_len_off],
654 bytes[page_index_len_off + 1],
655 bytes[page_index_len_off + 2],
656 bytes[page_index_len_off + 3],
657 ]) as usize;
658 let page_index_off = page_index_len_off + 4;
659 if bytes.len() < page_index_off + page_index_len {
660 return Err(SegmentError::TooShort {
661 got: bytes.len(),
662 need: page_index_off + page_index_len,
663 });
664 }
665 let page_index = parse_page_index(&bytes[page_index_off..page_index_off + page_index_len])?;
666 let pages_start_offset = page_index_off + page_index_len;
667 let pages_total_bytes = num_pages as usize * page_size_bytes as usize;
673 let expected_total = pages_start_offset + pages_total_bytes + FOOTER_LEN;
674 let exact_len_applies = codec_version == 0;
675 if pages_start_offset + FOOTER_LEN > bytes.len()
676 || page_index
677 .iter()
678 .any(|e| pages_start_offset + e.file_offset as usize > bytes.len() - FOOTER_LEN)
679 {
680 return Err(SegmentError::BadShape(format!(
681 "segment: page index points past the {} input bytes",
682 bytes.len()
683 )));
684 }
685 if exact_len_applies && bytes.len() != expected_total {
686 return Err(SegmentError::BadShape(format!(
687 "segment: input is {} bytes, header implies {expected_total}",
688 bytes.len()
689 )));
690 }
691 let stored_crc_off = bytes.len() - FOOTER_LEN;
696 let stored_crc = u32::from_le_bytes([
697 bytes[stored_crc_off],
698 bytes[stored_crc_off + 1],
699 bytes[stored_crc_off + 2],
700 bytes[stored_crc_off + 3],
701 ]);
702 let computed_crc = crc32(&bytes[8..stored_crc_off]);
703 if computed_crc != stored_crc {
704 return Err(SegmentError::BadCrc {
705 expected: stored_crc,
706 got: computed_crc,
707 });
708 }
709 let meta = SegmentMeta {
710 num_rows: u64::from(num_rows),
711 num_pages,
712 page_size_bytes,
713 min_pk,
714 max_pk,
715 total_bytes: bytes.len(),
716 };
717 Ok(SegmentMetadata {
718 meta,
719 bloom,
720 page_index,
721 codec_version,
722 pages_start_offset,
723 })
724}
725
726fn segment_might_contain(metadata: &SegmentMetadata, key: u64) -> bool {
730 if key < metadata.meta.min_pk || key > metadata.meta.max_pk {
731 return false;
732 }
733 metadata.bloom.contains(&key.to_le_bytes())
734}
735
736fn segment_lookup(metadata: &SegmentMetadata, bytes: &[u8], key: u64) -> Option<Vec<u8>> {
742 if !segment_might_contain(metadata, key) {
743 return None;
744 }
745 let candidate = match metadata
748 .page_index
749 .binary_search_by(|entry| entry.first_pk.cmp(&key))
750 {
751 Ok(i) => i,
752 Err(0) => return None,
753 Err(i) => i - 1,
754 };
755 let entry = metadata.page_index[candidate];
756 let page_off = metadata.pages_start_offset + entry.file_offset as usize;
757 let page_end = match metadata.page_index.get(candidate + 1) {
761 Some(next) => metadata.pages_start_offset + next.file_offset as usize,
762 None => bytes.len() - FOOTER_LEN,
763 };
764 if page_end > bytes.len() - FOOTER_LEN || page_off >= page_end {
765 return None;
766 }
767 let page = &bytes[page_off..page_end];
768 decode_page_lookup(page, key)
769}
770
771fn segment_scan<'a>(
773 metadata: &'a SegmentMetadata,
774 bytes: &'a [u8],
775) -> impl Iterator<Item = (u64, Vec<u8>)> + 'a {
776 let pages_end = bytes.len() - FOOTER_LEN;
779 (0..metadata.page_index.len()).flat_map(move |i| {
780 let off = metadata.pages_start_offset + metadata.page_index[i].file_offset as usize;
781 let end = match metadata.page_index.get(i + 1) {
782 Some(next) => metadata.pages_start_offset + next.file_offset as usize,
783 None => pages_end,
784 };
785 let page = &bytes[off..end.min(pages_end)];
786 decode_page_iter(page)
787 })
788}
789
790#[derive(Debug)]
799pub struct SegmentReader<'a> {
800 bytes: &'a [u8],
801 metadata: SegmentMetadata,
802}
803
804pub fn derive_brin_summaries(v1_bytes: &[u8]) -> Result<Vec<BrinSummary>, SegmentError> {
812 let reader = SegmentReader::open(v1_bytes)?;
813 let num_pages = reader.meta().num_pages as usize;
814 if num_pages == 0 {
815 return Ok(Vec::new());
816 }
817 let page_starts: Vec<u64> = reader
821 .metadata
822 .page_index
823 .iter()
824 .map(|e| e.first_pk)
825 .collect();
826 let mut min_by_page: Vec<Option<u64>> = alloc::vec![None; num_pages];
827 let mut max_by_page: Vec<Option<u64>> = alloc::vec![None; num_pages];
828 let mut current_page: usize = 0;
829 for (key, _) in reader.scan() {
830 while current_page + 1 < num_pages && key >= page_starts[current_page + 1] {
831 current_page += 1;
832 }
833 if min_by_page[current_page].is_none() {
834 min_by_page[current_page] = Some(key);
835 }
836 max_by_page[current_page] = Some(key);
837 }
838 let mut out = Vec::with_capacity(num_pages);
839 for p in 0..num_pages {
840 let (Some(min_key), Some(max_key)) = (min_by_page[p], max_by_page[p]) else {
841 continue;
842 };
843 out.push(BrinSummary {
844 page_index: u32::try_from(p).expect("page count fits u32"),
845 min_key,
846 max_key,
847 });
848 }
849 Ok(out)
850}
851
852#[must_use]
864pub fn wrap_v2_envelope_with_brin(
865 v1_bytes: Vec<u8>,
866 summaries: &[BrinSummary],
867 compress: bool,
868) -> Vec<u8> {
869 if summaries.is_empty() {
870 return wrap_v2_envelope(v1_bytes, compress);
871 }
872 let brin_section_len = 4 + summaries.len() * 20;
874 let mut inner = Vec::with_capacity(4 + 4 + brin_section_len + v1_bytes.len());
875 inner.extend_from_slice(&BRIN_SIDECAR_MAGIC);
876 let n = u32::try_from(summaries.len()).expect("BRIN summary count fits u32");
877 inner.extend_from_slice(&n.to_le_bytes());
878 for s in summaries {
879 inner.extend_from_slice(&s.page_index.to_le_bytes());
880 inner.extend_from_slice(&s.min_key.to_le_bytes());
881 inner.extend_from_slice(&s.max_key.to_le_bytes());
882 }
883 inner.extend_from_slice(&v1_bytes);
884 wrap_v2_envelope(inner, compress)
888}
889
890#[must_use]
896pub fn wrap_v2_envelope(v1_bytes: Vec<u8>, compress: bool) -> Vec<u8> {
897 if !compress {
898 return v1_bytes;
899 }
900 let compressed = spg_crypto::lzss::compress(&v1_bytes);
901 if compressed.len() + SEGMENT_V2_HEADER_LEN >= v1_bytes.len() {
902 return v1_bytes;
903 }
904 let inner_len = u32::try_from(v1_bytes.len()).expect("v1 segment < 4 GiB");
905 let mut out = Vec::with_capacity(SEGMENT_V2_HEADER_LEN + compressed.len());
906 out.extend_from_slice(&SEGMENT_MAGIC_V2);
907 out.push(SEGMENT_COMPRESS_ALGO_LZSS);
908 out.extend_from_slice(&inner_len.to_le_bytes());
909 out.extend_from_slice(&compressed);
910 out
911}
912
913pub(crate) fn unwrap_v2_envelope(
917 bytes: Vec<u8>,
918) -> Result<(Vec<u8>, Vec<BrinSummary>), SegmentError> {
919 if bytes.len() < 8 || bytes[..8] != SEGMENT_MAGIC_V2 {
920 return Ok((bytes, Vec::new()));
921 }
922 if bytes.len() < SEGMENT_V2_HEADER_LEN {
923 return Err(SegmentError::TooShort {
924 got: bytes.len(),
925 need: SEGMENT_V2_HEADER_LEN,
926 });
927 }
928 let algo = bytes[8];
929 let inner_len = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]) as usize;
930 let inner = &bytes[SEGMENT_V2_HEADER_LEN..];
931 let decoded = match algo {
932 SEGMENT_COMPRESS_ALGO_NONE => {
933 if inner.len() != inner_len {
934 return Err(SegmentError::BadShape(alloc::format!(
935 "v2 envelope algo=none: declared inner_len {inner_len} \
936 differs from body {}",
937 inner.len()
938 )));
939 }
940 inner.to_vec()
941 }
942 SEGMENT_COMPRESS_ALGO_LZSS => {
943 let decompressed = spg_crypto::lzss::decompress(inner)
944 .map_err(|e| SegmentError::CompressionDecodeFailed(alloc::format!("{e:?}")))?;
945 if decompressed.len() != inner_len {
946 return Err(SegmentError::BadShape(alloc::format!(
947 "v2 envelope LZSS: decompressed {} bytes, declared {inner_len}",
948 decompressed.len()
949 )));
950 }
951 decompressed
952 }
953 other => return Err(SegmentError::UnknownCompressionAlgo(other)),
954 };
955 if decoded.len() >= 4 && decoded[..4] == BRIN_SIDECAR_MAGIC {
957 return parse_brin_sidecar_then_v1(decoded);
958 }
959 Ok((decoded, Vec::new()))
960}
961
962fn parse_brin_sidecar_then_v1(
966 decoded: Vec<u8>,
967) -> Result<(Vec<u8>, Vec<BrinSummary>), SegmentError> {
968 if decoded.len() < 8 {
969 return Err(SegmentError::BadShape(alloc::format!(
970 "BRIN sidecar: truncated header ({}B < 8)",
971 decoded.len()
972 )));
973 }
974 let n_summaries = u32::from_le_bytes([decoded[4], decoded[5], decoded[6], decoded[7]]) as usize;
975 let summaries_end = 8 + n_summaries * 20;
976 if decoded.len() < summaries_end {
977 return Err(SegmentError::BadShape(alloc::format!(
978 "BRIN sidecar: truncated body (need {summaries_end}B, have {}B)",
979 decoded.len()
980 )));
981 }
982 let mut summaries = Vec::with_capacity(n_summaries);
983 for i in 0..n_summaries {
984 let off = 8 + i * 20;
985 let page_index = u32::from_le_bytes([
986 decoded[off],
987 decoded[off + 1],
988 decoded[off + 2],
989 decoded[off + 3],
990 ]);
991 let mut k = [0u8; 8];
992 k.copy_from_slice(&decoded[off + 4..off + 12]);
993 let min_key = u64::from_le_bytes(k);
994 k.copy_from_slice(&decoded[off + 12..off + 20]);
995 let max_key = u64::from_le_bytes(k);
996 summaries.push(BrinSummary {
997 page_index,
998 min_key,
999 max_key,
1000 });
1001 }
1002 let v1_bytes = decoded[summaries_end..].to_vec();
1004 Ok((v1_bytes, summaries))
1005}
1006
1007impl<'a> SegmentReader<'a> {
1008 pub fn open(bytes: &'a [u8]) -> Result<Self, SegmentError> {
1016 if bytes.len() >= 8 && bytes[..8] == SEGMENT_MAGIC_V2 {
1017 return Err(SegmentError::BadShape(
1018 "v2 envelope: SegmentReader requires the caller to first \
1019 unwrap to v1 bytes via OwnedSegment::from_bytes; the \
1020 borrowed-slice reader does not allocate."
1021 .into(),
1022 ));
1023 }
1024 let metadata = parse_segment_metadata(bytes)?;
1025 Ok(Self { bytes, metadata })
1026 }
1027
1028 #[must_use]
1029 pub fn meta(&self) -> &SegmentMeta {
1030 &self.metadata.meta
1031 }
1032
1033 #[must_use]
1037 pub fn codec_version(&self) -> u8 {
1038 self.metadata.codec_version
1039 }
1040
1041 #[must_use]
1046 pub fn might_contain(&self, key: u64) -> bool {
1047 segment_might_contain(&self.metadata, key)
1048 }
1049
1050 pub fn lookup(&self, key: u64) -> Option<Vec<u8>> {
1056 segment_lookup(&self.metadata, self.bytes, key)
1057 }
1058
1059 pub fn scan(&self) -> impl Iterator<Item = (u64, Vec<u8>)> + '_ {
1062 segment_scan(&self.metadata, self.bytes)
1063 }
1064}
1065
1066#[derive(Debug, Clone)]
1078pub struct OwnedSegment {
1079 bytes: Vec<u8>,
1080 metadata: SegmentMetadata,
1081 brin_summaries: Vec<BrinSummary>,
1085}
1086
1087impl OwnedSegment {
1088 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, SegmentError> {
1100 let (bytes, brin_summaries) = unwrap_v2_envelope(bytes)?;
1101 let metadata = parse_segment_metadata(&bytes)?;
1102 Ok(Self {
1103 bytes,
1104 metadata,
1105 brin_summaries,
1106 })
1107 }
1108
1109 #[must_use]
1113 pub fn brin_summaries(&self) -> &[BrinSummary] {
1114 &self.brin_summaries
1115 }
1116
1117 #[must_use]
1118 pub fn meta(&self) -> &SegmentMeta {
1119 &self.metadata.meta
1120 }
1121
1122 #[must_use]
1126 pub fn codec_version(&self) -> u8 {
1127 self.metadata.codec_version
1128 }
1129
1130 #[must_use]
1131 pub fn might_contain(&self, key: u64) -> bool {
1132 segment_might_contain(&self.metadata, key)
1133 }
1134
1135 pub fn lookup(&self, key: u64) -> Option<Vec<u8>> {
1136 segment_lookup(&self.metadata, &self.bytes, key)
1137 }
1138
1139 pub fn scan(&self) -> impl Iterator<Item = (u64, Vec<u8>)> + '_ {
1140 segment_scan(&self.metadata, &self.bytes)
1141 }
1142
1143 #[must_use]
1147 pub fn bytes(&self) -> &[u8] {
1148 &self.bytes
1149 }
1150}
1151
1152fn decode_page_lookup(page: &[u8], key: u64) -> Option<Vec<u8>> {
1157 if page.len() < 4 {
1158 return None;
1159 }
1160 let num_rows = u32::from_le_bytes([page[0], page[1], page[2], page[3]]) as usize;
1161 if num_rows == 0 {
1162 return None;
1163 }
1164 let offsets_start = 4;
1165 let offsets_end = offsets_start + num_rows * 4;
1166 if page.len() < offsets_end {
1167 return None;
1168 }
1169 let offsets: Vec<u32> = (0..num_rows)
1170 .map(|i| {
1171 let o = offsets_start + i * 4;
1172 u32::from_le_bytes([page[o], page[o + 1], page[o + 2], page[o + 3]])
1173 })
1174 .collect();
1175 let mut lo = 0usize;
1177 let mut hi = num_rows;
1178 while lo < hi {
1179 let mid = usize::midpoint(lo, hi);
1180 let row_off = offsets[mid] as usize;
1181 if row_off + 8 > page.len() {
1182 return None;
1183 }
1184 let row_key = u64::from_le_bytes([
1185 page[row_off],
1186 page[row_off + 1],
1187 page[row_off + 2],
1188 page[row_off + 3],
1189 page[row_off + 4],
1190 page[row_off + 5],
1191 page[row_off + 6],
1192 page[row_off + 7],
1193 ]);
1194 match row_key.cmp(&key) {
1195 core::cmp::Ordering::Less => lo = mid + 1,
1196 core::cmp::Ordering::Greater => hi = mid,
1197 core::cmp::Ordering::Equal => {
1198 let plen_off = row_off + 8;
1200 if plen_off + 4 > page.len() {
1201 return None;
1202 }
1203 let plen = u32::from_le_bytes([
1204 page[plen_off],
1205 page[plen_off + 1],
1206 page[plen_off + 2],
1207 page[plen_off + 3],
1208 ]) as usize;
1209 let payload_start = plen_off + 4;
1210 let payload_end = payload_start + plen;
1211 if payload_end > page.len() {
1212 return None;
1213 }
1214 return Some(page[payload_start..payload_end].to_vec());
1215 }
1216 }
1217 }
1218 None
1219}
1220
1221fn decode_page_iter(page: &[u8]) -> Vec<(u64, Vec<u8>)> {
1222 if page.len() < 4 {
1223 return vec![];
1224 }
1225 let num_rows = u32::from_le_bytes([page[0], page[1], page[2], page[3]]) as usize;
1226 if num_rows == 0 {
1227 return vec![];
1228 }
1229 let offsets_end = 4 + num_rows * 4;
1230 if page.len() < offsets_end {
1231 return vec![];
1232 }
1233 let offsets: Vec<u32> = (0..num_rows)
1234 .map(|i| {
1235 let o = 4 + i * 4;
1236 u32::from_le_bytes([page[o], page[o + 1], page[o + 2], page[o + 3]])
1237 })
1238 .collect();
1239 let mut out = Vec::with_capacity(num_rows);
1240 for off in offsets {
1241 let row_off = off as usize;
1242 if row_off + 12 > page.len() {
1243 break;
1244 }
1245 let key = u64::from_le_bytes([
1246 page[row_off],
1247 page[row_off + 1],
1248 page[row_off + 2],
1249 page[row_off + 3],
1250 page[row_off + 4],
1251 page[row_off + 5],
1252 page[row_off + 6],
1253 page[row_off + 7],
1254 ]);
1255 let plen = u32::from_le_bytes([
1256 page[row_off + 8],
1257 page[row_off + 9],
1258 page[row_off + 10],
1259 page[row_off + 11],
1260 ]) as usize;
1261 let payload_start = row_off + 12;
1262 let payload_end = payload_start + plen;
1263 if payload_end > page.len() {
1264 break;
1265 }
1266 out.push((key, page[payload_start..payload_end].to_vec()));
1267 }
1268 out
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273 use super::*;
1274
1275 fn build_rows(n: u64) -> Vec<(u64, Vec<u8>)> {
1276 (0..n)
1277 .map(|i| {
1278 let payload = format!("row-{i}").into_bytes();
1279 (i * 2 + 1, payload) })
1281 .collect()
1282 }
1283
1284 #[test]
1285 fn brin_summaries_derive_matches_per_page_pk_ranges() {
1286 let rows = build_rows(200);
1290 let expected: Vec<u64> = rows.iter().map(|(k, _)| *k).collect();
1291 let (v1_bytes, meta) =
1292 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode");
1293 let summaries = derive_brin_summaries(&v1_bytes).expect("derive");
1294 assert_eq!(summaries.len(), meta.num_pages as usize);
1295 for k in expected {
1297 let hits = summaries
1298 .iter()
1299 .filter(|s| k >= s.min_key && k <= s.max_key)
1300 .count();
1301 assert!(hits >= 1, "key {k} not covered by any BRIN summary");
1302 }
1303 for w in summaries.windows(2) {
1306 assert!(
1307 w[0].max_key < w[1].min_key,
1308 "summary ranges overlap: page {} max {} >= page {} min {}",
1309 w[0].page_index,
1310 w[0].max_key,
1311 w[1].page_index,
1312 w[1].min_key
1313 );
1314 }
1315 }
1316
1317 #[test]
1318 fn brin_sidecar_round_trips_through_v2_envelope() {
1319 let rows = build_rows(150);
1320 let (v1_bytes, _) =
1321 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode");
1322 let summaries = derive_brin_summaries(&v1_bytes).expect("derive");
1323 assert!(!summaries.is_empty());
1324 let wrapped = wrap_v2_envelope_with_brin(v1_bytes, &summaries, true);
1325 let seg = OwnedSegment::from_bytes(wrapped).expect("v2+brin parses");
1327 assert!(seg.lookup(1).is_some(), "lookup hits a known key");
1329 assert!(seg.lookup(299).is_some(), "lookup hits another known key");
1330 let recovered = seg.brin_summaries();
1332 assert_eq!(recovered.len(), summaries.len());
1333 for (a, b) in summaries.iter().zip(recovered) {
1334 assert_eq!(a, b);
1335 }
1336 }
1337
1338 #[test]
1339 fn segment_without_brin_sidecar_returns_empty_summaries() {
1340 let rows = build_rows(50);
1341 let (v1_bytes, _) =
1342 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode");
1343 let seg1 = OwnedSegment::from_bytes(v1_bytes.clone()).expect("v1 parses");
1345 assert!(seg1.brin_summaries().is_empty());
1346 let wrapped = wrap_v2_envelope(v1_bytes, true);
1348 let seg2 = OwnedSegment::from_bytes(wrapped).expect("v2 parses");
1349 assert!(seg2.brin_summaries().is_empty());
1350 }
1351
1352 #[test]
1353 fn v2_envelope_round_trips_byte_equal() {
1354 let rows = build_rows(1000);
1357 let (v1_bytes, _) =
1358 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode");
1359 let wrapped = wrap_v2_envelope(v1_bytes.clone(), true);
1360 assert!(
1363 wrapped.len() < v1_bytes.len(),
1364 "v2 envelope should be smaller: {} vs v1 {}",
1365 wrapped.len(),
1366 v1_bytes.len()
1367 );
1368 let seg = OwnedSegment::from_bytes(wrapped).expect("v2 unwrap + parse");
1369 assert_eq!(seg.meta().num_rows, 1000);
1370 assert!(seg.lookup(1).is_some());
1373 assert!(seg.lookup(1999).is_some());
1374 }
1375
1376 #[test]
1377 fn v2_envelope_with_compress_false_is_v1_passthrough() {
1378 let rows = build_rows(64);
1379 let (v1_bytes, _) =
1380 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode");
1381 let wrapped = wrap_v2_envelope(v1_bytes.clone(), false);
1382 assert_eq!(wrapped, v1_bytes);
1383 }
1384
1385 #[test]
1386 fn legacy_v1_segments_still_load_via_from_bytes() {
1387 let rows = build_rows(100);
1396 let (mut v1_bytes, _) =
1397 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode");
1398 assert_eq!(&v1_bytes[..8], &SEGMENT_MAGIC_V4, "encoder emits V4");
1399 v1_bytes[..8].copy_from_slice(&SEGMENT_MAGIC);
1400 let seg = OwnedSegment::from_bytes(v1_bytes).expect("v1 still parses");
1403 assert_eq!(seg.meta().num_rows, 100);
1404 assert_eq!(seg.codec_version(), 0, "v1 magic = legacy plain-u16 rules");
1405 }
1406
1407 #[test]
1408 fn v2_envelope_invalid_algo_byte_errors_loudly() {
1409 let mut bogus = Vec::new();
1412 bogus.extend_from_slice(&SEGMENT_MAGIC_V2);
1413 bogus.push(0x42); bogus.extend_from_slice(&0u32.to_le_bytes());
1415 let err = OwnedSegment::from_bytes(bogus).unwrap_err();
1416 assert!(matches!(err, SegmentError::UnknownCompressionAlgo(0x42)));
1417 }
1418
1419 #[test]
1420 fn encode_then_open_roundtrips_meta() {
1421 let rows = build_rows(1000);
1422 let (bytes, meta) =
1423 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode succeeds");
1424 let reader = SegmentReader::open(&bytes).expect("open succeeds");
1425 assert_eq!(reader.meta().num_rows, meta.num_rows);
1426 assert_eq!(reader.meta().num_pages, meta.num_pages);
1427 assert_eq!(reader.meta().min_pk, 1);
1428 assert_eq!(reader.meta().max_pk, 1999);
1429 assert_eq!(reader.meta().total_bytes, bytes.len());
1430 }
1431
1432 #[test]
1433 fn lookup_finds_every_inserted_key() {
1434 let rows = build_rows(1000);
1435 let expected: Vec<_> = rows.clone();
1436 let (bytes, _) =
1437 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode succeeds");
1438 let reader = SegmentReader::open(&bytes).expect("open succeeds");
1439 for (key, payload) in expected {
1440 assert_eq!(
1441 reader.lookup(key),
1442 Some(payload),
1443 "lookup({key}) returned wrong payload"
1444 );
1445 }
1446 }
1447
1448 #[test]
1449 fn lookup_returns_none_for_unknown_key() {
1450 let rows = build_rows(1000);
1451 let (bytes, _) =
1452 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("encode succeeds");
1453 let reader = SegmentReader::open(&bytes).expect("open succeeds");
1454 for k in (0..2000u64).step_by(2) {
1456 assert!(reader.lookup(k).is_none(), "expected None for gap key {k}");
1457 }
1458 assert!(reader.lookup(99_999).is_none());
1460 assert!(reader.lookup(0).is_none());
1461 }
1462
1463 #[test]
1464 fn might_contain_short_circuits_out_of_range() {
1465 let rows = build_rows(100);
1466 let (bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1467 let reader = SegmentReader::open(&bytes).unwrap();
1468 assert!(!reader.might_contain(0));
1470 assert!(!reader.might_contain(200));
1471 assert!(reader.might_contain(1));
1473 assert!(reader.might_contain(199));
1474 }
1475
1476 #[test]
1477 fn scan_yields_rows_in_key_order() {
1478 let rows = build_rows(500);
1479 let expected: Vec<_> = rows.clone();
1480 let (bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1481 let reader = SegmentReader::open(&bytes).unwrap();
1482 let scanned: Vec<_> = reader.scan().collect();
1483 assert_eq!(scanned.len(), 500);
1484 for w in scanned.windows(2) {
1486 assert!(
1487 w[0].0 < w[1].0,
1488 "scan out of order: {} >= {}",
1489 w[0].0,
1490 w[1].0
1491 );
1492 }
1493 assert_eq!(scanned, expected);
1495 }
1496
1497 #[test]
1498 fn open_rejects_bad_magic() {
1499 let rows = build_rows(10);
1500 let (mut bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1501 bytes[0] ^= 0xff;
1502 match SegmentReader::open(&bytes) {
1503 Err(SegmentError::BadMagic { .. }) => {}
1504 other => panic!("expected BadMagic, got {other:?}"),
1505 }
1506 }
1507
1508 #[test]
1509 fn open_rejects_bad_crc() {
1510 let rows = build_rows(10);
1511 let (mut bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1512 let off = bytes.len() / 2;
1514 bytes[off] ^= 0x01;
1515 match SegmentReader::open(&bytes) {
1516 Err(SegmentError::BadCrc { .. }) => {}
1517 other => panic!("expected BadCrc, got {other:?}"),
1518 }
1519 }
1520
1521 #[test]
1522 fn encode_rejects_unsorted_keys() {
1523 let rows = vec![(10u64, vec![1]), (5u64, vec![2])];
1524 match encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES) {
1525 Err(SegmentError::UnsortedKey { prev: 10, next: 5 }) => {}
1526 other => panic!("expected UnsortedKey, got {other:?}"),
1527 }
1528 }
1529
1530 #[test]
1531 fn encode_rejects_empty_input() {
1532 let rows: Vec<(u64, Vec<u8>)> = vec![];
1533 match encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES) {
1534 Err(SegmentError::BadShape(_)) => {}
1535 other => panic!("expected BadShape for empty input, got {other:?}"),
1536 }
1537 }
1538
1539 #[test]
1540 fn encode_rejects_bad_page_size() {
1541 let rows = build_rows(1);
1542 match encode_segment(rows.clone().into_iter(), 0.01, 128) {
1543 Err(SegmentError::BadShape(_)) => {}
1544 other => panic!("expected BadShape for tiny page, got {other:?}"),
1545 }
1546 match encode_segment(rows.into_iter(), 0.01, 1_000_000) {
1547 Err(SegmentError::BadShape(_)) => {}
1548 other => panic!("expected BadShape for huge page, got {other:?}"),
1549 }
1550 }
1551
1552 #[test]
1553 fn large_payload_becomes_a_jumbo_page_and_reads_back() {
1554 let rows = vec![
1559 (1u64, vec![0xABu8; 8192]),
1560 (2u64, vec![7u8; 16]),
1561 (3u64, vec![0xCDu8; 70_000]),
1562 ];
1563 let (bytes, meta) =
1564 encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).expect("jumbo encode");
1565 assert_eq!(meta.num_rows, 3);
1566 let seg = OwnedSegment::from_bytes(bytes).expect("parses");
1567 assert_eq!(seg.lookup(1).expect("pk 1").len(), 8192);
1568 assert_eq!(seg.lookup(2).expect("pk 2").len(), 16);
1569 let big = seg.lookup(3).expect("pk 3");
1570 assert_eq!(big.len(), 70_000);
1571 assert!(big.iter().all(|b| *b == 0xCD));
1572 let scanned: Vec<(u64, usize)> = seg.scan().map(|(k, p)| (k, p.len())).collect();
1574 assert_eq!(scanned, vec![(1, 8192), (2, 16), (3, 70_000)]);
1575 }
1576
1577 #[test]
1580 fn owned_segment_lookup_matches_reader_for_every_key() {
1581 let rows = build_rows(500);
1582 let expected: Vec<_> = rows.clone();
1583 let (bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1584 let bytes_len = bytes.len();
1585 let (r_meta_num_rows, r_meta_min_pk, r_meta_max_pk, r_lookups, r_scan) = {
1588 let reader = SegmentReader::open(&bytes).unwrap();
1589 let lookups: Vec<_> = expected.iter().map(|(k, _)| reader.lookup(*k)).collect();
1590 let scan: Vec<_> = reader.scan().collect();
1591 (
1592 reader.meta().num_rows,
1593 reader.meta().min_pk,
1594 reader.meta().max_pk,
1595 lookups,
1596 scan,
1597 )
1598 };
1599 let owned = OwnedSegment::from_bytes(bytes).unwrap();
1600 for ((key, expected_payload), reader_payload) in expected.iter().zip(r_lookups.iter()) {
1601 assert_eq!(reader_payload.as_ref(), Some(expected_payload));
1602 assert_eq!(owned.lookup(*key).as_ref(), Some(expected_payload));
1603 }
1604 assert_eq!(r_meta_num_rows, owned.meta().num_rows);
1606 assert_eq!(r_meta_min_pk, owned.meta().min_pk);
1607 assert_eq!(r_meta_max_pk, owned.meta().max_pk);
1608 let o_scan: Vec<_> = owned.scan().collect();
1609 assert_eq!(r_scan, o_scan);
1610 assert_eq!(owned.bytes().len(), bytes_len);
1611 }
1612
1613 #[test]
1614 fn owned_segment_might_contain_matches_reader() {
1615 let rows = build_rows(64);
1616 let (bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1617 let probes = [0u64, 1, 50, 127, 128, 200];
1618 let reader_results: Vec<bool> = {
1619 let reader = SegmentReader::open(&bytes).unwrap();
1620 probes.iter().map(|k| reader.might_contain(*k)).collect()
1621 };
1622 let owned = OwnedSegment::from_bytes(bytes).unwrap();
1623 for (key, r_hit) in probes.iter().zip(reader_results.iter()) {
1624 assert_eq!(*r_hit, owned.might_contain(*key));
1625 }
1626 }
1627
1628 #[test]
1629 fn owned_segment_rejects_bad_bytes_at_construction() {
1630 let rows = build_rows(8);
1632 let (mut bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1633 bytes[0] ^= 0xff; match OwnedSegment::from_bytes(bytes) {
1635 Err(SegmentError::BadMagic { .. }) => {}
1636 other => panic!("expected BadMagic, got {other:?}"),
1637 }
1638 }
1639
1640 #[test]
1641 fn owned_segment_lookup_returns_none_for_missing_key() {
1642 let rows = build_rows(100); let (bytes, _) = encode_segment(rows.into_iter(), 0.01, SEGMENT_PAGE_BYTES).unwrap();
1644 let owned = OwnedSegment::from_bytes(bytes).unwrap();
1645 for key in [0u64, 2, 50, 198, 200, 9999] {
1647 assert!(
1648 owned.lookup(key).is_none(),
1649 "expected None for non-inserted key {key}, got Some"
1650 );
1651 }
1652 }
1653}