1#[cfg(feature = "mmap")]
2use std::ops::Range;
3use std::{
4 borrow::Cow,
5 cmp::Ordering,
6 hash::BuildHasherDefault,
7 io,
8 marker::PhantomData,
9 path::Path,
10 rc::Rc,
11 sync::{
12 Arc,
13 atomic::{AtomicU64, Ordering as AtomicOrdering},
14 },
15};
16
17use anyhow::{Context, Result, bail, ensure};
18use fs_err::File;
19#[cfg(feature = "mmap")]
20use memmap2::Mmap;
21use quick_cache::{Lifecycle, sync::GuardResult};
22use rustc_hash::FxHasher;
23use smallvec::SmallVec;
24
25#[cfg(feature = "mmap")]
26use crate::mmap_helper::advise_mmap_for_persistence;
27use crate::{
28 AccessMode, Compression, QueryKey,
29 arc_bytes::ArcBytes,
30 be,
31 compression::checksum_block,
32 constants::MAX_INLINE_VALUE_SIZE,
33 lookup_entry::{IterValue, LookupEntry, LookupValue},
34 rc_bytes::RcBytes,
35 shared_bytes::SharedBytes,
36 static_sorted_file_builder::{
37 BLOCK_HEADER_SIZE, INDEX_BLOCK_ENTRY_SIZE, INDEX_BLOCK_HEADER_SIZE,
38 },
39};
40
41pub const BLOCK_TYPE_INDEX: u8 = 0;
43pub const BLOCK_TYPE_KEY_WITH_HASH: u8 = 1;
45pub const BLOCK_TYPE_KEY_NO_HASH: u8 = 2;
47pub const BLOCK_TYPE_FIXED_KEY_WITH_HASH: u8 = 3;
49pub const BLOCK_TYPE_FIXED_KEY_NO_HASH: u8 = 4;
51
52#[derive(Clone, Copy, PartialEq, Eq, Debug)]
54pub enum KeyBlockLayout {
55 HashThenKey,
57 KeyOnly,
59}
60
61impl KeyBlockLayout {
62 #[inline]
64 pub fn hash_len(self) -> u8 {
65 match self {
66 KeyBlockLayout::HashThenKey => size_of::<u64>() as u8,
67 KeyBlockLayout::KeyOnly => 0,
68 }
69 }
70
71 #[inline]
73 pub fn block_type(self, fixed: bool) -> u8 {
74 match (self, fixed) {
75 (KeyBlockLayout::HashThenKey, false) => BLOCK_TYPE_KEY_WITH_HASH,
76 (KeyBlockLayout::KeyOnly, false) => BLOCK_TYPE_KEY_NO_HASH,
77 (KeyBlockLayout::HashThenKey, true) => BLOCK_TYPE_FIXED_KEY_WITH_HASH,
78 (KeyBlockLayout::KeyOnly, true) => BLOCK_TYPE_FIXED_KEY_NO_HASH,
79 }
80 }
81
82 #[inline]
85 pub fn from_block_type(block_type: u8) -> Option<(Self, bool)> {
86 match block_type {
87 BLOCK_TYPE_KEY_WITH_HASH => Some((KeyBlockLayout::HashThenKey, false)),
88 BLOCK_TYPE_KEY_NO_HASH => Some((KeyBlockLayout::KeyOnly, false)),
89 BLOCK_TYPE_FIXED_KEY_WITH_HASH => Some((KeyBlockLayout::HashThenKey, true)),
90 BLOCK_TYPE_FIXED_KEY_NO_HASH => Some((KeyBlockLayout::KeyOnly, true)),
91 _ => None,
92 }
93 }
94}
95
96pub const FIXED_KEY_BLOCK_MIXED_VALUE_TYPE: u8 = 4;
99
100pub const KEY_BLOCK_ENTRY_TYPE_SMALL: u8 = 0;
102pub const KEY_BLOCK_ENTRY_TYPE_BLOB: u8 = 1;
104pub const KEY_BLOCK_ENTRY_TYPE_KEY_DELETED: u8 = 2;
106pub const KEY_BLOCK_ENTRY_TYPE_MEDIUM: u8 = 3;
108pub const KEY_BLOCK_ENTRY_TYPE_INLINE_MIN: u8 = 8;
110pub const KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN: u8 =
119 KEY_BLOCK_ENTRY_TYPE_INLINE_MIN + MAX_INLINE_VALUE_SIZE as u8 + 1;
120
121pub const KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH: usize = 4;
124pub const KEY_BLOCK_TABLE_ENTRY_SIZE_WITH_HASH: usize =
132 KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH + size_of::<u64>();
133
134#[inline(always)]
136pub fn key_block_table_stride(hash_len: u8) -> usize {
137 KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH + hash_len as usize
138}
139
140pub(crate) const SMALL_VALUE_REF_SIZE: usize = 8;
142pub(crate) const MEDIUM_VALUE_REF_SIZE: usize = 2;
144pub(crate) const BLOB_VALUE_REF_SIZE: usize = 4;
146pub(crate) const KEY_DELETED_REF_SIZE: usize = 0;
148
149const _: () = assert!(
153 MAX_INLINE_VALUE_SIZE <= (u8::MAX - KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN) as usize,
154 "MAX_INLINE_VALUE_SIZE exceeds what can be encoded in key type byte"
155);
156
157pub enum SstLookupResult {
159 Found(SmallVec<[LookupValue; 1]>),
161 NotFound,
163}
164
165impl From<LookupValue> for SstLookupResult {
166 fn from(value: LookupValue) -> Self {
167 SstLookupResult::Found(smallvec::smallvec![value])
168 }
169}
170
171#[derive(Clone, Default)]
172pub struct BlockWeighter;
173
174impl quick_cache::Weighter<(u32, u16), ArcBytes> for BlockWeighter {
175 fn weight(&self, _key: &(u32, u16), val: &ArcBytes) -> u64 {
176 if val.is_mmap_backed() {
177 debug_assert!(
180 !val.is_mmap_backed(),
181 "mmap-backed block should not be inserted into BlockCache"
182 );
183 64
184 } else {
185 val.len() as u64 + 8
186 }
187 }
188}
189
190#[derive(Clone, Default)]
193pub struct BlockCacheLifecycle;
194
195impl Lifecycle<(u32, u16), ArcBytes> for BlockCacheLifecycle {
196 type RequestState = ();
197
198 #[inline]
199 fn is_pinned(&self, _key: &(u32, u16), val: &ArcBytes) -> bool {
200 val.is_shared_arc()
201 }
202
203 #[inline]
204 fn begin_request(&self) -> Self::RequestState {}
205
206 #[inline]
207 fn on_evict(&self, _state: &mut Self::RequestState, _key: (u32, u16), _val: ArcBytes) {}
208}
209
210pub type BlockCache = quick_cache::sync::Cache<
211 (u32, u16),
212 ArcBytes,
213 BlockWeighter,
214 BuildHasherDefault<FxHasher>,
215 BlockCacheLifecycle,
216>;
217
218trait ValueBlockCache<B: SharedBytes> {
220 fn get_or_read(
221 self,
222 meta: &StaticSortedFileMetaData,
223 block_index: u16,
224 compression: Compression,
225 ) -> Result<B>;
226 fn read_uncached(
227 self,
228 meta: &StaticSortedFileMetaData,
229 block_index: u16,
230 compression: Compression,
231 ) -> Result<B>;
232}
233
234#[derive(Clone, Copy)]
236struct ArcBlockCacheReader<'a> {
237 backing: &'a StaticSortedFileBacking,
238 cache: &'a BlockCache,
239 verified_blocks: &'a [AtomicU64],
240}
241
242impl ValueBlockCache<ArcBytes> for ArcBlockCacheReader<'_> {
243 fn get_or_read(
244 self,
245 meta: &StaticSortedFileMetaData,
246 block_index: u16,
247 compression: Compression,
248 ) -> Result<ArcBytes> {
249 Ok(get_or_read_block(
253 self.backing,
254 meta,
255 block_index,
256 self.cache,
257 self.verified_blocks,
258 compression,
259 )?
260 .into_owned(self.backing))
261 }
262
263 fn read_uncached(
264 self,
265 meta: &StaticSortedFileMetaData,
266 block_index: u16,
267 compression: Compression,
268 ) -> Result<ArcBytes> {
269 read_block_lookup(self.backing, meta, block_index, compression)
270 }
271}
272
273struct RcBlockCacheReader<'a> {
275 backing: &'a StaticSortedFileIterBacking,
276 cache: &'a mut Option<(u16, RcBytes)>,
277}
278
279impl ValueBlockCache<RcBytes> for RcBlockCacheReader<'_> {
280 fn get_or_read(
281 self,
282 meta: &StaticSortedFileMetaData,
283 block_index: u16,
284 compression: Compression,
285 ) -> Result<RcBytes> {
286 if let Some((idx, block)) = self.cache.as_ref()
287 && *idx == block_index
288 {
289 return Ok(block.clone());
290 }
291 let block = read_block_iter(self.backing, meta, block_index, compression)?;
292 *self.cache = Some((block_index, block.clone()));
293 Ok(block)
294 }
295
296 fn read_uncached(
297 self,
298 meta: &StaticSortedFileMetaData,
299 block_index: u16,
300 compression: Compression,
301 ) -> Result<RcBytes> {
302 read_block_iter(self.backing, meta, block_index, compression)
303 }
304}
305
306#[derive(Clone, Copy, Debug)]
307pub struct StaticSortedFileMetaData {
308 pub sequence_number: u32,
310 pub block_count: u16,
312}
313
314impl StaticSortedFileMetaData {
315 pub fn block_offsets_start(&self, sst_len: usize) -> usize {
316 let bc: usize = self.block_count.into();
317 sst_len - (bc * size_of::<u32>())
318 }
319}
320
321enum StaticSortedFileBacking {
322 #[cfg(feature = "mmap")]
323 Mmap(Arc<Mmap>),
324 File {
325 file: Arc<File>,
326 file_len: usize,
327 block_offsets: Arc<[u32]>,
328 },
329}
330
331pub struct StaticSortedFile {
333 meta: StaticSortedFileMetaData,
335 backing: StaticSortedFileBacking,
336 verified_blocks: Box<[AtomicU64]>,
341 compression: Compression,
342 index: IndexBlock,
344}
345
346struct IndexBlock {
353 entries: IndexEntries,
356 first_block: u16,
358}
359
360enum IndexEntries {
362 #[cfg(feature = "mmap")]
369 Mmap(Range<usize>),
370 Owned(Box<[u8]>),
372}
373
374impl IndexBlock {
375 fn parse(backing: &StaticSortedFileBacking, meta: &StaticSortedFileMetaData) -> Result<Self> {
377 ensure!(
378 meta.block_count > 0,
379 "{:08}.sst has no blocks, so no index block",
380 meta.sequence_number
381 );
382 let block_index = meta.block_count - 1;
383 let (uncompressed_length, checksum, block) = get_raw_block(backing, meta, block_index)
384 .with_context(|| {
385 format!(
386 "Failed to read index block {} from {:08}.sst",
387 block_index, meta.sequence_number
388 )
389 })?;
390 ensure!(
391 uncompressed_length == 0,
392 "index block {} of {:08}.sst is compressed, but index blocks are always written \
393 uncompressed",
394 block_index,
395 meta.sequence_number
396 );
397 let data = &*block;
400 verify_checksum(meta, data, checksum, block_index)?;
401
402 ensure!(
403 data.len() >= INDEX_BLOCK_HEADER_SIZE,
404 "index block {} of {:08}.sst is too short ({} bytes)",
405 block_index,
406 meta.sequence_number,
407 data.len()
408 );
409 ensure!(
410 be::read_u8(data) == BLOCK_TYPE_INDEX,
411 "block {} of {:08}.sst is the last block but not an index block (type {})",
412 block_index,
413 meta.sequence_number,
414 be::read_u8(data)
415 );
416 let first_block = be::read_u16(&data[1..]);
417 let entry_bytes = &data[INDEX_BLOCK_HEADER_SIZE..];
418 ensure!(
419 entry_bytes.len().is_multiple_of(INDEX_BLOCK_ENTRY_SIZE),
420 "index block {} of {:08}.sst has {} trailing bytes past its last entry",
421 block_index,
422 meta.sequence_number,
423 entry_bytes.len() % INDEX_BLOCK_ENTRY_SIZE
424 );
425
426 let entries = match backing {
427 #[cfg(feature = "mmap")]
429 StaticSortedFileBacking::Mmap(mmap) => {
430 let start = entry_bytes.as_ptr() as usize - mmap.as_ptr() as usize;
431 IndexEntries::Mmap(start..start + entry_bytes.len())
432 }
433 StaticSortedFileBacking::File { .. } => IndexEntries::Owned(entry_bytes.into()),
434 };
435 Ok(Self {
436 entries,
437 first_block,
438 })
439 }
440}
441
442impl StaticSortedFile {
443 pub fn open(
448 db_path: &Path,
449 meta: StaticSortedFileMetaData,
450 compression: Compression,
451 access_mode: AccessMode,
452 ) -> Result<Self> {
453 let filename = format!("{:08}.sst", meta.sequence_number);
454 let path = db_path.join(&filename);
455 let file = File::open(&path)?;
456 let backing = match access_mode {
457 #[cfg(feature = "mmap")]
458 AccessMode::Mmap => {
459 let mmap = unsafe { Mmap::map(file.file()) }.with_context(|| {
460 format!(
461 "Failed to mmap SST file {} ({} bytes)",
462 path.display(),
463 file.metadata().map(|m| m.len()).unwrap_or(0)
464 )
465 })?;
466 #[cfg(unix)]
467 {
468 mmap.advise(memmap2::Advice::Random)?;
469 let offset = meta.block_offsets_start(mmap.len());
470 let _ =
471 mmap.advise_range(memmap2::Advice::Sequential, offset, mmap.len() - offset);
472 }
473 advise_mmap_for_persistence(&mmap)?;
474 StaticSortedFileBacking::Mmap(Arc::new(mmap))
475 }
476 AccessMode::File => {
477 let file_len: usize = file.metadata()?.len().try_into()?;
478 let offset = meta.block_offsets_start(file_len);
479 let mut bytes = vec![0; file_len - offset];
480 pread(file.file(), &mut bytes, offset as u64)?;
481 let block_offsets = bytes
482 .as_chunks::<4>()
483 .0
484 .iter()
485 .map(|bytes| be::read_u32(bytes))
486 .collect::<Vec<_>>()
487 .into();
488 StaticSortedFileBacking::File {
489 file: Arc::new(file),
490 file_len,
491 block_offsets,
492 }
493 }
494 };
495 let bitmap_words = (meta.block_count as usize).div_ceil(u64::BITS as usize);
496 let verified_blocks = (0..bitmap_words)
497 .map(|_| AtomicU64::new(0))
498 .collect::<Box<[_]>>();
499
500 let index = IndexBlock::parse(&backing, &meta)?;
501
502 Ok(Self {
503 meta,
504 backing,
505 verified_blocks,
506 compression,
507 index,
508 })
509 }
510
511 #[inline]
513 fn index_entries(&self) -> &[[u8; INDEX_BLOCK_ENTRY_SIZE]] {
514 let bytes = match (&self.index.entries, &self.backing) {
515 #[cfg(feature = "mmap")]
516 (IndexEntries::Mmap(range), StaticSortedFileBacking::Mmap(mmap)) => {
517 &mmap[range.clone()]
518 }
519 (IndexEntries::Owned(bytes), _) => &bytes[..],
520 #[cfg(feature = "mmap")]
523 (IndexEntries::Mmap(_), StaticSortedFileBacking::File { .. }) => unreachable!(
524 "mmap-ranged index entries with a file backing in {:08}.sst",
525 self.meta.sequence_number
526 ),
527 };
528 debug_assert!(
529 bytes.len().is_multiple_of(INDEX_BLOCK_ENTRY_SIZE),
530 "index entry range is not entry-aligned"
531 );
532 unsafe { bytes.as_chunks_unchecked::<INDEX_BLOCK_ENTRY_SIZE>() }
536 }
537
538 pub fn lookup<K: QueryKey, const FIND_ALL: bool>(
544 &self,
545 key_hash: u64,
546 key: &K,
547 key_block_cache: &BlockCache,
548 value_block_cache: &BlockCache,
549 ) -> Result<SstLookupResult> {
550 let key_block_index = self.lookup_index_block(key_hash);
552
553 let key_block = get_or_read_block(
556 &self.backing,
557 &self.meta,
558 key_block_index,
559 key_block_cache,
560 &self.verified_blocks,
561 self.compression,
562 )?;
563 let key_block = key_block.as_slice();
564
565 let reader = ArcBlockCacheReader {
566 backing: &self.backing,
567 cache: value_block_cache,
568 verified_blocks: &self.verified_blocks,
569 };
570 let block_type = be::read_u8(key_block);
571 match KeyBlockLayout::from_block_type(block_type) {
572 Some((layout, false)) => self
573 .lookup_variable_key_block::<K, FIND_ALL>(key_block, key_hash, key, layout, reader),
574 Some((layout, true)) => {
575 self.lookup_fixed_key_block::<K, FIND_ALL>(key_block, key_hash, key, layout, reader)
576 }
577 None => {
578 bail!("Invalid block type");
579 }
580 }
581 }
582
583 #[inline]
590 fn lookup_index_block(&self, hash: u64) -> u16 {
591 let entries = self.index_entries();
592 match entries.binary_search_by(|entry| be::read_u64(entry).cmp(&hash)) {
593 Ok(i) => be::read_u16(&entries[i][size_of::<u64>()..]),
594 Err(0) => self.index.first_block,
595 Err(i) => be::read_u16(&entries[i - 1][size_of::<u64>()..]),
596 }
597 }
598
599 fn lookup_variable_key_block<K: QueryKey, const FIND_ALL: bool>(
604 &self,
605 block: &[u8],
606 key_hash: u64,
607 key: &K,
608 layout: KeyBlockLayout,
609 reader: ArcBlockCacheReader<'_>,
610 ) -> Result<SstLookupResult> {
611 let hash_len = layout.hash_len();
612 ensure!(block.len() >= 4, "key block too short");
613 let entry_count = be::read_u24(&block[1..]) as usize;
614 let data = &block[4..];
615 let table_len = entry_count * key_block_table_stride(hash_len);
616 ensure!(
617 data.len() >= table_len,
618 "key block too short for {entry_count} entries"
619 );
620 let offsets = &data[..table_len];
621 let entries = &data[table_len..];
622
623 self.lookup_block_inner::<K, FIND_ALL>(entry_count, key_hash, key, layout, reader, |i| {
624 get_key_entry(offsets, entries, entry_count, i, hash_len)
625 })
626 }
627
628 fn lookup_fixed_key_block<K: QueryKey, const FIND_ALL: bool>(
633 &self,
634 block: &[u8],
635 key_hash: u64,
636 key: &K,
637 layout: KeyBlockLayout,
638 reader: ArcBlockCacheReader<'_>,
639 ) -> Result<SstLookupResult> {
640 ensure!(block.len() >= 6, "fixed key block too short");
641 let entry_count = be::read_u24(&block[1..]) as usize;
642 let key_size = be::read_u8(&block[4..]) as usize;
643 let header_type = be::read_u8(&block[5..]);
644 let FixedValueLayout {
645 value_type,
646 val_size,
647 header_size,
648 } = fixed_value_layout(block, header_type)?;
649 let regions = FixedRegions::new(entry_count, layout, key_size, val_size);
650 let entries = &block[header_size..];
651 ensure!(
652 entries.len() == regions.total_len(entry_count),
653 "fixed key block for {entry_count} entries is the wrong size"
654 );
655
656 self.lookup_block_inner::<K, FIND_ALL>(entry_count, key_hash, key, layout, reader, |i| {
657 get_fixed_key_entry(entries, i, regions, value_type)
658 })
659 }
660
661 fn lookup_block_inner<'a, K: QueryKey, const FIND_ALL: bool>(
666 &self,
667 entry_count: usize,
668 key_hash: u64,
669 key: &K,
670 layout: KeyBlockLayout,
671 reader: ArcBlockCacheReader<'_>,
672 get_entry: impl Fn(usize) -> Result<GetKeyEntryResult<'a>>,
673 ) -> Result<SstLookupResult> {
674 let mut l = 0;
675 let mut r = entry_count;
676 while l < r {
678 let m = (l + r) / 2;
679 let GetKeyEntryResult {
680 hash: mid_hash,
681 key: mid_key,
682 ty,
683 val,
684 } = get_entry(m)?;
685
686 let comparison = compare_hash_key(layout, mid_hash, mid_key, key_hash, key);
687
688 match comparison {
689 Ordering::Less => r = m,
690 Ordering::Equal => {
691 if !FIND_ALL {
692 let result = self.handle_key_match(ty, val, reader)?;
695 return Ok(SstLookupResult::Found(SmallVec::from_buf([result])));
696 }
697 let mut results = SmallVec::new();
702 for i in (l..m).rev() {
703 let GetKeyEntryResult {
704 hash,
705 key: entry_key,
706 ty,
707 val,
708 } = get_entry(i)?;
709 if !entry_matches_key(layout, hash, entry_key, key_hash, key) {
710 break;
711 }
712 results.push(self.handle_key_match(ty, val, reader)?);
713 }
714 results.reverse();
718
719 results.push(self.handle_key_match(ty, val, reader)?);
721 for i in (m + 1)..r {
722 let GetKeyEntryResult {
723 hash,
724 key: entry_key,
725 ty,
726 val,
727 } = get_entry(i)?;
728 if !entry_matches_key(layout, hash, entry_key, key_hash, key) {
729 break;
730 }
731 results.push(self.handle_key_match(ty, val, reader)?);
732 }
733 return Ok(SstLookupResult::Found(results));
734 }
735 Ordering::Greater => l = m + 1,
736 }
737 }
738
739 Ok(SstLookupResult::NotFound)
740 }
741
742 fn handle_key_match(
744 &self,
745 ty: u8,
746 val: &[u8],
747 reader: ArcBlockCacheReader<'_>,
748 ) -> Result<LookupValue> {
749 handle_key_match_generic(&self.meta, ty, val, self.compression, reader)
750 }
751}
752
753enum BlockRef<'l> {
761 #[cfg(feature = "mmap")]
763 Mmap(&'l [u8]),
764 Cached(ArcBytes, PhantomData<&'l ()>),
767}
768
769impl BlockRef<'_> {
770 #[inline]
771 fn as_slice(&self) -> &[u8] {
772 match self {
773 #[cfg(feature = "mmap")]
774 BlockRef::Mmap(data) => data,
775 BlockRef::Cached(block, _) => block,
776 }
777 }
778
779 #[inline]
783 #[cfg_attr(not(feature = "mmap"), allow(unused_variables))]
784 fn into_owned(self, backing: &StaticSortedFileBacking) -> ArcBytes {
785 match self {
786 #[cfg(feature = "mmap")]
787 BlockRef::Mmap(data) => {
788 let StaticSortedFileBacking::Mmap(mmap) = backing else {
789 unreachable!("mmap-borrowed block with a file backing")
791 };
792 unsafe { ArcBytes::from_mmap(mmap, data) }
794 }
795 BlockRef::Cached(block, _) => block,
796 }
797 }
798}
799
800fn get_or_read_block<'l>(
810 backing: &'l StaticSortedFileBacking,
811 meta: &StaticSortedFileMetaData,
812 block_index: u16,
813 cache: &BlockCache,
814 verified_blocks: &[AtomicU64],
815 compression: Compression,
816) -> Result<BlockRef<'l>> {
817 #[cfg(not(feature = "mmap"))]
820 let _ = verified_blocks;
821 #[cfg(feature = "mmap")]
822 let mmap_block = if let StaticSortedFileBacking::Mmap(mmap) = backing {
823 let (uncompressed_length, checksum, block_data) =
824 get_raw_block_slice(mmap, meta, block_index).with_context(|| {
825 format!(
826 "Failed to read raw block {} from {:08}.sst",
827 block_index, meta.sequence_number
828 )
829 })?;
830
831 if uncompressed_length == 0 {
832 verify_checksum_once(meta, block_data, checksum, block_index, verified_blocks)?;
835 return Ok(BlockRef::Mmap(block_data));
836 }
837 Some((uncompressed_length, checksum, block_data))
838 } else {
839 None
840 };
841 #[cfg(not(feature = "mmap"))]
844 let mmap_block: Option<(u32, u32, &[u8])> = None;
845
846 Ok(BlockRef::Cached(
849 match cache.get_value_or_guard(&(meta.sequence_number, block_index), None) {
850 GuardResult::Value(block) => block,
851 GuardResult::Guard(guard) => {
852 let (uncompressed_length, checksum, block_data) = match mmap_block {
853 Some((uncompressed_length, checksum, block_data)) => {
854 (uncompressed_length, checksum, Cow::Borrowed(block_data))
855 }
856 None => get_raw_block(backing, meta, block_index)?,
857 };
858 match backing {
861 #[cfg(feature = "mmap")]
862 StaticSortedFileBacking::Mmap(_) => verify_checksum_once(
863 meta,
864 &block_data,
865 checksum,
866 block_index,
867 verified_blocks,
868 )?,
869 StaticSortedFileBacking::File { .. } => {
870 verify_checksum(meta, &block_data, checksum, block_index)?
871 }
872 }
873 let block = if uncompressed_length == 0 {
874 ArcBytes::from(block_data.into_owned().into_boxed_slice())
875 } else {
876 ArcBytes::from_decompressed(compression, uncompressed_length, &block_data)
877 .with_context(|| {
878 format!(
879 "Failed to decompress block {} from {:08}.sst ({} bytes \
880 uncompressed)",
881 block_index, meta.sequence_number, uncompressed_length
882 )
883 })?
884 };
885 let _ = guard.insert(block.clone());
886 block
887 }
888 GuardResult::Timeout => unreachable!(),
889 },
890 PhantomData,
891 ))
892}
893
894#[cfg(feature = "mmap")]
897fn get_raw_block_slice<'a>(
898 mmap: &'a Mmap,
899 meta: &StaticSortedFileMetaData,
900 block_index: u16,
901) -> Result<(u32, u32, &'a [u8])> {
902 #[cfg(feature = "strict_checks")]
903 if block_index >= meta.block_count {
904 bail!(
905 "Corrupted file seq:{} block:{} > number of blocks {} (block_offsets: {:x})",
906 meta.sequence_number,
907 block_index,
908 meta.block_count,
909 meta.block_offsets_start(mmap.len()),
910 );
911 }
912 let offset = meta.block_offsets_start(mmap.len()) + block_index as usize * 4;
913 #[cfg(feature = "strict_checks")]
914 if offset + 4 > mmap.len() {
915 bail!(
916 "Corrupted file seq:{} block:{} block offset locations {} + 4 bytes > file end {} \
917 (block_offsets: {:x})",
918 meta.sequence_number,
919 block_index,
920 offset,
921 mmap.len(),
922 meta.block_offsets_start(mmap.len()),
923 );
924 }
925 let block_start = if block_index == 0 {
926 0
927 } else {
928 be::read_u32(&mmap[offset - 4..]) as usize
929 };
930 let block_end = be::read_u32(&mmap[offset..]) as usize;
931 #[cfg(feature = "strict_checks")]
932 if block_end > mmap.len() || block_start > mmap.len() {
933 bail!(
934 "Corrupted file seq:{} block:{} block {} - {} > file end {} (block_offsets: {:x})",
935 meta.sequence_number,
936 block_index,
937 block_start,
938 block_end,
939 mmap.len(),
940 meta.block_offsets_start(mmap.len()),
941 );
942 }
943 ensure!(
944 block_start + BLOCK_HEADER_SIZE <= block_end,
945 "block {} header truncated in {:08}.sst",
946 block_index,
947 meta.sequence_number
948 );
949 let uncompressed_length = be::read_u32(&mmap[block_start..]);
950 let checksum = be::read_u32(&mmap[block_start + 4..]);
951 let block = &mmap[block_start + BLOCK_HEADER_SIZE..block_end];
952 Ok((uncompressed_length, checksum, block))
953}
954
955fn get_raw_block<'a>(
957 backing: &'a StaticSortedFileBacking,
958 meta: &StaticSortedFileMetaData,
959 block_index: u16,
960) -> Result<(u32, u32, Cow<'a, [u8]>)> {
961 match backing {
962 #[cfg(feature = "mmap")]
963 StaticSortedFileBacking::Mmap(mmap) => {
964 let (uncompressed_length, checksum, block) =
965 get_raw_block_slice(mmap, meta, block_index)?;
966 Ok((uncompressed_length, checksum, Cow::Borrowed(block)))
967 }
968 StaticSortedFileBacking::File {
969 file,
970 file_len,
971 block_offsets,
972 } => {
973 let index = block_index as usize;
974 #[cfg(feature = "strict_checks")]
975 ensure!(index < block_offsets.len(), "block index out of bounds");
976 let block_start = if index == 0 {
977 0
978 } else {
979 block_offsets[index - 1] as usize
980 };
981 let block_end = block_offsets[index] as usize;
982 #[cfg(feature = "strict_checks")]
983 ensure!(block_end <= *file_len, "block end out of bounds");
984 let _ = file_len;
985 ensure!(
986 block_start + BLOCK_HEADER_SIZE <= block_end,
987 "block {} header truncated in {:08}.sst",
988 block_index,
989 meta.sequence_number
990 );
991 let mut bytes = vec![0; block_end - block_start];
992 pread(file.file(), &mut bytes, block_start as u64)?;
993 let uncompressed_length = be::read_u32(&bytes);
994 let checksum = be::read_u32(&bytes[4..]);
995 let block = bytes.split_off(BLOCK_HEADER_SIZE);
996 Ok((uncompressed_length, checksum, Cow::Owned(block)))
997 }
998 }
999}
1000
1001fn pread(file: &std::fs::File, buf: &mut [u8], offset: u64) -> io::Result<()> {
1002 #[cfg(unix)]
1003 {
1004 use std::os::unix::fs::FileExt;
1005 file.read_exact_at(buf, offset)
1006 }
1007 #[cfg(windows)]
1008 {
1009 use std::os::windows::fs::FileExt;
1010 let mut read = 0;
1011 while read < buf.len() {
1012 let count = file.seek_read(&mut buf[read..], offset + read as u64)?;
1013 if count == 0 {
1014 return Err(io::Error::new(
1015 io::ErrorKind::UnexpectedEof,
1016 "unexpected EOF",
1017 ));
1018 }
1019 read += count;
1020 }
1021 Ok(())
1022 }
1023 #[cfg(target_os = "wasi")]
1024 {
1025 use std::os::wasi::fs::FileExt;
1026 file.read_exact_at(buf, offset)
1027 }
1028}
1029
1030fn verify_checksum(
1032 meta: &StaticSortedFileMetaData,
1033 data: &[u8],
1034 expected: u32,
1035 block_index: u16,
1036) -> Result<()> {
1037 let actual = checksum_block(data);
1038 if actual != expected {
1039 bail!(
1040 "Cache corruption detected: checksum mismatch in block {} of {:08}.sst (expected \
1041 {:08x}, got {:08x})",
1042 block_index,
1043 meta.sequence_number,
1044 expected,
1045 actual
1046 );
1047 }
1048 Ok(())
1049}
1050
1051#[cfg_attr(not(feature = "mmap"), allow(dead_code))]
1058fn verify_checksum_once(
1059 meta: &StaticSortedFileMetaData,
1060 data: &[u8],
1061 expected: u32,
1062 block_index: u16,
1063 verified_blocks: &[AtomicU64],
1064) -> Result<()> {
1065 let word_idx = block_index as usize / u64::BITS as usize;
1066 let bit = 1u64 << (block_index as usize % u64::BITS as usize);
1067 if verified_blocks[word_idx].load(AtomicOrdering::Relaxed) & bit != 0 {
1068 return Ok(());
1069 }
1070 verify_checksum(meta, data, expected, block_index)?;
1071 verified_blocks[word_idx].fetch_or(bit, AtomicOrdering::Relaxed);
1072 Ok(())
1073}
1074
1075fn read_block_lookup(
1077 backing: &StaticSortedFileBacking,
1078 meta: &StaticSortedFileMetaData,
1079 block_index: u16,
1080 compression: Compression,
1081) -> Result<ArcBytes> {
1082 let (uncompressed_length, checksum, block) = get_raw_block(backing, meta, block_index)?;
1083 verify_checksum(meta, &block, checksum, block_index)?;
1084 if uncompressed_length == 0 {
1085 return match (backing, block) {
1086 #[cfg(feature = "mmap")]
1087 (StaticSortedFileBacking::Mmap(mmap), Cow::Borrowed(block)) => {
1088 Ok(unsafe { ArcBytes::from_mmap(mmap, block) })
1090 }
1091 (_, block) => Ok(ArcBytes::from(block.into_owned().into_boxed_slice())),
1092 };
1093 }
1094 ArcBytes::from_decompressed(compression, uncompressed_length, &block).with_context(|| {
1095 format!(
1096 "Failed to decompress block {} from {:08}.sst ({} bytes uncompressed)",
1097 block_index, meta.sequence_number, uncompressed_length
1098 )
1099 })
1100}
1101
1102fn get_raw_block_iter(
1105 backing: &StaticSortedFileIterBacking,
1106 meta: &StaticSortedFileMetaData,
1107 block_index: u16,
1108) -> Result<(u32, u32, RcBytes)> {
1109 match backing {
1110 #[cfg(feature = "mmap")]
1111 StaticSortedFileIterBacking::Mmap(mmap) => {
1112 let (uncompressed_length, checksum, block) =
1113 get_raw_block_slice(mmap, meta, block_index)?;
1114 Ok((uncompressed_length, checksum, unsafe {
1116 RcBytes::from_mmap(mmap, block)
1117 }))
1118 }
1119 StaticSortedFileIterBacking::File {
1120 file,
1121 file_len,
1122 block_offsets,
1123 } => {
1124 let index = block_index as usize;
1125 #[cfg(feature = "strict_checks")]
1126 ensure!(index < block_offsets.len(), "block index out of bounds");
1127 let block_start = if index == 0 {
1128 0
1129 } else {
1130 block_offsets[index - 1] as usize
1131 };
1132 let block_end = block_offsets[index] as usize;
1133 #[cfg(feature = "strict_checks")]
1134 ensure!(block_end <= *file_len, "block end out of bounds");
1135 let _ = file_len;
1136 ensure!(
1137 block_start + BLOCK_HEADER_SIZE <= block_end,
1138 "block {} header truncated in {:08}.sst",
1139 block_index,
1140 meta.sequence_number
1141 );
1142 let mut bytes = vec![0; block_end - block_start];
1143 pread(file.file(), &mut bytes, block_start as u64)?;
1144 let uncompressed_length = be::read_u32(&bytes);
1145 let checksum = be::read_u32(&bytes[4..]);
1146 let block = bytes.split_off(BLOCK_HEADER_SIZE);
1147 Ok((
1148 uncompressed_length,
1149 checksum,
1150 RcBytes::from(block.into_boxed_slice()),
1151 ))
1152 }
1153 }
1154}
1155
1156fn read_block_iter(
1158 backing: &StaticSortedFileIterBacking,
1159 meta: &StaticSortedFileMetaData,
1160 block_index: u16,
1161 compression: Compression,
1162) -> Result<RcBytes> {
1163 let (uncompressed_length, checksum, block) = get_raw_block_iter(backing, meta, block_index)?;
1164 verify_checksum(meta, &block, checksum, block_index)?;
1165 if uncompressed_length == 0 {
1166 return Ok(block);
1167 }
1168 RcBytes::from_decompressed(compression, uncompressed_length, &block).with_context(|| {
1169 format!(
1170 "Failed to decompress block {} from {:08}.sst ({} bytes uncompressed)",
1171 block_index, meta.sequence_number, uncompressed_length
1172 )
1173 })
1174}
1175
1176fn handle_key_match_generic<B: SharedBytes>(
1178 meta: &StaticSortedFileMetaData,
1179 ty: u8,
1180 val: &[u8],
1181 compression: Compression,
1182 reader: impl ValueBlockCache<B>,
1183) -> Result<LookupValue<B>> {
1184 Ok(match ty {
1185 KEY_BLOCK_ENTRY_TYPE_SMALL => {
1186 let block = be::read_u16(val);
1187 let size = be::read_u16(&val[2..]) as usize;
1188 let position = be::read_u32(&val[4..]) as usize;
1189 let value = reader
1190 .get_or_read(meta, block, compression)?
1191 .slice(position..position + size);
1192 LookupValue::Slice { value }
1193 }
1194 KEY_BLOCK_ENTRY_TYPE_MEDIUM => {
1195 let block = be::read_u16(val);
1196 let value = reader.read_uncached(meta, block, compression)?;
1197 LookupValue::Slice { value }
1198 }
1199 KEY_BLOCK_ENTRY_TYPE_BLOB => {
1200 let sequence_number = be::read_u32(val);
1201 LookupValue::Blob { sequence_number }
1202 }
1203 KEY_BLOCK_ENTRY_TYPE_KEY_DELETED => LookupValue::KeyDeleted,
1204 ty if ty >= KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN => {
1206 let value = B::from_inline(val);
1207 LookupValue::KeyValueDeleted { value }
1208 }
1209 _ => {
1210 let value = B::from_inline(val);
1212 LookupValue::Slice { value }
1213 }
1214 })
1215}
1216
1217enum StaticSortedFileIterBacking {
1218 #[cfg(feature = "mmap")]
1219 Mmap(Rc<Mmap>),
1220 File {
1221 file: Rc<File>,
1222 file_len: usize,
1223 block_offsets: Rc<[u32]>,
1224 },
1225}
1226
1227pub struct StaticSortedFileIter {
1229 backing: StaticSortedFileIterBacking,
1230 meta: StaticSortedFileMetaData,
1232
1233 index_entries: RcBytes,
1236 num_index_entries: usize,
1238 index_pos: usize,
1240 current_key_block: CurrentKeyBlock,
1241 value_block_cache: Option<(u16, RcBytes)>,
1245 compression: Compression,
1246}
1247
1248enum CurrentKeyBlockKind {
1249 Variable { offsets: RcBytes },
1251 Fixed {
1253 value_type: Option<u8>,
1255 regions: FixedRegions,
1256 },
1257}
1258
1259impl CurrentKeyBlockKind {
1260 fn entry<'l>(
1265 &'l self,
1266 entries: &'l [u8],
1267 entry_count: u32,
1268 index: usize,
1269 hash_len: u8,
1270 ) -> Result<GetKeyEntryResult<'l>> {
1271 match self {
1272 CurrentKeyBlockKind::Variable { offsets } => {
1273 get_key_entry(offsets, entries, entry_count as usize, index, hash_len)
1274 }
1275 CurrentKeyBlockKind::Fixed {
1276 value_type,
1277 regions,
1278 } => get_fixed_key_entry(entries, index, *regions, *value_type),
1279 }
1280 }
1281}
1282
1283struct HashOrderEntry {
1286 hash: u64,
1287 entry_index: u32,
1288}
1289
1290struct CurrentKeyBlock {
1291 kind: CurrentKeyBlockKind,
1292 layout: KeyBlockLayout,
1294 entries: RcBytes,
1295 entry_count: u32,
1297 index: u32,
1300 hash_order: Option<Vec<HashOrderEntry>>,
1303}
1304
1305impl Iterator for StaticSortedFileIter {
1306 type Item = Result<LookupEntry>;
1307
1308 fn next(&mut self) -> Option<Self::Item> {
1309 self.next_internal().transpose()
1310 }
1311}
1312
1313impl StaticSortedFileIter {
1314 pub fn open(
1316 db_path: &Path,
1317 meta: StaticSortedFileMetaData,
1318 compression: Compression,
1319 access_mode: AccessMode,
1320 ) -> Result<Self> {
1321 let filename = format!("{:08}.sst", meta.sequence_number);
1322 let path = db_path.join(&filename);
1323 let file = File::open(&path)?;
1324 let backing = match access_mode {
1325 #[cfg(feature = "mmap")]
1326 AccessMode::Mmap => {
1327 let mmap = unsafe { Mmap::map(file.file()) }.with_context(|| {
1328 format!(
1329 "Failed to mmap SST file {} ({} bytes)",
1330 path.display(),
1331 file.metadata().map(|m| m.len()).unwrap_or(0)
1332 )
1333 })?;
1334 #[cfg(unix)]
1335 mmap.advise(memmap2::Advice::Sequential)?;
1336 advise_mmap_for_persistence(&mmap)?;
1337 StaticSortedFileIterBacking::Mmap(Rc::new(mmap))
1338 }
1339 AccessMode::File => {
1340 let file_len: usize = file.metadata()?.len().try_into()?;
1341 let offset = meta.block_offsets_start(file_len);
1342 let mut bytes = vec![0; file_len - offset];
1343 pread(file.file(), &mut bytes, offset as u64)?;
1344 let block_offsets = bytes
1345 .as_chunks::<4>()
1346 .0
1347 .iter()
1348 .map(|bytes| be::read_u32(bytes))
1349 .collect::<Vec<_>>()
1350 .into();
1351 StaticSortedFileIterBacking::File {
1352 file: Rc::new(file),
1353 file_len,
1354 block_offsets,
1355 }
1356 }
1357 };
1358 Self::new(backing, meta, compression)
1359 .with_context(|| format!("Unable to open static sorted file {filename}"))
1360 }
1361
1362 fn new(
1363 backing: StaticSortedFileIterBacking,
1364 meta: StaticSortedFileMetaData,
1365 compression: Compression,
1366 ) -> Result<Self> {
1367 let root_block_index = meta.block_count - 1;
1368 let block = read_block_iter(&backing, &meta, root_block_index, compression)?;
1369 let block_type = block[0];
1370
1371 if block_type != BLOCK_TYPE_INDEX {
1373 bail!("Root block must be an index block");
1374 }
1375 let block_len = block.len();
1376 ensure!(block_len >= 3, "index block too short");
1377 let index_entries = block.slice(1..block_len);
1378 let first_child = be::read_u16(&index_entries);
1379 let num_index_entries: usize = (index_entries.len() + INDEX_BLOCK_ENTRY_SIZE
1384 - size_of::<u16>())
1385 / INDEX_BLOCK_ENTRY_SIZE;
1386
1387 let current_key_block = Self::parse_key_block(&backing, &meta, first_child, compression)?;
1388 Ok(StaticSortedFileIter {
1389 backing,
1390 meta,
1391 index_entries,
1392 num_index_entries,
1393 index_pos: 1,
1394 current_key_block,
1395 value_block_cache: None,
1396 compression,
1397 })
1398 }
1399
1400 fn parse_key_block(
1402 backing: &StaticSortedFileIterBacking,
1403 meta: &StaticSortedFileMetaData,
1404 block_index: u16,
1405 compression: Compression,
1406 ) -> Result<CurrentKeyBlock> {
1407 let block = read_block_iter(backing, meta, block_index, compression)?;
1408 let data = &*block;
1409 ensure!(data.len() >= 4, "key block too short");
1410 let block_type = data[0];
1411 let entry_count = be::read_u24(&data[1..]);
1412 let block_len = block.len();
1413 let Some((layout, fixed)) = KeyBlockLayout::from_block_type(block_type) else {
1414 bail!("Invalid key block type: {block_type}");
1415 };
1416 let hash_len = layout.hash_len();
1417
1418 let (kind, entries) = if fixed {
1419 ensure!(data.len() >= 6, "fixed key block too short");
1420 let key_size = data[4] as usize;
1423 let FixedValueLayout {
1424 value_type,
1425 val_size,
1426 header_size,
1427 } = fixed_value_layout(data, data[5])?;
1428 let regions = FixedRegions::new(entry_count as usize, layout, key_size, val_size);
1429 let entries = block.slice(header_size..block_len);
1430 ensure!(
1431 entries.len() == regions.total_len(entry_count as usize),
1432 "fixed key block for {entry_count} entries is the wrong size"
1433 );
1434 (
1435 CurrentKeyBlockKind::Fixed {
1436 value_type,
1437 regions,
1438 },
1439 entries,
1440 )
1441 } else {
1442 let offset_table_begin = 4usize;
1443 let offset_table_end = 4 + (entry_count as usize) * key_block_table_stride(hash_len);
1444 ensure!(
1445 block_len >= offset_table_end,
1446 "key block too short for {entry_count} entries"
1447 );
1448 let offsets = block.clone().slice(offset_table_begin..offset_table_end);
1449 let entries = block.slice(offset_table_end..block_len);
1450 (CurrentKeyBlockKind::Variable { offsets }, entries)
1451 };
1452
1453 let hash_order = match layout {
1455 KeyBlockLayout::HashThenKey => None,
1456 KeyBlockLayout::KeyOnly => Some(hash_order_for_block(entry_count, |i| {
1457 kind.entry(&entries, entry_count, i, hash_len)
1458 })?),
1459 };
1460
1461 Ok(CurrentKeyBlock {
1462 kind,
1463 layout,
1464 entries,
1465 entry_count,
1466 index: 0,
1467 hash_order,
1468 })
1469 }
1470
1471 fn next_internal(&mut self) -> Result<Option<LookupEntry>> {
1473 loop {
1474 let kb = &mut self.current_key_block;
1475 if kb.index < kb.entry_count {
1476 let (precomputed_hash, index) = match &kb.hash_order {
1477 None => (None, kb.index as usize),
1478 Some(hash_order) => {
1479 let HashOrderEntry { hash, entry_index } = hash_order[kb.index as usize];
1480 (Some(hash), entry_index as usize)
1481 }
1482 };
1483 let GetKeyEntryResult { hash, key, ty, val } =
1484 kb.kind
1485 .entry(&kb.entries, kb.entry_count, index, kb.layout.hash_len())?;
1486 let full_hash = match precomputed_hash {
1487 Some(hash) => hash,
1488 None => be::read_u64(hash),
1489 };
1490 let value = if ty == KEY_BLOCK_ENTRY_TYPE_MEDIUM {
1491 let block = be::read_u16(val);
1492 let (uncompressed_size, checksum, block) =
1493 get_raw_block_iter(&self.backing, &self.meta, block)?;
1494 IterValue::Medium {
1495 uncompressed_size,
1496 checksum,
1497 block,
1498 }
1499 } else {
1500 handle_key_match_generic(
1501 &self.meta,
1502 ty,
1503 val,
1504 self.compression,
1505 RcBlockCacheReader {
1506 backing: &self.backing,
1507 cache: &mut self.value_block_cache,
1508 },
1509 )?
1510 .into()
1511 };
1512 let entry = LookupEntry {
1513 hash: full_hash,
1514 key: unsafe { kb.entries.slice_from_subslice(key) },
1515 value,
1516 };
1517 kb.index += 1;
1518 return Ok(Some(entry));
1519 }
1520 if self.index_pos < self.num_index_entries {
1521 let base = self.index_pos * INDEX_BLOCK_ENTRY_SIZE;
1522 let block_index = be::read_u16(&self.index_entries[base..]);
1523 self.index_pos += 1;
1524 self.current_key_block = Self::parse_key_block(
1525 &self.backing,
1526 &self.meta,
1527 block_index,
1528 self.compression,
1529 )?;
1530 } else {
1531 return Ok(None);
1532 }
1533 }
1534 }
1535}
1536
1537struct GetKeyEntryResult<'l> {
1538 hash: &'l [u8],
1539 key: &'l [u8],
1540 ty: u8,
1541 val: &'l [u8],
1542}
1543
1544fn hash_order_for_block<'l>(
1547 entry_count: u32,
1548 get_entry: impl Fn(usize) -> Result<GetKeyEntryResult<'l>>,
1549) -> Result<Vec<HashOrderEntry>> {
1550 let mut order = Vec::with_capacity(entry_count as usize);
1551 for entry_index in 0..entry_count {
1552 let key = get_entry(entry_index as usize)?.key;
1553 order.push(HashOrderEntry {
1554 hash: crate::key::hash_key(&key),
1555 entry_index,
1556 });
1557 }
1558 order.sort_by_key(|entry| entry.hash);
1561 Ok(order)
1562}
1563
1564fn compare_hash_key<K: QueryKey>(
1567 layout: KeyBlockLayout,
1568 entry_hash: &[u8],
1569 entry_key: &[u8],
1570 full_hash: u64,
1571 query_key: &K,
1572) -> Ordering {
1573 match layout {
1574 KeyBlockLayout::KeyOnly => {
1575 debug_assert!(entry_hash.is_empty(), "KeyOnly entries carry no hash");
1576 query_key.cmp(entry_key)
1577 }
1578 KeyBlockLayout::HashThenKey => match full_hash.to_be_bytes()[..].cmp(entry_hash) {
1579 Ordering::Equal => query_key.cmp(entry_key),
1580 ord => ord,
1581 },
1582 }
1583}
1584
1585fn entry_matches_key<K: QueryKey>(
1587 layout: KeyBlockLayout,
1588 entry_hash: &[u8],
1589 entry_key: &[u8],
1590 full_hash: u64,
1591 query_key: &K,
1592) -> bool {
1593 match layout {
1594 KeyBlockLayout::KeyOnly => {
1595 debug_assert!(entry_hash.is_empty(), "KeyOnly entries carry no hash");
1596 query_key.eq(entry_key)
1597 }
1598 KeyBlockLayout::HashThenKey => {
1599 full_hash.to_be_bytes()[..] == *entry_hash && query_key.eq(entry_key)
1600 }
1601 }
1602}
1603
1604fn entry_val_size(ty: u8) -> Result<usize> {
1611 match ty {
1612 KEY_BLOCK_ENTRY_TYPE_SMALL => Ok(SMALL_VALUE_REF_SIZE),
1613 KEY_BLOCK_ENTRY_TYPE_MEDIUM => Ok(MEDIUM_VALUE_REF_SIZE),
1614 KEY_BLOCK_ENTRY_TYPE_BLOB => Ok(BLOB_VALUE_REF_SIZE),
1615 KEY_BLOCK_ENTRY_TYPE_KEY_DELETED => Ok(KEY_DELETED_REF_SIZE),
1616 ty if ty >= KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN => {
1618 let size = (ty - KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN) as usize;
1619 ensure!(
1620 size <= MAX_INLINE_VALUE_SIZE,
1621 "key-value tombstone type {ty} claims a {size} byte value, over the \
1622 {MAX_INLINE_VALUE_SIZE} byte maximum"
1623 );
1624 Ok(size)
1625 }
1626 ty if ty >= KEY_BLOCK_ENTRY_TYPE_INLINE_MIN => {
1627 let size = (ty - KEY_BLOCK_ENTRY_TYPE_INLINE_MIN) as usize;
1628 ensure!(
1629 size <= MAX_INLINE_VALUE_SIZE,
1630 "inline value type {ty} claims a {size} byte value, over the \
1631 {MAX_INLINE_VALUE_SIZE} byte maximum"
1632 );
1633 Ok(size)
1634 }
1635 _ => bail!("Invalid key block entry type: {ty}"),
1636 }
1637}
1638
1639#[inline(always)]
1645fn read_offset_entry(
1646 offsets: &[u8],
1647 index: usize,
1648 table_stride: usize,
1649 hash_len: u8,
1650) -> (u8, usize) {
1651 let base = index * table_stride + (hash_len as usize);
1653 let word = be::read_u32(&offsets[base..]);
1654 let ty = (word >> 24) as u8;
1655 let offset = (word & 0x00FF_FFFF) as usize;
1656 (ty, offset)
1657}
1658
1659fn get_key_entry<'l>(
1661 offsets: &'l [u8],
1662 entries: &'l [u8],
1663 entry_count: usize,
1664 index: usize,
1665 hash_len: u8,
1666) -> Result<GetKeyEntryResult<'l>> {
1667 let table_stride = key_block_table_stride(hash_len);
1668 let (ty, start) = read_offset_entry(offsets, index, table_stride, hash_len);
1669 let end = if index == entry_count - 1 {
1670 entries.len()
1671 } else {
1672 let (_, next_start) = read_offset_entry(offsets, index + 1, table_stride, hash_len);
1673 next_start
1674 };
1675 let hash = &offsets[index * table_stride..index * table_stride + hash_len as usize];
1677 let val_size = entry_val_size(ty)?;
1678 Ok(GetKeyEntryResult {
1679 hash,
1680 key: &entries[start..end - val_size],
1681 ty,
1682 val: &entries[end - val_size..end],
1683 })
1684}
1685
1686struct FixedValueLayout {
1692 value_type: Option<u8>,
1694 val_size: usize,
1696 header_size: usize,
1698}
1699
1700fn fixed_value_layout(block: &[u8], header_type: u8) -> Result<FixedValueLayout> {
1702 if header_type == FIXED_KEY_BLOCK_MIXED_VALUE_TYPE {
1703 ensure!(block.len() >= 7, "mixed-type fixed key block too short");
1706 let value_footprint = be::read_u8(&block[6..]) as usize;
1708 ensure!(
1709 value_footprint <= MAX_INLINE_VALUE_SIZE,
1710 "mixed-type fixed key block claims a {value_footprint} byte value footprint, over the \
1711 {MAX_INLINE_VALUE_SIZE} byte maximum"
1712 );
1713 Ok(FixedValueLayout {
1714 value_type: None,
1715 val_size: value_footprint + 1,
1717 header_size: 7,
1718 })
1719 } else {
1720 Ok(FixedValueLayout {
1721 value_type: Some(header_type),
1722 val_size: entry_val_size(header_type)?,
1723 header_size: 6,
1724 })
1725 }
1726}
1727
1728#[derive(Clone, Copy)]
1738pub struct FixedRegions {
1739 layout: KeyBlockLayout,
1741 pub search_stride: usize,
1743 pub tail_start: usize,
1745 pub tail_stride: usize,
1747 key_size: usize,
1748}
1749
1750impl FixedRegions {
1751 pub fn new(
1755 entry_count: usize,
1756 layout: KeyBlockLayout,
1757 key_size: usize,
1758 val_size: usize,
1759 ) -> Self {
1760 let (search_stride, tail_stride) = match layout {
1763 KeyBlockLayout::HashThenKey => (layout.hash_len() as usize, key_size + val_size),
1764 KeyBlockLayout::KeyOnly => (key_size, val_size),
1765 };
1766 Self {
1767 layout,
1768 search_stride,
1769 tail_start: entry_count * search_stride,
1770 tail_stride,
1771 key_size,
1772 }
1773 }
1774
1775 pub fn tail_key_size(&self) -> usize {
1778 match self.layout {
1779 KeyBlockLayout::HashThenKey => self.key_size,
1780 KeyBlockLayout::KeyOnly => 0,
1781 }
1782 }
1783
1784 pub fn total_len(&self, entry_count: usize) -> usize {
1786 self.tail_start + entry_count * self.tail_stride
1787 }
1788}
1789
1790fn get_fixed_key_entry<'l>(
1791 entries: &'l [u8],
1792 index: usize,
1793 regions: FixedRegions,
1794 value_type: Option<u8>,
1795) -> Result<GetKeyEntryResult<'l>> {
1796 let FixedRegions {
1797 layout,
1798 search_stride,
1799 tail_start,
1800 tail_stride,
1801 key_size,
1802 } = regions;
1803 let search = index * search_stride;
1807 let tail = tail_start + index * tail_stride;
1808 let (hash, key, tail_rest) = match layout {
1809 KeyBlockLayout::HashThenKey => (
1810 &entries[search..search + search_stride],
1811 &entries[tail..tail + key_size],
1812 tail + key_size,
1813 ),
1814 KeyBlockLayout::KeyOnly => (&entries[..0], &entries[search..search + key_size], tail),
1815 };
1816 let (ty, val_start) = match value_type {
1818 Some(ty) => (ty, tail_rest),
1819 None => (be::read_u8(&entries[tail_rest..]), tail_rest + 1),
1820 };
1821 Ok(GetKeyEntryResult {
1822 hash,
1823 key,
1824 ty,
1825 val: &entries[val_start..tail + tail_stride],
1826 })
1827}
1828
1829#[cfg(test)]
1830mod tests {
1831 use super::*;
1832
1833 #[test]
1837 fn block_type_round_trips() {
1838 for layout in [KeyBlockLayout::HashThenKey, KeyBlockLayout::KeyOnly] {
1839 for fixed in [false, true] {
1840 let byte = layout.block_type(fixed);
1841 assert_eq!(
1842 KeyBlockLayout::from_block_type(byte),
1843 Some((layout, fixed)),
1844 "{layout:?} (fixed={fixed}) encoded as {byte} did not round-trip"
1845 );
1846 }
1847 }
1848 }
1849
1850 #[test]
1853 fn block_types_are_distinct() {
1854 let mut seen = vec![BLOCK_TYPE_INDEX];
1855 for layout in [KeyBlockLayout::HashThenKey, KeyBlockLayout::KeyOnly] {
1856 for fixed in [false, true] {
1857 let byte = layout.block_type(fixed);
1858 assert!(!seen.contains(&byte), "block type {byte} is used twice");
1859 seen.push(byte);
1860 }
1861 }
1862 assert!(KeyBlockLayout::from_block_type(BLOCK_TYPE_INDEX).is_none());
1863 }
1864
1865 #[test]
1868 fn hash_len_matches_layout() {
1869 assert_eq!(
1870 KeyBlockLayout::HashThenKey.hash_len() as usize,
1871 size_of::<u64>()
1872 );
1873 assert_eq!(KeyBlockLayout::KeyOnly.hash_len(), 0);
1874 }
1875}