1use std::{
2 cmp::Ordering,
3 hash::BuildHasherDefault,
4 io,
5 marker::PhantomData,
6 ops::{Deref, Range},
7 path::Path,
8 sync::{
9 Arc,
10 atomic::{AtomicU64, Ordering as AtomicOrdering},
11 },
12};
13
14use anyhow::{Context, Result, bail, ensure};
15use fs_err::File;
16#[cfg(feature = "mmap")]
17use memmap2::Mmap;
18use quick_cache::{Lifecycle, sync::GuardResult};
19use rustc_hash::FxHasher;
20use smallvec::SmallVec;
21use zerocopy::IntoBytes;
22
23#[cfg(feature = "mmap")]
24use crate::mmap_helper::advise_mmap_for_persistence;
25use crate::{
26 AccessMode, Compression, QueryKey,
27 arc_bytes::ArcBytes,
28 be,
29 compression::checksum_block,
30 constants::MAX_INLINE_VALUE_SIZE,
31 lookup_entry::{IterValue, LookupEntry, LookupValue},
32 rc_bytes::RcBytes,
33 shared_bytes::SharedBytes,
34 static_sorted_file_builder::{
35 BLOCK_HEADER_SIZE, INDEX_BLOCK_ENTRY_SIZE, INDEX_BLOCK_HEADER_SIZE,
36 },
37};
38
39pub const BLOCK_TYPE_INDEX: u8 = 0;
41pub const BLOCK_TYPE_KEY_WITH_HASH: u8 = 1;
43pub const BLOCK_TYPE_KEY_NO_HASH: u8 = 2;
45pub const BLOCK_TYPE_FIXED_KEY_WITH_HASH: u8 = 3;
47pub const BLOCK_TYPE_FIXED_KEY_NO_HASH: u8 = 4;
49
50#[derive(Clone, Copy, PartialEq, Eq, Debug)]
52pub enum KeyBlockLayout {
53 HashThenKey,
55 KeyOnly,
57}
58
59impl KeyBlockLayout {
60 #[inline]
62 pub fn hash_len(self) -> u8 {
63 match self {
64 KeyBlockLayout::HashThenKey => size_of::<u64>() as u8,
65 KeyBlockLayout::KeyOnly => 0,
66 }
67 }
68
69 #[inline]
71 pub fn block_type(self, fixed: bool) -> u8 {
72 match (self, fixed) {
73 (KeyBlockLayout::HashThenKey, false) => BLOCK_TYPE_KEY_WITH_HASH,
74 (KeyBlockLayout::KeyOnly, false) => BLOCK_TYPE_KEY_NO_HASH,
75 (KeyBlockLayout::HashThenKey, true) => BLOCK_TYPE_FIXED_KEY_WITH_HASH,
76 (KeyBlockLayout::KeyOnly, true) => BLOCK_TYPE_FIXED_KEY_NO_HASH,
77 }
78 }
79
80 #[inline]
83 pub fn from_block_type(block_type: u8) -> Option<(Self, bool)> {
84 match block_type {
85 BLOCK_TYPE_KEY_WITH_HASH => Some((KeyBlockLayout::HashThenKey, false)),
86 BLOCK_TYPE_KEY_NO_HASH => Some((KeyBlockLayout::KeyOnly, false)),
87 BLOCK_TYPE_FIXED_KEY_WITH_HASH => Some((KeyBlockLayout::HashThenKey, true)),
88 BLOCK_TYPE_FIXED_KEY_NO_HASH => Some((KeyBlockLayout::KeyOnly, true)),
89 _ => None,
90 }
91 }
92}
93
94pub const FIXED_KEY_BLOCK_MIXED_VALUE_TYPE: u8 = 4;
97
98pub const KEY_BLOCK_ENTRY_TYPE_SMALL: u8 = 0;
100pub const KEY_BLOCK_ENTRY_TYPE_BLOB: u8 = 1;
102pub const KEY_BLOCK_ENTRY_TYPE_KEY_DELETED: u8 = 2;
104pub const KEY_BLOCK_ENTRY_TYPE_MEDIUM: u8 = 3;
106pub const KEY_BLOCK_ENTRY_TYPE_INLINE_MIN: u8 = 8;
108pub const KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN: u8 =
117 KEY_BLOCK_ENTRY_TYPE_INLINE_MIN + MAX_INLINE_VALUE_SIZE as u8 + 1;
118
119pub const KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH: usize = 4;
122pub const KEY_BLOCK_TABLE_ENTRY_SIZE_WITH_HASH: usize =
130 KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH + size_of::<u64>();
131
132#[inline(always)]
134pub fn key_block_table_stride(hash_len: u8) -> usize {
135 KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH + hash_len as usize
136}
137
138pub(crate) const SMALL_VALUE_REF_SIZE: usize = 8;
140pub(crate) const MEDIUM_VALUE_REF_SIZE: usize = 2;
142pub(crate) const BLOB_VALUE_REF_SIZE: usize = 4;
144pub(crate) const KEY_DELETED_REF_SIZE: usize = 0;
146
147const _: () = assert!(
151 MAX_INLINE_VALUE_SIZE <= (u8::MAX - KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN) as usize,
152 "MAX_INLINE_VALUE_SIZE exceeds what can be encoded in key type byte"
153);
154
155pub enum SstLookupResult {
157 Found(SmallVec<[LookupValue; 1]>),
159 NotFound,
161}
162
163impl From<LookupValue> for SstLookupResult {
164 fn from(value: LookupValue) -> Self {
165 SstLookupResult::Found(smallvec::smallvec![value])
166 }
167}
168
169#[derive(Clone, Default)]
170pub struct BlockWeighter;
171
172impl quick_cache::Weighter<(u32, u16), ArcBytes> for BlockWeighter {
173 fn weight(&self, _key: &(u32, u16), val: &ArcBytes) -> u64 {
174 if val.is_mmap_backed() {
175 debug_assert!(
178 !val.is_mmap_backed(),
179 "mmap-backed block should not be inserted into BlockCache"
180 );
181 64
182 } else {
183 val.len() as u64 + 8
184 }
185 }
186}
187
188#[derive(Clone, Default)]
191pub struct BlockCacheLifecycle;
192
193impl Lifecycle<(u32, u16), ArcBytes> for BlockCacheLifecycle {
194 type RequestState = ();
195
196 #[inline]
197 fn is_pinned(&self, _key: &(u32, u16), val: &ArcBytes) -> bool {
198 val.is_shared_arc()
199 }
200
201 #[inline]
202 fn begin_request(&self) -> Self::RequestState {}
203
204 #[inline]
205 fn on_evict(&self, _state: &mut Self::RequestState, _key: (u32, u16), _val: ArcBytes) {}
206}
207
208pub type BlockCache = quick_cache::sync::Cache<
209 (u32, u16),
210 ArcBytes,
211 BlockWeighter,
212 BuildHasherDefault<FxHasher>,
213 BlockCacheLifecycle,
214>;
215
216trait ValueBlockCache<B: SharedBytes> {
218 fn get_or_read(
219 self,
220 meta: &StaticSortedFileMetaData,
221 block_index: u16,
222 compression: Compression,
223 ) -> Result<B>;
224 fn read_uncached(
225 self,
226 meta: &StaticSortedFileMetaData,
227 block_index: u16,
228 compression: Compression,
229 ) -> Result<B>;
230}
231
232#[derive(Clone, Copy)]
234struct ArcBlockCacheReader<'a> {
235 backing: &'a StaticSortedFileBacking,
236 block_offsets: &'a BlockOffsets,
237 cache: &'a BlockCache,
238 verified_blocks: &'a [AtomicU64],
239}
240
241impl ValueBlockCache<ArcBytes> for ArcBlockCacheReader<'_> {
242 fn get_or_read(
243 self,
244 meta: &StaticSortedFileMetaData,
245 block_index: u16,
246 compression: Compression,
247 ) -> Result<ArcBytes> {
248 Ok(get_or_read_block(
252 self.backing,
253 self.block_offsets,
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(
270 self.backing,
271 self.block_offsets,
272 meta,
273 block_index,
274 compression,
275 )
276 }
277}
278
279struct RcBlockCacheReader<'a> {
281 file: &'a File,
282 block_offsets: &'a BlockOffsets,
283 cache: &'a mut Option<(u16, RcBytes)>,
284}
285
286impl ValueBlockCache<RcBytes> for RcBlockCacheReader<'_> {
287 fn get_or_read(
288 self,
289 meta: &StaticSortedFileMetaData,
290 block_index: u16,
291 compression: Compression,
292 ) -> Result<RcBytes> {
293 if let Some((idx, block)) = self.cache.as_ref()
294 && *idx == block_index
295 {
296 return Ok(block.clone());
297 }
298 let block = read_block_iter(
299 self.file,
300 self.block_offsets,
301 meta,
302 block_index,
303 compression,
304 )?;
305 *self.cache = Some((block_index, block.clone()));
306 Ok(block)
307 }
308
309 fn read_uncached(
310 self,
311 meta: &StaticSortedFileMetaData,
312 block_index: u16,
313 compression: Compression,
314 ) -> Result<RcBytes> {
315 read_block_iter(
316 self.file,
317 self.block_offsets,
318 meta,
319 block_index,
320 compression,
321 )
322 }
323}
324
325#[derive(Clone, Copy, Debug)]
326pub struct StaticSortedFileMetaData {
327 pub sequence_number: u32,
329 pub block_count: u16,
331}
332
333#[derive(Debug)]
339struct BlockOffsets(Box<[u32]>);
340
341impl BlockOffsets {
342 fn read(file: &File, meta: &StaticSortedFileMetaData, file_len: usize) -> Result<Self> {
345 let block_count = meta.block_count as usize;
346 let table_start = file_len
347 .checked_sub(block_count * size_of::<u32>())
348 .with_context(|| {
349 format!(
350 "{:08}.sst is too short ({file_len} bytes) for its block-offset table of \
351 {block_count} blocks",
352 meta.sequence_number
353 )
354 })?;
355 let mut offsets = vec![0u32; block_count].into_boxed_slice();
356 pread(file, offsets.as_mut_bytes(), table_start as u64).with_context(|| {
357 format!(
358 "Failed to read the block-offset table of {:08}.sst",
359 meta.sequence_number
360 )
361 })?;
362 let mut start = 0;
363 for (index, offset) in offsets.iter_mut().enumerate() {
364 *offset = u32::from_be(*offset);
365 let end = *offset as usize;
366 ensure!(
367 end.checked_sub(start)
368 .is_some_and(|len| len >= BLOCK_HEADER_SIZE),
369 "block {index} of {:08}.sst spans {start}..{end}, shorter than its \
370 {BLOCK_HEADER_SIZE}-byte header",
371 meta.sequence_number
372 );
373 start = end;
374 }
375 ensure!(
376 start <= table_start,
377 "the blocks of {:08}.sst end at {start}, past the start of its block-offset table at \
378 {table_start}",
379 meta.sequence_number
380 );
381 Ok(Self(offsets))
382 }
383
384 #[inline]
388 fn block_range(&self, block_index: u16) -> Result<Range<usize>> {
389 let index = block_index as usize;
390 let Some(&end) = self.0.get(index) else {
391 bail!(
392 "block index {block_index} out of bounds ({} blocks)",
393 self.0.len()
394 );
395 };
396 let start = if index == 0 {
397 0
398 } else {
399 self.0[index - 1] as usize
400 };
401 Ok(start..end as usize)
402 }
403}
404
405enum StaticSortedFileBacking {
406 #[cfg(feature = "mmap")]
407 Mmap(Arc<Mmap>),
408 File {
409 file: Arc<File>,
410 },
411}
412
413pub struct StaticSortedFile {
415 meta: StaticSortedFileMetaData,
417 backing: StaticSortedFileBacking,
418 block_offsets: BlockOffsets,
419 verified_blocks: Box<[AtomicU64]>,
424 compression: Compression,
425 index: IndexBlock,
427}
428
429struct IndexBlock {
436 entries: Box<[[u8; INDEX_BLOCK_ENTRY_SIZE]]>,
438 first_block: u16,
440}
441
442impl IndexBlock {
443 fn read(
445 file: &File,
446 block_offsets: &BlockOffsets,
447 meta: &StaticSortedFileMetaData,
448 ) -> Result<Self> {
449 let data: ArcBytes = read_index_block(file, block_offsets, meta)?;
450 let block_index = meta.block_count - 1;
451 ensure!(
452 data.len() >= INDEX_BLOCK_HEADER_SIZE,
453 "index block {} of {:08}.sst is too short ({} bytes)",
454 block_index,
455 meta.sequence_number,
456 data.len()
457 );
458 let first_block = be::read_u16(&data[1..]);
459 let (entries, rest) = data[INDEX_BLOCK_HEADER_SIZE..].as_chunks::<INDEX_BLOCK_ENTRY_SIZE>();
460 ensure!(
461 rest.is_empty(),
462 "index block {} of {:08}.sst has {} trailing bytes past its last entry",
463 block_index,
464 meta.sequence_number,
465 rest.len()
466 );
467 Ok(Self {
468 entries: entries.into(),
469 first_block,
470 })
471 }
472}
473
474fn read_index_block<B: SharedBytes>(
479 file: &File,
480 block_offsets: &BlockOffsets,
481 meta: &StaticSortedFileMetaData,
482) -> Result<B> {
483 ensure!(
484 meta.block_count > 0,
485 "{:08}.sst has no blocks, so no index block",
486 meta.sequence_number
487 );
488 let block_index = meta.block_count - 1;
489 let range = block_range(block_offsets, meta, block_index)?;
490 let (uncompressed_length, checksum, data) =
491 pread_block::<B>(file, range).with_context(|| {
492 format!(
493 "Failed to read index block {} from {:08}.sst",
494 block_index, meta.sequence_number
495 )
496 })?;
497 ensure!(
498 uncompressed_length == 0,
499 "index block {} of {:08}.sst is compressed, but index blocks are always written \
500 uncompressed",
501 block_index,
502 meta.sequence_number
503 );
504 verify_checksum(meta, &data, checksum, block_index)?;
507 ensure!(
508 data.first() == Some(&BLOCK_TYPE_INDEX),
509 "block {} of {:08}.sst is the last block but not an index block (type {:?})",
510 block_index,
511 meta.sequence_number,
512 data.first()
513 );
514 Ok(data)
515}
516
517impl StaticSortedFile {
518 pub fn open(
523 db_path: &Path,
524 meta: StaticSortedFileMetaData,
525 compression: Compression,
526 access_mode: AccessMode,
527 ) -> Result<Self> {
528 let filename = format!("{:08}.sst", meta.sequence_number);
529 let path = db_path.join(&filename);
530 let file = File::open(&path)?;
531 let file_len: usize = file.metadata()?.len().try_into()?;
532 let block_offsets = BlockOffsets::read(&file, &meta, file_len)?;
533 let index = IndexBlock::read(&file, &block_offsets, &meta)?;
534 let backing = match access_mode {
535 #[cfg(feature = "mmap")]
536 AccessMode::Mmap => {
537 let mmap = unsafe { Mmap::map(file.file()) }.with_context(|| {
538 format!(
539 "Failed to mmap SST file {} ({file_len} bytes)",
540 path.display(),
541 )
542 })?;
543 ensure!(
544 mmap.len() == file_len,
545 "{filename} changed size while being opened ({file_len} -> {} bytes)",
546 mmap.len()
547 );
548 #[cfg(unix)]
553 if page_size().is_some() {
554 mmap.advise(memmap2::Advice::Random)?;
555 }
556 advise_mmap_for_persistence(&mmap)?;
557 StaticSortedFileBacking::Mmap(Arc::new(mmap))
558 }
559 AccessMode::File => StaticSortedFileBacking::File {
560 file: Arc::new(file),
561 },
562 };
563 let bitmap_words = (meta.block_count as usize).div_ceil(u64::BITS as usize);
564 let verified_blocks = (0..bitmap_words)
565 .map(|_| AtomicU64::new(0))
566 .collect::<Box<[_]>>();
567
568 Ok(Self {
569 meta,
570 backing,
571 block_offsets,
572 verified_blocks,
573 compression,
574 index,
575 })
576 }
577
578 #[inline]
580 fn index_entries(&self) -> &[[u8; INDEX_BLOCK_ENTRY_SIZE]] {
581 &self.index.entries
582 }
583
584 pub fn lookup<K: QueryKey, const FIND_ALL: bool>(
590 &self,
591 key_hash: u64,
592 key: &K,
593 key_block_cache: &BlockCache,
594 value_block_cache: &BlockCache,
595 ) -> Result<SstLookupResult> {
596 let key_block_index = self.lookup_index_block(key_hash);
598
599 let key_block = get_or_read_block(
602 &self.backing,
603 &self.block_offsets,
604 &self.meta,
605 key_block_index,
606 key_block_cache,
607 &self.verified_blocks,
608 self.compression,
609 )?;
610 let key_block = key_block.as_slice();
611
612 let reader = ArcBlockCacheReader {
613 backing: &self.backing,
614 block_offsets: &self.block_offsets,
615 cache: value_block_cache,
616 verified_blocks: &self.verified_blocks,
617 };
618 let block_type = be::read_u8(key_block);
619 match KeyBlockLayout::from_block_type(block_type) {
620 Some((layout, false)) => self
621 .lookup_variable_key_block::<K, FIND_ALL>(key_block, key_hash, key, layout, reader),
622 Some((layout, true)) => {
623 self.lookup_fixed_key_block::<K, FIND_ALL>(key_block, key_hash, key, layout, reader)
624 }
625 None => {
626 bail!("Invalid block type");
627 }
628 }
629 }
630
631 #[inline]
638 fn lookup_index_block(&self, hash: u64) -> u16 {
639 let entries = self.index_entries();
640 match entries.binary_search_by(|entry| be::read_u64(entry).cmp(&hash)) {
641 Ok(i) => be::read_u16(&entries[i][size_of::<u64>()..]),
642 Err(0) => self.index.first_block,
643 Err(i) => be::read_u16(&entries[i - 1][size_of::<u64>()..]),
644 }
645 }
646
647 fn lookup_variable_key_block<K: QueryKey, const FIND_ALL: bool>(
652 &self,
653 block: &[u8],
654 key_hash: u64,
655 key: &K,
656 layout: KeyBlockLayout,
657 reader: ArcBlockCacheReader<'_>,
658 ) -> Result<SstLookupResult> {
659 let hash_len = layout.hash_len();
660 ensure!(block.len() >= 4, "key block too short");
661 let entry_count = be::read_u24(&block[1..]) as usize;
662 let data = &block[4..];
663 let table_len = entry_count * key_block_table_stride(hash_len);
664 ensure!(
665 data.len() >= table_len,
666 "key block too short for {entry_count} entries"
667 );
668 let offsets = &data[..table_len];
669 let entries = &data[table_len..];
670
671 self.lookup_block_inner::<K, FIND_ALL>(entry_count, key_hash, key, layout, reader, |i| {
672 get_key_entry(offsets, entries, entry_count, i, hash_len)
673 })
674 }
675
676 fn lookup_fixed_key_block<K: QueryKey, const FIND_ALL: bool>(
681 &self,
682 block: &[u8],
683 key_hash: u64,
684 key: &K,
685 layout: KeyBlockLayout,
686 reader: ArcBlockCacheReader<'_>,
687 ) -> Result<SstLookupResult> {
688 ensure!(block.len() >= 6, "fixed key block too short");
689 let entry_count = be::read_u24(&block[1..]) as usize;
690 let key_size = be::read_u8(&block[4..]) as usize;
691 let header_type = be::read_u8(&block[5..]);
692 let FixedValueLayout {
693 value_type,
694 val_size,
695 header_size,
696 } = fixed_value_layout(block, header_type)?;
697 let regions = FixedRegions::new(entry_count, layout, key_size, val_size);
698 let entries = &block[header_size..];
699 ensure!(
700 entries.len() == regions.total_len(entry_count),
701 "fixed key block for {entry_count} entries is the wrong size"
702 );
703
704 self.lookup_block_inner::<K, FIND_ALL>(entry_count, key_hash, key, layout, reader, |i| {
705 get_fixed_key_entry(entries, i, regions, value_type)
706 })
707 }
708
709 fn lookup_block_inner<'a, K: QueryKey, const FIND_ALL: bool>(
714 &self,
715 entry_count: usize,
716 key_hash: u64,
717 key: &K,
718 layout: KeyBlockLayout,
719 reader: ArcBlockCacheReader<'_>,
720 get_entry: impl Fn(usize) -> Result<GetKeyEntryResult<'a>>,
721 ) -> Result<SstLookupResult> {
722 let mut l = 0;
723 let mut r = entry_count;
724 while l < r {
726 let m = (l + r) / 2;
727 let GetKeyEntryResult {
728 hash: mid_hash,
729 key: mid_key,
730 ty,
731 val,
732 } = get_entry(m)?;
733
734 let comparison = compare_hash_key(layout, mid_hash, mid_key, key_hash, key);
735
736 match comparison {
737 Ordering::Less => r = m,
738 Ordering::Equal => {
739 if !FIND_ALL {
740 let result = self.handle_key_match(ty, val, reader)?;
743 return Ok(SstLookupResult::Found(SmallVec::from_buf([result])));
744 }
745 let mut results = SmallVec::new();
750 for i in (l..m).rev() {
751 let GetKeyEntryResult {
752 hash,
753 key: entry_key,
754 ty,
755 val,
756 } = get_entry(i)?;
757 if !entry_matches_key(layout, hash, entry_key, key_hash, key) {
758 break;
759 }
760 results.push(self.handle_key_match(ty, val, reader)?);
761 }
762 results.reverse();
766
767 results.push(self.handle_key_match(ty, val, reader)?);
769 for i in (m + 1)..r {
770 let GetKeyEntryResult {
771 hash,
772 key: entry_key,
773 ty,
774 val,
775 } = get_entry(i)?;
776 if !entry_matches_key(layout, hash, entry_key, key_hash, key) {
777 break;
778 }
779 results.push(self.handle_key_match(ty, val, reader)?);
780 }
781 return Ok(SstLookupResult::Found(results));
782 }
783 Ordering::Greater => l = m + 1,
784 }
785 }
786
787 Ok(SstLookupResult::NotFound)
788 }
789
790 fn handle_key_match(
792 &self,
793 ty: u8,
794 val: &[u8],
795 reader: ArcBlockCacheReader<'_>,
796 ) -> Result<LookupValue> {
797 handle_key_match_generic(&self.meta, ty, val, self.compression, reader)
798 }
799}
800
801enum BlockRef<'l> {
809 #[cfg(feature = "mmap")]
811 Mmap(&'l [u8]),
812 Cached(ArcBytes, PhantomData<&'l ()>),
815}
816
817impl BlockRef<'_> {
818 #[inline]
819 fn as_slice(&self) -> &[u8] {
820 match self {
821 #[cfg(feature = "mmap")]
822 BlockRef::Mmap(data) => data,
823 BlockRef::Cached(block, _) => block,
824 }
825 }
826
827 #[inline]
831 #[cfg_attr(not(feature = "mmap"), allow(unused_variables))]
832 fn into_owned(self, backing: &StaticSortedFileBacking) -> ArcBytes {
833 match self {
834 #[cfg(feature = "mmap")]
835 BlockRef::Mmap(data) => {
836 let StaticSortedFileBacking::Mmap(mmap) = backing else {
837 unreachable!("mmap-borrowed block with a file backing")
839 };
840 unsafe { ArcBytes::from_mmap(mmap, data) }
842 }
843 BlockRef::Cached(block, _) => block,
844 }
845 }
846}
847
848fn get_or_read_block<'l>(
858 backing: &'l StaticSortedFileBacking,
859 block_offsets: &BlockOffsets,
860 meta: &StaticSortedFileMetaData,
861 block_index: u16,
862 cache: &BlockCache,
863 verified_blocks: &[AtomicU64],
864 compression: Compression,
865) -> Result<BlockRef<'l>> {
866 #[cfg(not(feature = "mmap"))]
869 let _ = verified_blocks;
870 #[cfg(feature = "mmap")]
871 let mmap_block = if let StaticSortedFileBacking::Mmap(mmap) = backing {
872 let will_need = !is_block_verified(block_index, verified_blocks);
876 let (uncompressed_length, checksum, block_data) =
877 get_raw_block_slice(mmap, block_offsets, meta, block_index, will_need).with_context(
878 || {
879 format!(
880 "Failed to read raw block {} from {:08}.sst",
881 block_index, meta.sequence_number
882 )
883 },
884 )?;
885
886 if uncompressed_length == 0 {
887 verify_checksum_once(meta, block_data, checksum, block_index, verified_blocks)?;
890 return Ok(BlockRef::Mmap(block_data));
891 }
892 Some((uncompressed_length, checksum, block_data))
893 } else {
894 None
895 };
896 #[cfg(not(feature = "mmap"))]
899 let mmap_block: Option<(u32, u32, &[u8])> = None;
900
901 Ok(BlockRef::Cached(
904 match cache.get_value_or_guard(&(meta.sequence_number, block_index), None) {
905 GuardResult::Value(block) => block,
906 GuardResult::Guard(guard) => {
907 let (uncompressed_length, checksum, block_data) = match mmap_block {
908 Some((uncompressed_length, checksum, block_data)) => (
909 uncompressed_length,
910 checksum,
911 RawBlock::Borrowed(block_data),
912 ),
913 None => get_raw_block(backing, block_offsets, meta, block_index)?,
914 };
915 match backing {
918 #[cfg(feature = "mmap")]
919 StaticSortedFileBacking::Mmap(_) => verify_checksum_once(
920 meta,
921 &block_data,
922 checksum,
923 block_index,
924 verified_blocks,
925 )?,
926 StaticSortedFileBacking::File { .. } => {
927 verify_checksum(meta, &block_data, checksum, block_index)?
928 }
929 }
930 let block = if uncompressed_length == 0 {
931 block_data.into_arc_bytes()
932 } else {
933 ArcBytes::from_decompressed(compression, uncompressed_length, &block_data)
934 .with_context(|| {
935 format!(
936 "Failed to decompress block {} from {:08}.sst ({} bytes \
937 uncompressed)",
938 block_index, meta.sequence_number, uncompressed_length
939 )
940 })?
941 };
942 let _ = guard.insert(block.clone());
943 block
944 }
945 GuardResult::Timeout => unreachable!(),
946 },
947 PhantomData,
948 ))
949}
950
951#[cfg(feature = "mmap")]
956fn will_need_range(mmap: &Mmap, range: Range<usize>) {
957 #[cfg(unix)]
958 if let Some(page_size) = page_size()
959 && spans_multiple_pages(&range, page_size)
960 {
961 #[cfg(test)]
962 will_need_log::record(range.clone());
963 let _ = mmap.advise_range(memmap2::Advice::WillNeed, range.start, range.len());
964 }
965 #[cfg(not(unix))]
966 let _ = (mmap, range);
967}
968
969#[cfg(all(feature = "mmap", unix))]
971fn spans_multiple_pages(range: &Range<usize>, page_size: usize) -> bool {
972 debug_assert!(!range.is_empty());
973 range.start / page_size != (range.end - 1) / page_size
974}
975
976#[cfg(all(feature = "mmap", unix))]
979fn page_size() -> Option<usize> {
980 static PAGE_SIZE: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
981 *PAGE_SIZE.get_or_init(|| {
982 let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
984 usize::try_from(size).ok().filter(|&size| size > 0)
985 })
986}
987
988#[cfg(all(test, feature = "mmap", unix))]
990pub(crate) mod will_need_log {
991 use std::{cell::RefCell, ops::Range};
992
993 thread_local! {
994 static LOG: RefCell<Vec<Range<usize>>> = const { RefCell::new(Vec::new()) };
995 }
996
997 pub(crate) fn record(range: Range<usize>) {
998 LOG.with(|log| log.borrow_mut().push(range));
999 }
1000
1001 pub(crate) fn take() -> Vec<Range<usize>> {
1003 LOG.with(|log| std::mem::take(&mut *log.borrow_mut()))
1004 }
1005}
1006
1007#[cfg(feature = "mmap")]
1013fn get_raw_block_slice<'a>(
1014 mmap: &'a Mmap,
1015 block_offsets: &BlockOffsets,
1016 meta: &StaticSortedFileMetaData,
1017 block_index: u16,
1018 will_need: bool,
1019) -> Result<(u32, u32, &'a [u8])> {
1020 let range = block_range(block_offsets, meta, block_index)?;
1021 if will_need {
1022 will_need_range(mmap, range.clone());
1023 }
1024 let uncompressed_length = be::read_u32(&mmap[range.start..]);
1025 let checksum = be::read_u32(&mmap[range.start + 4..]);
1026 let block = &mmap[range.start + BLOCK_HEADER_SIZE..range.end];
1027 Ok((uncompressed_length, checksum, block))
1028}
1029
1030fn block_range(
1032 block_offsets: &BlockOffsets,
1033 meta: &StaticSortedFileMetaData,
1034 block_index: u16,
1035) -> Result<Range<usize>> {
1036 block_offsets
1037 .block_range(block_index)
1038 .with_context(|| format!("Corrupted file {:08}.sst", meta.sequence_number))
1039}
1040
1041fn pread_block<B: SharedBytes>(file: &File, range: Range<usize>) -> Result<(u32, u32, B)> {
1047 let bytes = B::read_filled(range.len(), |buf| pread_buf(file, buf, range.start as u64))?;
1048 let uncompressed_length = be::read_u32(&bytes);
1049 let checksum = be::read_u32(&bytes[4..]);
1050 let len = bytes.len();
1051 Ok((
1052 uncompressed_length,
1053 checksum,
1054 bytes.slice(BLOCK_HEADER_SIZE..len),
1055 ))
1056}
1057
1058enum RawBlock<'a> {
1061 #[cfg_attr(not(feature = "mmap"), allow(dead_code))]
1063 Borrowed(&'a [u8]),
1064 Owned(ArcBytes),
1066}
1067
1068impl RawBlock<'_> {
1069 fn into_arc_bytes(self) -> ArcBytes {
1071 match self {
1072 RawBlock::Borrowed(block) => ArcBytes::from(Arc::<[u8]>::from(block)),
1073 RawBlock::Owned(block) => block,
1074 }
1075 }
1076}
1077
1078impl Deref for RawBlock<'_> {
1079 type Target = [u8];
1080
1081 fn deref(&self) -> &[u8] {
1082 match self {
1083 RawBlock::Borrowed(block) => block,
1084 RawBlock::Owned(block) => block,
1085 }
1086 }
1087}
1088
1089fn get_raw_block<'a>(
1091 backing: &'a StaticSortedFileBacking,
1092 block_offsets: &BlockOffsets,
1093 meta: &StaticSortedFileMetaData,
1094 block_index: u16,
1095) -> Result<(u32, u32, RawBlock<'a>)> {
1096 match backing {
1097 #[cfg(feature = "mmap")]
1098 StaticSortedFileBacking::Mmap(mmap) => {
1099 let (uncompressed_length, checksum, block) =
1102 get_raw_block_slice(mmap, block_offsets, meta, block_index, true)?;
1103 Ok((uncompressed_length, checksum, RawBlock::Borrowed(block)))
1104 }
1105 StaticSortedFileBacking::File { file } => {
1106 let range = block_range(block_offsets, meta, block_index)?;
1107 let (uncompressed_length, checksum, block) = pread_block(file, range)?;
1108 Ok((uncompressed_length, checksum, RawBlock::Owned(block)))
1109 }
1110 }
1111}
1112
1113fn pread(file: &File, buf: &mut [u8], offset: u64) -> io::Result<()> {
1115 pread_buf(file, io::BorrowedBuf::from(buf).unfilled(), offset)
1116}
1117
1118fn pread_buf(file: &File, buf: io::BorrowedCursor<'_, u8>, offset: u64) -> io::Result<()> {
1121 let with_path = |err: io::Error| {
1123 io::Error::new(
1124 err.kind(),
1125 format!("{err} reading {}", file.path().display()),
1126 )
1127 };
1128 #[cfg(unix)]
1129 {
1130 use std::os::unix::fs::FileExt;
1131 file.file()
1132 .read_buf_exact_at(buf, offset)
1133 .map_err(with_path)
1134 }
1135 #[cfg(windows)]
1136 {
1137 use std::os::windows::fs::FileExt;
1138 file.file()
1139 .seek_read_buf_exact(buf, offset)
1140 .map_err(with_path)
1141 }
1142 #[cfg(target_os = "wasi")]
1143 {
1144 use std::os::wasi::fs::FileExt;
1146 let mut buf = buf;
1147 let mut offset = offset;
1148 while buf.capacity() > 0 {
1149 let written = buf.written();
1150 match file.file().read_buf_at(buf.reborrow(), offset) {
1151 Ok(()) => {}
1152 Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
1153 Err(err) => return Err(with_path(err)),
1154 }
1155 let read = buf.written() - written;
1156 if read == 0 {
1157 return Err(with_path(io::Error::new(
1158 io::ErrorKind::UnexpectedEof,
1159 "unexpected EOF",
1160 )));
1161 }
1162 offset += read as u64;
1163 }
1164 Ok(())
1165 }
1166}
1167
1168fn verify_checksum(
1170 meta: &StaticSortedFileMetaData,
1171 data: &[u8],
1172 expected: u32,
1173 block_index: u16,
1174) -> Result<()> {
1175 let actual = checksum_block(data);
1176 if actual != expected {
1177 bail!(
1178 "Cache corruption detected: checksum mismatch in block {} of {:08}.sst (expected \
1179 {:08x}, got {:08x})",
1180 block_index,
1181 meta.sequence_number,
1182 expected,
1183 actual
1184 );
1185 }
1186 Ok(())
1187}
1188
1189#[cfg(feature = "mmap")]
1192fn is_block_verified(block_index: u16, verified_blocks: &[AtomicU64]) -> bool {
1193 let word_idx = block_index as usize / u64::BITS as usize;
1194 let bit = 1u64 << (block_index as usize % u64::BITS as usize);
1195 verified_blocks
1196 .get(word_idx)
1197 .is_some_and(|word| word.load(AtomicOrdering::Relaxed) & bit != 0)
1198}
1199
1200#[cfg_attr(not(feature = "mmap"), allow(dead_code))]
1207fn verify_checksum_once(
1208 meta: &StaticSortedFileMetaData,
1209 data: &[u8],
1210 expected: u32,
1211 block_index: u16,
1212 verified_blocks: &[AtomicU64],
1213) -> Result<()> {
1214 let word_idx = block_index as usize / u64::BITS as usize;
1215 let bit = 1u64 << (block_index as usize % u64::BITS as usize);
1216 if verified_blocks[word_idx].load(AtomicOrdering::Relaxed) & bit != 0 {
1217 return Ok(());
1218 }
1219 verify_checksum(meta, data, expected, block_index)?;
1220 verified_blocks[word_idx].fetch_or(bit, AtomicOrdering::Relaxed);
1221 Ok(())
1222}
1223
1224fn read_block_lookup(
1226 backing: &StaticSortedFileBacking,
1227 block_offsets: &BlockOffsets,
1228 meta: &StaticSortedFileMetaData,
1229 block_index: u16,
1230 compression: Compression,
1231) -> Result<ArcBytes> {
1232 let (uncompressed_length, checksum, block) =
1233 get_raw_block(backing, block_offsets, meta, block_index)?;
1234 verify_checksum(meta, &block, checksum, block_index)?;
1235 if uncompressed_length == 0 {
1236 return match (backing, block) {
1237 #[cfg(feature = "mmap")]
1238 (StaticSortedFileBacking::Mmap(mmap), RawBlock::Borrowed(block)) => {
1239 Ok(unsafe { ArcBytes::from_mmap(mmap, block) })
1241 }
1242 (_, block) => Ok(block.into_arc_bytes()),
1243 };
1244 }
1245 ArcBytes::from_decompressed(compression, uncompressed_length, &block).with_context(|| {
1246 format!(
1247 "Failed to decompress block {} from {:08}.sst ({} bytes uncompressed)",
1248 block_index, meta.sequence_number, uncompressed_length
1249 )
1250 })
1251}
1252
1253fn get_raw_block_iter(
1256 file: &File,
1257 block_offsets: &BlockOffsets,
1258 meta: &StaticSortedFileMetaData,
1259 block_index: u16,
1260) -> Result<(u32, u32, RcBytes)> {
1261 let range = block_range(block_offsets, meta, block_index)?;
1262 pread_block(file, range)
1263}
1264
1265fn read_block_iter(
1267 file: &File,
1268 block_offsets: &BlockOffsets,
1269 meta: &StaticSortedFileMetaData,
1270 block_index: u16,
1271 compression: Compression,
1272) -> Result<RcBytes> {
1273 let (uncompressed_length, checksum, block) =
1274 get_raw_block_iter(file, block_offsets, meta, block_index)?;
1275 verify_checksum(meta, &block, checksum, block_index)?;
1276 if uncompressed_length == 0 {
1277 return Ok(block);
1278 }
1279 RcBytes::from_decompressed(compression, uncompressed_length, &block).with_context(|| {
1280 format!(
1281 "Failed to decompress block {} from {:08}.sst ({} bytes uncompressed)",
1282 block_index, meta.sequence_number, uncompressed_length
1283 )
1284 })
1285}
1286
1287fn handle_key_match_generic<B: SharedBytes>(
1289 meta: &StaticSortedFileMetaData,
1290 ty: u8,
1291 val: &[u8],
1292 compression: Compression,
1293 reader: impl ValueBlockCache<B>,
1294) -> Result<LookupValue<B>> {
1295 Ok(match ty {
1296 KEY_BLOCK_ENTRY_TYPE_SMALL => {
1297 let block = be::read_u16(val);
1298 let size = be::read_u16(&val[2..]) as usize;
1299 let position = be::read_u32(&val[4..]) as usize;
1300 let value = reader
1301 .get_or_read(meta, block, compression)?
1302 .slice(position..position + size);
1303 LookupValue::Slice { value }
1304 }
1305 KEY_BLOCK_ENTRY_TYPE_MEDIUM => {
1306 let block = be::read_u16(val);
1307 let value = reader.read_uncached(meta, block, compression)?;
1308 LookupValue::Slice { value }
1309 }
1310 KEY_BLOCK_ENTRY_TYPE_BLOB => {
1311 let sequence_number = be::read_u32(val);
1312 LookupValue::Blob { sequence_number }
1313 }
1314 KEY_BLOCK_ENTRY_TYPE_KEY_DELETED => LookupValue::KeyDeleted,
1315 ty if ty >= KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN => {
1317 let value = B::from_inline(val);
1318 LookupValue::KeyValueDeleted { value }
1319 }
1320 _ => {
1321 let value = B::from_inline(val);
1323 LookupValue::Slice { value }
1324 }
1325 })
1326}
1327
1328pub struct StaticSortedFileIter {
1330 file: File,
1338 block_offsets: BlockOffsets,
1339 meta: StaticSortedFileMetaData,
1341
1342 index_entries: RcBytes,
1345 num_index_entries: usize,
1347 index_pos: usize,
1349 current_key_block: CurrentKeyBlock,
1350 value_block_cache: Option<(u16, RcBytes)>,
1354 compression: Compression,
1355}
1356
1357enum CurrentKeyBlockKind {
1358 Variable { offsets: RcBytes },
1360 Fixed {
1362 value_type: Option<u8>,
1364 regions: FixedRegions,
1365 },
1366}
1367
1368impl CurrentKeyBlockKind {
1369 fn entry<'l>(
1374 &'l self,
1375 entries: &'l [u8],
1376 entry_count: u32,
1377 index: usize,
1378 hash_len: u8,
1379 ) -> Result<GetKeyEntryResult<'l>> {
1380 match self {
1381 CurrentKeyBlockKind::Variable { offsets } => {
1382 get_key_entry(offsets, entries, entry_count as usize, index, hash_len)
1383 }
1384 CurrentKeyBlockKind::Fixed {
1385 value_type,
1386 regions,
1387 } => get_fixed_key_entry(entries, index, *regions, *value_type),
1388 }
1389 }
1390}
1391
1392struct HashOrderEntry {
1395 hash: u64,
1396 entry_index: u32,
1397}
1398
1399struct CurrentKeyBlock {
1400 kind: CurrentKeyBlockKind,
1401 layout: KeyBlockLayout,
1403 entries: RcBytes,
1404 entry_count: u32,
1406 index: u32,
1409 hash_order: Option<Vec<HashOrderEntry>>,
1412}
1413
1414impl Iterator for StaticSortedFileIter {
1415 type Item = Result<LookupEntry>;
1416
1417 fn next(&mut self) -> Option<Self::Item> {
1418 self.next_internal().transpose()
1419 }
1420}
1421
1422impl StaticSortedFileIter {
1423 pub fn open(
1426 db_path: &Path,
1427 meta: StaticSortedFileMetaData,
1428 compression: Compression,
1429 ) -> Result<Self> {
1430 let filename = format!("{:08}.sst", meta.sequence_number);
1431 let path = db_path.join(&filename);
1432 let file = File::open(&path)?;
1433 let file_len: usize = file.metadata()?.len().try_into()?;
1434 let block_offsets = BlockOffsets::read(&file, &meta, file_len)?;
1435 let index_block = read_index_block(&file, &block_offsets, &meta)
1436 .with_context(|| format!("Unable to open static sorted file {filename}"))?;
1437 Self::new(file, block_offsets, index_block, meta, compression)
1438 .with_context(|| format!("Unable to open static sorted file {filename}"))
1439 }
1440
1441 fn new(
1443 file: File,
1444 block_offsets: BlockOffsets,
1445 index_block: RcBytes,
1446 meta: StaticSortedFileMetaData,
1447 compression: Compression,
1448 ) -> Result<Self> {
1449 let block = index_block;
1450 let block_len = block.len();
1451 ensure!(block_len >= 3, "index block too short");
1452 let index_entries = block.slice(1..block_len);
1453 let first_child = be::read_u16(&index_entries);
1454 let num_index_entries: usize = (index_entries.len() + INDEX_BLOCK_ENTRY_SIZE
1459 - size_of::<u16>())
1460 / INDEX_BLOCK_ENTRY_SIZE;
1461
1462 let current_key_block =
1463 Self::parse_key_block(&file, &block_offsets, &meta, first_child, compression)?;
1464 Ok(StaticSortedFileIter {
1465 file,
1466 block_offsets,
1467 meta,
1468 index_entries,
1469 num_index_entries,
1470 index_pos: 1,
1471 current_key_block,
1472 value_block_cache: None,
1473 compression,
1474 })
1475 }
1476
1477 fn parse_key_block(
1479 file: &File,
1480 block_offsets: &BlockOffsets,
1481 meta: &StaticSortedFileMetaData,
1482 block_index: u16,
1483 compression: Compression,
1484 ) -> Result<CurrentKeyBlock> {
1485 let block = read_block_iter(file, block_offsets, meta, block_index, compression)?;
1486 let data = &*block;
1487 ensure!(data.len() >= 4, "key block too short");
1488 let block_type = data[0];
1489 let entry_count = be::read_u24(&data[1..]);
1490 let block_len = block.len();
1491 let Some((layout, fixed)) = KeyBlockLayout::from_block_type(block_type) else {
1492 bail!("Invalid key block type: {block_type}");
1493 };
1494 let hash_len = layout.hash_len();
1495
1496 let (kind, entries) = if fixed {
1497 ensure!(data.len() >= 6, "fixed key block too short");
1498 let key_size = data[4] as usize;
1501 let FixedValueLayout {
1502 value_type,
1503 val_size,
1504 header_size,
1505 } = fixed_value_layout(data, data[5])?;
1506 let regions = FixedRegions::new(entry_count as usize, layout, key_size, val_size);
1507 let entries = block.slice(header_size..block_len);
1508 ensure!(
1509 entries.len() == regions.total_len(entry_count as usize),
1510 "fixed key block for {entry_count} entries is the wrong size"
1511 );
1512 (
1513 CurrentKeyBlockKind::Fixed {
1514 value_type,
1515 regions,
1516 },
1517 entries,
1518 )
1519 } else {
1520 let offset_table_begin = 4usize;
1521 let offset_table_end = 4 + (entry_count as usize) * key_block_table_stride(hash_len);
1522 ensure!(
1523 block_len >= offset_table_end,
1524 "key block too short for {entry_count} entries"
1525 );
1526 let offsets = block.clone().slice(offset_table_begin..offset_table_end);
1527 let entries = block.slice(offset_table_end..block_len);
1528 (CurrentKeyBlockKind::Variable { offsets }, entries)
1529 };
1530
1531 let hash_order = match layout {
1533 KeyBlockLayout::HashThenKey => None,
1534 KeyBlockLayout::KeyOnly => Some(hash_order_for_block(entry_count, |i| {
1535 kind.entry(&entries, entry_count, i, hash_len)
1536 })?),
1537 };
1538
1539 Ok(CurrentKeyBlock {
1540 kind,
1541 layout,
1542 entries,
1543 entry_count,
1544 index: 0,
1545 hash_order,
1546 })
1547 }
1548
1549 fn next_internal(&mut self) -> Result<Option<LookupEntry>> {
1551 loop {
1552 let kb = &mut self.current_key_block;
1553 if kb.index < kb.entry_count {
1554 let (precomputed_hash, index) = match &kb.hash_order {
1555 None => (None, kb.index as usize),
1556 Some(hash_order) => {
1557 let HashOrderEntry { hash, entry_index } = hash_order[kb.index as usize];
1558 (Some(hash), entry_index as usize)
1559 }
1560 };
1561 let GetKeyEntryResult { hash, key, ty, val } =
1562 kb.kind
1563 .entry(&kb.entries, kb.entry_count, index, kb.layout.hash_len())?;
1564 let full_hash = match precomputed_hash {
1565 Some(hash) => hash,
1566 None => be::read_u64(hash),
1567 };
1568 let value = if ty == KEY_BLOCK_ENTRY_TYPE_MEDIUM {
1569 let block = be::read_u16(val);
1570 let (uncompressed_size, checksum, block) =
1571 get_raw_block_iter(&self.file, &self.block_offsets, &self.meta, block)?;
1572 IterValue::Medium {
1573 uncompressed_size,
1574 checksum,
1575 block,
1576 }
1577 } else {
1578 handle_key_match_generic(
1579 &self.meta,
1580 ty,
1581 val,
1582 self.compression,
1583 RcBlockCacheReader {
1584 file: &self.file,
1585 block_offsets: &self.block_offsets,
1586 cache: &mut self.value_block_cache,
1587 },
1588 )?
1589 .into()
1590 };
1591 let entry = LookupEntry {
1592 hash: full_hash,
1593 key: unsafe { kb.entries.slice_from_subslice(key) },
1594 value,
1595 };
1596 kb.index += 1;
1597 return Ok(Some(entry));
1598 }
1599 if self.index_pos < self.num_index_entries {
1600 let base = self.index_pos * INDEX_BLOCK_ENTRY_SIZE;
1601 let block_index = be::read_u16(&self.index_entries[base..]);
1602 self.index_pos += 1;
1603 self.current_key_block = Self::parse_key_block(
1604 &self.file,
1605 &self.block_offsets,
1606 &self.meta,
1607 block_index,
1608 self.compression,
1609 )?;
1610 } else {
1611 return Ok(None);
1612 }
1613 }
1614 }
1615}
1616
1617struct GetKeyEntryResult<'l> {
1618 hash: &'l [u8],
1619 key: &'l [u8],
1620 ty: u8,
1621 val: &'l [u8],
1622}
1623
1624fn hash_order_for_block<'l>(
1627 entry_count: u32,
1628 get_entry: impl Fn(usize) -> Result<GetKeyEntryResult<'l>>,
1629) -> Result<Vec<HashOrderEntry>> {
1630 let mut order = Vec::with_capacity(entry_count as usize);
1631 for entry_index in 0..entry_count {
1632 let key = get_entry(entry_index as usize)?.key;
1633 order.push(HashOrderEntry {
1634 hash: crate::key::hash_key(&key),
1635 entry_index,
1636 });
1637 }
1638 order.sort_by_key(|entry| entry.hash);
1641 Ok(order)
1642}
1643
1644fn compare_hash_key<K: QueryKey>(
1647 layout: KeyBlockLayout,
1648 entry_hash: &[u8],
1649 entry_key: &[u8],
1650 full_hash: u64,
1651 query_key: &K,
1652) -> Ordering {
1653 match layout {
1654 KeyBlockLayout::KeyOnly => {
1655 debug_assert!(entry_hash.is_empty(), "KeyOnly entries carry no hash");
1656 query_key.cmp(entry_key)
1657 }
1658 KeyBlockLayout::HashThenKey => match full_hash.to_be_bytes()[..].cmp(entry_hash) {
1659 Ordering::Equal => query_key.cmp(entry_key),
1660 ord => ord,
1661 },
1662 }
1663}
1664
1665fn entry_matches_key<K: QueryKey>(
1667 layout: KeyBlockLayout,
1668 entry_hash: &[u8],
1669 entry_key: &[u8],
1670 full_hash: u64,
1671 query_key: &K,
1672) -> bool {
1673 match layout {
1674 KeyBlockLayout::KeyOnly => {
1675 debug_assert!(entry_hash.is_empty(), "KeyOnly entries carry no hash");
1676 query_key.eq(entry_key)
1677 }
1678 KeyBlockLayout::HashThenKey => {
1679 full_hash.to_be_bytes()[..] == *entry_hash && query_key.eq(entry_key)
1680 }
1681 }
1682}
1683
1684fn entry_val_size(ty: u8) -> Result<usize> {
1691 match ty {
1692 KEY_BLOCK_ENTRY_TYPE_SMALL => Ok(SMALL_VALUE_REF_SIZE),
1693 KEY_BLOCK_ENTRY_TYPE_MEDIUM => Ok(MEDIUM_VALUE_REF_SIZE),
1694 KEY_BLOCK_ENTRY_TYPE_BLOB => Ok(BLOB_VALUE_REF_SIZE),
1695 KEY_BLOCK_ENTRY_TYPE_KEY_DELETED => Ok(KEY_DELETED_REF_SIZE),
1696 ty if ty >= KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN => {
1698 let size = (ty - KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN) as usize;
1699 ensure!(
1700 size <= MAX_INLINE_VALUE_SIZE,
1701 "key-value tombstone type {ty} claims a {size} byte value, over the \
1702 {MAX_INLINE_VALUE_SIZE} byte maximum"
1703 );
1704 Ok(size)
1705 }
1706 ty if ty >= KEY_BLOCK_ENTRY_TYPE_INLINE_MIN => {
1707 let size = (ty - KEY_BLOCK_ENTRY_TYPE_INLINE_MIN) as usize;
1708 ensure!(
1709 size <= MAX_INLINE_VALUE_SIZE,
1710 "inline value type {ty} claims a {size} byte value, over the \
1711 {MAX_INLINE_VALUE_SIZE} byte maximum"
1712 );
1713 Ok(size)
1714 }
1715 _ => bail!("Invalid key block entry type: {ty}"),
1716 }
1717}
1718
1719#[inline(always)]
1725fn read_offset_entry(
1726 offsets: &[u8],
1727 index: usize,
1728 table_stride: usize,
1729 hash_len: u8,
1730) -> (u8, usize) {
1731 let base = index * table_stride + (hash_len as usize);
1733 let word = be::read_u32(&offsets[base..]);
1734 let ty = (word >> 24) as u8;
1735 let offset = (word & 0x00FF_FFFF) as usize;
1736 (ty, offset)
1737}
1738
1739fn get_key_entry<'l>(
1741 offsets: &'l [u8],
1742 entries: &'l [u8],
1743 entry_count: usize,
1744 index: usize,
1745 hash_len: u8,
1746) -> Result<GetKeyEntryResult<'l>> {
1747 let table_stride = key_block_table_stride(hash_len);
1748 let (ty, start) = read_offset_entry(offsets, index, table_stride, hash_len);
1749 let end = if index == entry_count - 1 {
1750 entries.len()
1751 } else {
1752 let (_, next_start) = read_offset_entry(offsets, index + 1, table_stride, hash_len);
1753 next_start
1754 };
1755 let hash = &offsets[index * table_stride..index * table_stride + hash_len as usize];
1757 let val_size = entry_val_size(ty)?;
1758 Ok(GetKeyEntryResult {
1759 hash,
1760 key: &entries[start..end - val_size],
1761 ty,
1762 val: &entries[end - val_size..end],
1763 })
1764}
1765
1766struct FixedValueLayout {
1772 value_type: Option<u8>,
1774 val_size: usize,
1776 header_size: usize,
1778}
1779
1780fn fixed_value_layout(block: &[u8], header_type: u8) -> Result<FixedValueLayout> {
1782 if header_type == FIXED_KEY_BLOCK_MIXED_VALUE_TYPE {
1783 ensure!(block.len() >= 7, "mixed-type fixed key block too short");
1786 let value_footprint = be::read_u8(&block[6..]) as usize;
1788 ensure!(
1789 value_footprint <= MAX_INLINE_VALUE_SIZE,
1790 "mixed-type fixed key block claims a {value_footprint} byte value footprint, over the \
1791 {MAX_INLINE_VALUE_SIZE} byte maximum"
1792 );
1793 Ok(FixedValueLayout {
1794 value_type: None,
1795 val_size: value_footprint + 1,
1797 header_size: 7,
1798 })
1799 } else {
1800 Ok(FixedValueLayout {
1801 value_type: Some(header_type),
1802 val_size: entry_val_size(header_type)?,
1803 header_size: 6,
1804 })
1805 }
1806}
1807
1808#[derive(Clone, Copy)]
1818pub struct FixedRegions {
1819 layout: KeyBlockLayout,
1821 pub search_stride: usize,
1823 pub tail_start: usize,
1825 pub tail_stride: usize,
1827 key_size: usize,
1828}
1829
1830impl FixedRegions {
1831 pub fn new(
1835 entry_count: usize,
1836 layout: KeyBlockLayout,
1837 key_size: usize,
1838 val_size: usize,
1839 ) -> Self {
1840 let (search_stride, tail_stride) = match layout {
1843 KeyBlockLayout::HashThenKey => (layout.hash_len() as usize, key_size + val_size),
1844 KeyBlockLayout::KeyOnly => (key_size, val_size),
1845 };
1846 Self {
1847 layout,
1848 search_stride,
1849 tail_start: entry_count * search_stride,
1850 tail_stride,
1851 key_size,
1852 }
1853 }
1854
1855 pub fn tail_key_size(&self) -> usize {
1858 match self.layout {
1859 KeyBlockLayout::HashThenKey => self.key_size,
1860 KeyBlockLayout::KeyOnly => 0,
1861 }
1862 }
1863
1864 pub fn total_len(&self, entry_count: usize) -> usize {
1866 self.tail_start + entry_count * self.tail_stride
1867 }
1868}
1869
1870fn get_fixed_key_entry<'l>(
1871 entries: &'l [u8],
1872 index: usize,
1873 regions: FixedRegions,
1874 value_type: Option<u8>,
1875) -> Result<GetKeyEntryResult<'l>> {
1876 let FixedRegions {
1877 layout,
1878 search_stride,
1879 tail_start,
1880 tail_stride,
1881 key_size,
1882 } = regions;
1883 let search = index * search_stride;
1887 let tail = tail_start + index * tail_stride;
1888 let (hash, key, tail_rest) = match layout {
1889 KeyBlockLayout::HashThenKey => (
1890 &entries[search..search + search_stride],
1891 &entries[tail..tail + key_size],
1892 tail + key_size,
1893 ),
1894 KeyBlockLayout::KeyOnly => (&entries[..0], &entries[search..search + key_size], tail),
1895 };
1896 let (ty, val_start) = match value_type {
1898 Some(ty) => (ty, tail_rest),
1899 None => (be::read_u8(&entries[tail_rest..]), tail_rest + 1),
1900 };
1901 Ok(GetKeyEntryResult {
1902 hash,
1903 key,
1904 ty,
1905 val: &entries[val_start..tail + tail_stride],
1906 })
1907}
1908
1909#[cfg(test)]
1910mod tests {
1911 use super::*;
1912
1913 #[test]
1917 fn block_type_round_trips() {
1918 for layout in [KeyBlockLayout::HashThenKey, KeyBlockLayout::KeyOnly] {
1919 for fixed in [false, true] {
1920 let byte = layout.block_type(fixed);
1921 assert_eq!(
1922 KeyBlockLayout::from_block_type(byte),
1923 Some((layout, fixed)),
1924 "{layout:?} (fixed={fixed}) encoded as {byte} did not round-trip"
1925 );
1926 }
1927 }
1928 }
1929
1930 #[test]
1933 fn block_types_are_distinct() {
1934 let mut seen = vec![BLOCK_TYPE_INDEX];
1935 for layout in [KeyBlockLayout::HashThenKey, KeyBlockLayout::KeyOnly] {
1936 for fixed in [false, true] {
1937 let byte = layout.block_type(fixed);
1938 assert!(!seen.contains(&byte), "block type {byte} is used twice");
1939 seen.push(byte);
1940 }
1941 }
1942 assert!(KeyBlockLayout::from_block_type(BLOCK_TYPE_INDEX).is_none());
1943 }
1944
1945 #[test]
1948 fn hash_len_matches_layout() {
1949 assert_eq!(
1950 KeyBlockLayout::HashThenKey.hash_len() as usize,
1951 size_of::<u64>()
1952 );
1953 assert_eq!(KeyBlockLayout::KeyOnly.hash_len(), 0);
1954 }
1955
1956 #[cfg(all(feature = "mmap", unix))]
1959 #[test]
1960 fn spans_multiple_pages_detects_page_crossings() {
1961 for page in [4096, 16384] {
1962 assert!(!spans_multiple_pages(&(0..page), page), "one full page");
1963 assert!(
1964 !spans_multiple_pages(&(page..page + 100), page),
1965 "inside a page"
1966 );
1967 assert!(spans_multiple_pages(&(0..page + 1), page), "one byte over");
1968 assert!(
1969 spans_multiple_pages(&(page - 8..page + 8), page),
1970 "straddles a boundary"
1971 );
1972 assert!(
1973 spans_multiple_pages(&(page / 2..3 * page), page),
1974 "several pages"
1975 );
1976 }
1977 assert!(spans_multiple_pages(&(0..5120), 4096));
1979 assert!(!spans_multiple_pages(&(0..5120), 16384));
1980 }
1981
1982 fn read_offsets(ends: &[u32], data_len: usize) -> Result<BlockOffsets> {
1985 let mut bytes = vec![0u8; data_len];
1986 for end in ends {
1987 bytes.extend_from_slice(&end.to_be_bytes());
1988 }
1989 read_offsets_from(&bytes, ends.len())
1990 }
1991
1992 fn read_offsets_from(bytes: &[u8], block_count: usize) -> Result<BlockOffsets> {
1994 let dir = tempfile::tempdir()?;
1995 let path = dir.path().join("00000007.sst");
1996 fs_err::write(&path, bytes)?;
1997 let file = File::open(&path)?;
1998 let meta = StaticSortedFileMetaData {
1999 sequence_number: 7,
2000 block_count: block_count.try_into()?,
2001 };
2002 BlockOffsets::read(&file, &meta, bytes.len())
2003 }
2004
2005 #[test]
2006 fn block_offsets_ranges_use_previous_end_as_start() -> Result<()> {
2007 let offsets = read_offsets(&[8, 300, 70_000], 70_000)?;
2010 assert_eq!(offsets.block_range(0)?, 0..8);
2011 assert_eq!(offsets.block_range(1)?, 8..300);
2012 assert_eq!(offsets.block_range(2)?, 300..70_000);
2013 assert!(offsets.block_range(3).is_err());
2014 Ok(())
2015 }
2016
2017 #[test]
2018 fn block_offsets_reject_file_shorter_than_table() {
2019 let err = read_offsets_from(&[0; 8], 3).unwrap_err();
2020 assert!(err.to_string().contains("too short"), "{err:#}");
2021 }
2022
2023 #[test]
2024 fn block_offsets_reject_block_shorter_than_header() {
2025 let err = read_offsets(&[12, 16, 40], 40).unwrap_err();
2026 assert!(err.to_string().contains("block 1"), "{err:#}");
2027 }
2028
2029 #[test]
2030 fn block_offsets_reject_non_monotonic_offsets() {
2031 let err = read_offsets(&[20, 12, 40], 40).unwrap_err();
2032 assert!(err.to_string().contains("block 1"), "{err:#}");
2033 }
2034
2035 #[test]
2037 fn block_offsets_read_error_names_the_file() -> Result<()> {
2038 let dir = tempfile::tempdir()?;
2039 let path = dir.path().join("00000007.sst");
2040 fs_err::write(&path, [0u8; 8])?;
2041 let file = File::open(&path)?;
2042 let meta = StaticSortedFileMetaData {
2043 sequence_number: 7,
2044 block_count: 1,
2045 };
2046 let err = BlockOffsets::read(&file, &meta, 16).unwrap_err();
2048 let message = format!("{err:#}");
2049 assert!(message.contains(&*path.to_string_lossy()), "{message}");
2050 Ok(())
2051 }
2052
2053 #[test]
2054 fn block_offsets_reject_blocks_past_the_table() {
2055 let err = read_offsets(&[12, 40, 60], 52).unwrap_err();
2056 assert!(err.to_string().contains("past"), "{err:#}");
2057 }
2058}