mod iter;
#[cfg(test)]
mod iter_test;
pub use iter::Iter;
use super::block::{
Block, Decodable, Decoder, Encodable, Encoder, ParsedItem, TRAILER_START_MARKER, Trailer,
decoder::read_leb128, hash_index::Reader as HashIndexReader,
};
use crate::io::Cursor;
#[cfg(not(feature = "std"))]
use crate::io::VarintWriter;
use crate::io::WriteBytesExt;
use crate::io::{LittleEndian, ReadBytesExt};
use crate::key::InternalKey;
use crate::table::block::hash_index::{MARKER_CONFLICT, MARKER_FREE};
use crate::table::util::{SliceIndexes, compare_prefixed_slice};
use crate::{InternalValue, SeqNo, Slice, ValueType};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use varint_rs::VarintWriter;
impl Decodable<DataBlockParsedItem> for InternalValue {
fn parse_restart_key<'a>(
reader: &mut Cursor<&[u8]>,
offset: usize,
data: &'a [u8],
entries_end: usize,
) -> Option<(&'a [u8], SeqNo)> {
let buf: &[u8] = reader.get_ref();
let mut pos = usize::try_from(reader.position()).ok()?;
let value_type = *buf.get(pos)?;
pos += 1;
if value_type == TRAILER_START_MARKER {
return None;
}
let (seqno, np) = read_leb128!(buf, pos);
pos = np;
let (key_len_raw, np) = read_leb128!(buf, pos);
pos = np;
let key_len = usize::try_from(key_len_raw)
.ok()
.filter(|&k| u16::try_from(k).is_ok())?;
let key_start = offset.checked_add(pos)?;
let key_end = key_start.checked_add(key_len)?;
if key_end > entries_end {
return None;
}
pos = pos.checked_add(key_len)?;
let key = data.get(key_start..key_end)?;
reader.set_position(pos as u64);
Some((key, seqno))
}
fn parse_full(
reader: &mut Cursor<&[u8]>,
offset: usize,
entries_end: usize,
) -> Option<DataBlockParsedItem> {
let buf: &[u8] = reader.get_ref();
let mut pos = usize::try_from(reader.position()).ok()?;
let value_type = *buf.get(pos)?;
pos += 1;
if value_type == TRAILER_START_MARKER {
return None;
}
let value_type = if offset == 0 {
value_type & !SEQNO_DELTAS
} else {
value_type
};
let value_type = ValueType::try_from(value_type).ok()?;
let (seqno, np) = read_leb128!(buf, pos);
pos = np;
let (key_len_raw, np) = read_leb128!(buf, pos);
pos = np;
let key_len = usize::try_from(key_len_raw)
.ok()
.filter(|&k| u16::try_from(k).is_ok())?;
let key_start = offset.checked_add(pos)?;
if key_start > entries_end {
return None;
}
let key_end = key_start.checked_add(key_len)?;
if key_end > entries_end {
return None;
}
pos = pos.checked_add(key_len)?;
let is_value = !value_type.is_tombstone();
let val_len: usize = if is_value {
let (val_len_raw, np) = read_leb128!(buf, pos);
pos = np;
usize::try_from(val_len_raw)
.ok()
.filter(|&v| u32::try_from(v).is_ok())?
} else {
0
};
let val_offset = offset.checked_add(pos)?;
if val_offset > entries_end {
return None;
}
let val_end = val_offset.checked_add(val_len)?;
if val_end > entries_end {
return None;
}
pos = pos.checked_add(val_len)?;
reader.set_position(pos as u64);
Some(if is_value {
DataBlockParsedItem {
value_type,
seqno,
prefix: None,
key: SliceIndexes(key_start, key_end),
value: Some(SliceIndexes(val_offset, val_end)),
}
} else {
DataBlockParsedItem {
value_type,
seqno,
prefix: None,
key: SliceIndexes(key_start, key_end),
value: None, }
})
}
fn parse_truncated(
reader: &mut Cursor<&[u8]>,
offset: usize,
base_key_offset: usize,
base_key_end: usize,
base_seqno: Option<SeqNo>,
entries_end: usize,
) -> Option<DataBlockParsedItem> {
let buf: &[u8] = reader.get_ref();
let mut pos = usize::try_from(reader.position()).ok()?;
let value_type = *buf.get(pos)?;
pos += 1;
if value_type == TRAILER_START_MARKER {
return None;
}
let value_type = ValueType::try_from(value_type).ok()?;
let (stored, np) = read_leb128!(buf, pos);
pos = np;
let seqno = match base_seqno {
Some(head) => seqno_from_delta(stored, head),
None => stored,
};
let (shared_raw, np) = read_leb128!(buf, pos);
pos = np;
let shared_prefix_len = usize::try_from(shared_raw)
.ok()
.filter(|&k| u16::try_from(k).is_ok())?;
let (rest_raw, np) = read_leb128!(buf, pos);
pos = np;
let rest_key_len = usize::try_from(rest_raw)
.ok()
.filter(|&k| u16::try_from(k).is_ok())?;
if base_key_end < base_key_offset || base_key_end > offset {
return None;
}
let prefix_end = base_key_offset.checked_add(shared_prefix_len)?;
if prefix_end > base_key_end {
return None;
}
let key_offset = offset.checked_add(pos)?;
if key_offset > entries_end {
return None;
}
let key_end = key_offset.checked_add(rest_key_len)?;
if key_end > entries_end {
return None;
}
pos = pos.checked_add(rest_key_len)?;
let is_value = !value_type.is_tombstone();
let val_len: usize = if is_value {
let (val_len_raw, np) = read_leb128!(buf, pos);
pos = np;
usize::try_from(val_len_raw)
.ok()
.filter(|&v| u32::try_from(v).is_ok())?
} else {
0
};
let val_offset = offset.checked_add(pos)?;
if val_offset > entries_end {
return None;
}
let val_end = val_offset.checked_add(val_len)?;
if val_end > entries_end {
return None;
}
pos = pos.checked_add(val_len)?;
reader.set_position(pos as u64);
Some(if is_value {
DataBlockParsedItem {
value_type,
seqno,
prefix: Some(SliceIndexes(base_key_offset, prefix_end)),
key: SliceIndexes(key_offset, key_end),
value: Some(SliceIndexes(val_offset, val_end)),
}
} else {
DataBlockParsedItem {
value_type,
seqno,
prefix: Some(SliceIndexes(base_key_offset, prefix_end)),
key: SliceIndexes(key_offset, key_end),
value: None,
}
})
}
fn seqno_deltas(entries: &[u8]) -> bool {
entries
.first()
.is_some_and(|&byte| byte & SEQNO_DELTAS != 0)
}
}
const SEQNO_DELTAS: u8 = 0x80;
#[derive(Clone, Copy, Debug, Default)]
pub struct SeqnoCoding {
head: SeqNo,
deltas: bool,
mark: bool,
}
impl SeqnoCoding {
fn new(deltas: bool) -> Self {
Self {
head: 0,
deltas,
mark: deltas,
}
}
}
#[inline]
fn varint_len(value: u64) -> u32 {
let bits = 64 - (value | 1).leading_zeros();
(9 * bits + 64) >> 6
}
fn block_uses_deltas(items: &[InternalValue], restart_interval: u8) -> bool {
let (mut full, mut delta) = (0u64, 0u64);
for interval in items.chunks(usize::from(restart_interval)) {
let Some((head, rest)) = interval.split_first() else {
continue;
};
for item in rest {
full += u64::from(varint_len(item.key.seqno));
delta += u64::from(varint_len(seqno_delta(item.key.seqno, head.key.seqno)));
}
}
delta < full
}
#[inline]
fn seqno_delta(seqno: SeqNo, head_seqno: SeqNo) -> u64 {
#[expect(
clippy::cast_possible_wrap,
reason = "the wrapping difference is reinterpreted as signed on purpose"
)]
let delta = seqno.wrapping_sub(head_seqno) as i64;
#[expect(
clippy::cast_sign_loss,
reason = "zigzag maps the signed delta onto u64 bit for bit"
)]
let zigzag = ((delta << 1) ^ (delta >> 63)) as u64;
zigzag
}
#[inline]
fn seqno_from_delta(zigzag: u64, head_seqno: SeqNo) -> SeqNo {
#[expect(
clippy::cast_possible_wrap,
reason = "zigzag decode reinterprets the low bit as the sign"
)]
let delta = ((zigzag >> 1) as i64) ^ -((zigzag & 1) as i64);
#[expect(
clippy::cast_sign_loss,
reason = "the signed delta is applied as a wrapping difference"
)]
head_seqno.wrapping_add(delta as u64)
}
impl Encodable<SeqnoCoding> for InternalValue {
fn encode_full_into<W: crate::io::Write>(
&self,
writer: &mut W,
coding: &mut SeqnoCoding,
) -> crate::Result<()> {
let flag = if core::mem::take(&mut coding.mark) {
SEQNO_DELTAS
} else {
0
};
writer.write_u8(u8::from(self.key.value_type) | flag)?; writer.write_u64_varint(self.key.seqno)?; coding.head = self.key.seqno;
#[expect(clippy::cast_possible_truncation, reason = "keys are u16 length max")]
writer.write_u16_varint(self.key.user_key.len() as u16)?; writer.write_all(&self.key.user_key)?;
if !self.is_tombstone() {
#[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
writer.write_u32_varint(self.value.len() as u32)?; writer.write_all(&self.value)?; }
Ok(())
}
fn encode_truncated_into<W: crate::io::Write>(
&self,
writer: &mut W,
coding: &mut SeqnoCoding,
shared_len: usize,
) -> crate::Result<()> {
writer.write_u8(u8::from(self.key.value_type))?; let seqno = if coding.deltas {
seqno_delta(self.key.seqno, coding.head)
} else {
self.key.seqno
};
writer.write_u64_varint(seqno)?;
#[expect(clippy::cast_possible_truncation, reason = "keys are u16 length max")]
writer.write_u16_varint(shared_len as u16)?;
let rest_len = self.key().len() - shared_len;
#[expect(clippy::cast_possible_truncation, reason = "keys are u16 length max")]
writer.write_u16_varint(rest_len as u16)?;
#[expect(
clippy::expect_used,
reason = "the shared len should not be greater than key length"
)]
let truncated_user_key = self
.key
.user_key
.get(shared_len..)
.expect("should be in bounds");
writer.write_all(truncated_user_key)?;
if !self.is_tombstone() {
#[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
writer.write_u32_varint(self.value.len() as u32)?; writer.write_all(&self.value)?; }
Ok(())
}
fn key(&self) -> &[u8] {
&self.key.user_key
}
}
#[derive(Debug)]
pub struct DataBlockParsedItem {
pub value_type: ValueType,
pub seqno: SeqNo,
pub prefix: Option<SliceIndexes>,
pub key: SliceIndexes,
pub value: Option<SliceIndexes>,
}
impl ParsedItem<InternalValue> for DataBlockParsedItem {
fn compare_key(
&self,
needle: &[u8],
bytes: &[u8],
cmp: &dyn crate::comparator::UserComparator,
) -> core::cmp::Ordering {
if let Some(prefix) = &self.prefix {
let prefix = unsafe { bytes.get_unchecked(prefix.0..prefix.1) };
let rest_key = unsafe { bytes.get_unchecked(self.key.0..self.key.1) };
compare_prefixed_slice(prefix, rest_key, needle, cmp)
} else {
let key = unsafe { bytes.get_unchecked(self.key.0..self.key.1) };
cmp.compare(key, needle)
}
}
fn seqno(&self) -> SeqNo {
self.seqno
}
fn key_offset(&self) -> usize {
self.key.0
}
fn key_end_offset(&self) -> usize {
self.key.1
}
fn materialize(&self, bytes: &Slice) -> InternalValue {
#[expect(clippy::indexing_slicing)]
let key = if let Some(prefix) = &self.prefix {
let prefix_key = &bytes[prefix.0..prefix.1];
let rest_key = &bytes[self.key.0..self.key.1];
Slice::fused(prefix_key, rest_key)
} else {
bytes.slice(self.key.0..self.key.1)
};
let key = InternalKey::new(key, self.seqno, self.value_type);
let value = self
.value
.as_ref()
.map_or_else(Slice::empty, |v| bytes.slice(v.0..v.1));
InternalValue { key, value }
}
}
#[derive(Clone)]
pub struct DataBlock {
pub inner: Block,
}
fn seek_data_block(
decoder: &mut Decoder<'_, InternalValue, DataBlockParsedItem>,
needle: &[u8],
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
) -> bool {
if comparator.is_lexicographic() {
decoder.seek(
|head_key, head_seqno| match head_key.cmp(needle) {
core::cmp::Ordering::Less => true,
core::cmp::Ordering::Equal => head_seqno >= seqno,
core::cmp::Ordering::Greater => false,
},
false,
)
} else {
decoder.seek(
|head_key, head_seqno| match comparator.compare(head_key, needle) {
core::cmp::Ordering::Less => true,
core::cmp::Ordering::Equal => head_seqno >= seqno,
core::cmp::Ordering::Greater => false,
},
false,
)
}
}
impl DataBlock {
#[must_use]
pub fn new(inner: Block) -> Self {
Self { inner }
}
#[cfg(feature = "columnar")]
pub(crate) fn column_batch_entries(
pages: impl IntoIterator<Item = crate::table::columnar::ColumnBatch>,
gathered: &mut usize,
) -> crate::Result<Vec<InternalValue>> {
let mut entries = Vec::new();
for batch in pages {
let rows = crate::table::columnar::column_batch_into_entries(batch, gathered)?;
if rows.is_empty() {
return Err(crate::Error::InvalidHeader(
"columnar: empty reconstructed data block",
));
}
if entries.is_empty() {
entries = rows;
} else {
entries.extend(rows);
}
}
if entries.is_empty() {
return Err(crate::Error::InvalidHeader(
"columnar: empty reconstructed data block",
));
}
Ok(entries)
}
#[cfg(feature = "columnar")]
pub(crate) fn column_batch_entries_masked(
pages: impl IntoIterator<Item = crate::table::columnar::ColumnBatch>,
deletes: &crate::table::delete_bitmap::DeleteBitmap,
block_start_row: u32,
gathered: &mut usize,
) -> crate::Result<Option<Vec<InternalValue>>> {
let entries = Self::column_batch_entries(pages, gathered)?;
let mut kept = Vec::with_capacity(entries.len());
for (index, entry) in entries.into_iter().enumerate() {
let offset = u32::try_from(index).map_err(|_| {
crate::Error::InvalidHeader("columnar: block row index exceeds u32::MAX")
})?;
let Some(pos) = block_start_row.checked_add(offset) else {
return Err(crate::Error::InvalidHeader(
"columnar: row position exceeds u32::MAX",
));
};
if !deletes.contains(pos) {
kept.push(entry);
}
}
if kept.is_empty() {
return Ok(None);
}
Ok(Some(kept))
}
#[cfg(feature = "columnar")]
pub(crate) fn from_column_batch(
pages: impl IntoIterator<Item = crate::table::columnar::ColumnBatch>,
restart_interval: u8,
gathered: &mut usize,
) -> crate::Result<Self> {
let entries = Self::column_batch_entries(pages, gathered)?;
Self::encode_entries_to_block(&entries, restart_interval)
}
#[cfg(feature = "columnar")]
pub(crate) fn columnar_point_block(
pages: Vec<crate::table::columnar::RowPageColumns<'_>>,
runs: &[core::ops::Range<u32>],
needle: &[u8],
restart_interval: u8,
deletes: Option<(&crate::table::delete_bitmap::DeleteBitmap, u32)>,
gathered: &mut usize,
budget: &mut crate::table::columnar::DecodeBudget,
) -> crate::Result<Option<Self>> {
let overflow = || crate::Error::InvalidHeader("columnar: row position exceeds u32::MAX");
if pages.len() != runs.len() {
return Err(crate::Error::InvalidHeader(
"columnar: row pages read differ from the pages the key search hit",
));
}
let mut entries = Vec::new();
let mut offset = 0u32;
for (page, run) in pages.into_iter().zip(runs) {
let rows = page.rows;
let page_deletes = match deletes {
Some((bitmap, start)) => {
Some((bitmap, start.checked_add(offset).ok_or_else(overflow)?))
}
None => None,
};
crate::table::columnar::page_match_entries(
page,
run.clone(),
needle,
page_deletes,
gathered,
budget,
&mut entries,
)?;
offset = offset.checked_add(rows).ok_or_else(overflow)?;
}
if entries.is_empty() {
return Ok(None);
}
Self::encode_entries_to_block(&entries, restart_interval).map(Some)
}
#[cfg(feature = "columnar")]
pub(crate) fn from_column_batch_masked(
pages: impl IntoIterator<Item = crate::table::columnar::ColumnBatch>,
restart_interval: u8,
deletes: &crate::table::delete_bitmap::DeleteBitmap,
block_start_row: u32,
gathered: &mut usize,
) -> crate::Result<Option<Self>> {
let kept = Self::column_batch_entries_masked(pages, deletes, block_start_row, gathered)?;
kept.map(|kept| Self::encode_entries_to_block(&kept, restart_interval))
.transpose()
}
#[cfg(feature = "columnar")]
fn encode_entries_to_block(
entries: &[InternalValue],
restart_interval: u8,
) -> crate::Result<Self> {
let mut buf = Vec::new();
Self::encode_into(&mut buf, entries, restart_interval, 0.0)?;
let len = u32::try_from(buf.len())
.map_err(|_| crate::Error::InvalidHeader("columnar: re-encoded block exceeds u32"))?;
let header = crate::table::block::Header {
block_type: crate::table::block::BlockType::Data,
block_flags: 0,
stored_checksum: crate::checksum::Checksum::from_raw(crate::hash::hash128(&buf)),
data_length: len,
uncompressed_length: len,
};
Self::from_loaded(
crate::table::block::Block {
header,
data: crate::Slice::from(buf),
},
false,
)
}
pub(crate) fn from_loaded(block: Block, has_kv_footer: bool) -> crate::Result<Self> {
use crate::table::block::kv_checksum;
if !has_kv_footer {
return Ok(Self::new(block));
}
let array_start = kv_checksum::split_inner(&block.data)?.len();
let mut header = block.header;
#[expect(
clippy::cast_possible_truncation,
reason = "array_start <= original uncompressed_length, which is u32"
)]
{
header.uncompressed_length = array_start as u32;
}
header.block_flags &= !crate::table::block::header::block_flags::KV_CHECKSUM_FOOTER;
Ok(Self::new(Block {
header,
data: block.data.slice(..array_start),
}))
}
#[must_use]
pub fn as_slice(&self) -> &Slice {
&self.inner.data
}
#[must_use]
pub fn get_hash_index_reader(&self) -> Option<HashIndexReader<'_>> {
use core::mem::size_of;
let trailer = Trailer::new(&self.inner);
let offset = size_of::<u8>() + size_of::<u8>() + size_of::<u32>() + size_of::<u32>();
let mut reader = unwrap!(trailer.as_slice().get(offset..));
let hash_index_len = unwrap!(reader.read_u32::<LittleEndian>());
let hash_index_offset = unwrap!(reader.read_u32::<LittleEndian>());
if hash_index_len == 0 {
debug_assert_eq!(
0, hash_index_offset,
"hash index offset should be 0 if its length is 0"
);
None
} else {
Some(HashIndexReader::new(
&self.inner.data,
hash_index_offset,
hash_index_len,
))
}
}
#[cfg(test)]
pub(crate) fn hash_index_span(&self) -> Option<(usize, usize)> {
use core::mem::size_of;
let trailer = Trailer::new(&self.inner);
let offset = size_of::<u8>() + size_of::<u8>() + size_of::<u32>() + size_of::<u32>();
let mut reader = trailer.as_slice().get(offset..)?;
let hash_index_len = reader.read_u32::<LittleEndian>().ok()?;
let hash_index_offset = reader.read_u32::<LittleEndian>().ok()?;
if hash_index_len == 0 {
return None;
}
Some((hash_index_offset as usize, hash_index_len as usize))
}
#[must_use]
pub fn hash_bucket_count(&self) -> Option<usize> {
self.get_hash_index_reader()
.map(|reader| reader.bucket_count())
}
pub fn point_read(
&self,
needle: &[u8],
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
) -> crate::Result<Option<InternalValue>> {
self.point_read_hashed(needle, None, seqno, comparator)
}
pub(crate) fn point_read_hashed(
&self,
needle: &[u8],
key_hash: Option<u64>,
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
) -> crate::Result<Option<InternalValue>> {
self.point_read_with(needle, key_hash, seqno, comparator, None, |item, bytes| {
item.materialize(bytes)
})
}
pub fn point_read_at_slot(
&self,
slot: u64,
is_entry: bool,
needle: &[u8],
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
) -> crate::Result<Option<InternalValue>> {
self.point_read_with(
needle,
None,
seqno,
comparator,
Some((slot, is_entry)),
DataBlockParsedItem::materialize,
)
}
pub fn point_read_value(
&self,
needle: &[u8],
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
) -> crate::Result<Option<(ValueType, SeqNo, Slice)>> {
self.point_read_value_hashed(needle, None, seqno, comparator)
}
pub(crate) fn point_read_value_hashed(
&self,
needle: &[u8],
key_hash: Option<u64>,
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
) -> crate::Result<Option<(ValueType, SeqNo, Slice)>> {
self.point_read_with(needle, key_hash, seqno, comparator, None, |item, bytes| {
let value = item
.value
.as_ref()
.map_or_else(Slice::empty, |v| bytes.slice(v.0..v.1));
(item.value_type, item.seqno, value)
})
}
pub fn point_read_value_at_slot(
&self,
slot: u64,
is_entry: bool,
needle: &[u8],
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
) -> crate::Result<Option<(ValueType, SeqNo, Slice)>> {
self.point_read_with(
needle,
None,
seqno,
comparator,
Some((slot, is_entry)),
|item, bytes| {
let value = item
.value
.as_ref()
.map_or_else(Slice::empty, |v| bytes.slice(v.0..v.1));
(item.value_type, item.seqno, value)
},
)
}
#[inline]
fn point_read_with<R>(
&self,
needle: &[u8],
key_hash: Option<u64>,
seqno: SeqNo,
comparator: &crate::comparator::SharedComparator,
slot_hint: Option<(u64, bool)>,
extract: impl FnOnce(&DataBlockParsedItem, &Slice) -> R,
) -> crate::Result<Option<R>> {
use crate::table::block::BlockType;
if !matches!(
self.inner.header.block_type,
BlockType::Data | BlockType::Meta
) {
return Err(crate::Error::InvalidTag((
"BlockType",
self.inner.header.block_type.into(),
)));
}
let mut decoder = Decoder::<InternalValue, DataBlockParsedItem>::try_new(&self.inner)?;
let positioned = if let Some((slot, is_entry)) = slot_hint {
let restart_idx = if is_entry {
let ri = u64::from(decoder.restart_interval());
if ri == 0 { slot } else { slot / ri }
} else {
slot
};
let restart_idx = usize::try_from(restart_idx).unwrap_or(usize::MAX);
match decoder.cached_binary_index_reader() {
Some(binary_index_reader) if restart_idx < binary_index_reader.len() => {
let offset: usize = binary_index_reader.get(restart_idx);
decoder.set_lo_offset(offset);
true
}
_ => seek_data_block(&mut decoder, needle, seqno, comparator),
}
} else if let Some(hash_index_reader) = decoder.cached_hash_index_reader() {
debug_assert!(
key_hash.is_none_or(|hash| hash == crate::hash::hash64(needle)),
"the hash of the needle"
);
match hash_index_reader.get(key_hash.unwrap_or_else(|| crate::hash::hash64(needle))) {
MARKER_FREE => return Ok(None),
MARKER_CONFLICT => seek_data_block(&mut decoder, needle, seqno, comparator),
idx => match decoder.cached_binary_index_reader() {
Some(binary_index_reader) => {
let offset: usize = binary_index_reader.get(usize::from(idx));
decoder.set_lo_offset(offset);
true
}
None => seek_data_block(&mut decoder, needle, seqno, comparator),
},
}
} else {
seek_data_block(&mut decoder, needle, seqno, comparator)
};
if !positioned {
return Ok(None);
}
for item in decoder.by_ref() {
match item.compare_key(needle, &self.inner.data, comparator.as_ref()) {
core::cmp::Ordering::Greater => {
return Ok(None);
}
core::cmp::Ordering::Equal => {
}
core::cmp::Ordering::Less => {
continue;
}
}
if item.seqno >= seqno {
continue;
}
return Ok(Some(extract(&item, &self.inner.data)));
}
Ok(None)
}
pub fn try_iter(
&self,
comparator: crate::comparator::SharedComparator,
) -> crate::Result<Iter<'_>> {
use crate::table::block::BlockType;
if !matches!(
self.inner.header.block_type,
BlockType::Data | BlockType::Meta
) {
return Err(crate::Error::InvalidTag((
"BlockType",
self.inner.header.block_type.into(),
)));
}
Ok(Iter::new(
&self.inner.data,
Decoder::<InternalValue, DataBlockParsedItem>::try_new(&self.inner)?,
comparator,
))
}
#[must_use]
pub fn iter(&self, comparator: crate::comparator::SharedComparator) -> Iter<'_> {
Iter::new(
&self.inner.data,
Decoder::<InternalValue, DataBlockParsedItem>::new(&self.inner),
comparator,
)
}
#[cfg(feature = "std")]
pub(crate) fn try_for_each_entry(
&self,
comparator: crate::comparator::SharedComparator,
mut f: impl FnMut(InternalValue) -> crate::Result<()>,
) -> crate::Result<()> {
for item in self.try_iter(comparator)? {
f(item.materialize(&self.inner.data))?;
}
Ok(())
}
#[cfg(feature = "std")]
pub(crate) fn first_user_key(
&self,
comparator: crate::comparator::SharedComparator,
) -> crate::Result<Option<crate::UserKey>> {
match self.try_iter(comparator)?.next() {
Some(item) => Ok(Some(item.materialize(&self.inner.data).key.user_key)),
None => Ok(None),
}
}
#[cfg_attr(
not(feature = "std"),
allow(
dead_code,
reason = "reconcile-gate check; the verify/scrub consumers are std-gated"
)
)]
pub(crate) fn verify_binary_index(&self) -> crate::Result<()> {
Decoder::<InternalValue, DataBlockParsedItem>::verify_binary_index(&self.inner)
}
#[must_use]
pub fn binary_index_len(&self) -> u32 {
use core::mem::size_of;
let trailer = Trailer::new(&self.inner);
let offset = 2 * size_of::<u8>();
let mut reader = unwrap!(trailer.as_slice().get(offset..));
unwrap!(reader.read_u32::<LittleEndian>())
}
#[must_use]
#[expect(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
Trailer::new(&self.inner).item_count()
}
pub fn encode_into_vec(
items: &[InternalValue],
restart_interval: u8,
hash_index_ratio: f32,
) -> crate::Result<Vec<u8>> {
let mut buf = vec![];
Self::encode_into(&mut buf, items, restart_interval, hash_index_ratio)?;
Ok(buf)
}
pub fn encode_into(
writer: &mut Vec<u8>,
items: &[InternalValue],
restart_interval: u8,
hash_index_ratio: f32,
) -> crate::Result<()> {
Self::encode_hashed_into(writer, items, None, restart_interval, hash_index_ratio)
}
pub(crate) fn encode_hashed_into(
writer: &mut Vec<u8>,
items: &[InternalValue],
key_hashes: Option<&[u64]>,
restart_interval: u8,
hash_index_ratio: f32,
) -> crate::Result<()> {
debug_assert!(key_hashes.is_none_or(|hashes| hashes.len() == items.len()));
#[expect(clippy::expect_used, reason = "the chunk should not be empty")]
let first_key = &items
.first()
.expect("chunk should not be empty")
.key
.user_key;
let mut serializer = Encoder::<'_, SeqnoCoding, InternalValue>::new(
writer,
items.len(),
restart_interval,
hash_index_ratio,
first_key,
);
serializer.state = SeqnoCoding::new(block_uses_deltas(items, restart_interval));
for (at, item) in items.iter().enumerate() {
let key_hash = key_hashes.and_then(|hashes| hashes.get(at).copied());
serializer.write_hashed(item, key_hash)?;
}
serializer.finish()
}
#[cfg(test)]
pub(crate) fn encode_kv_checked_into(
writer: &mut Vec<u8>,
items: &[InternalValue],
digests: &[u64],
algo: crate::runtime_config::ChecksumAlgorithm,
restart_interval: u8,
hash_index_ratio: f32,
) -> crate::Result<()> {
Self::encode_kv_checked_hashed_into(
writer,
items,
None,
digests,
algo,
restart_interval,
hash_index_ratio,
)
}
pub(crate) fn encode_kv_checked_hashed_into(
writer: &mut Vec<u8>,
items: &[InternalValue],
key_hashes: Option<&[u64]>,
digests: &[u64],
algo: crate::runtime_config::ChecksumAlgorithm,
restart_interval: u8,
hash_index_ratio: f32,
) -> crate::Result<()> {
if items.len() != digests.len() {
return Err(crate::Error::InvalidTrailer);
}
Self::encode_hashed_into(
writer,
items,
key_hashes,
restart_interval,
hash_index_ratio,
)?;
super::block::kv_checksum::append_footer(writer, digests, algo);
Ok(())
}
#[cfg_attr(
not(feature = "std"),
allow(
dead_code,
reason = "core+alloc per-KV scrub; the verify/scrub consumer is std-gated, so unused under no_std"
)
)]
pub(crate) fn verify_kv_checked(
footer_wrapped: &Slice,
header: super::block::Header,
comparator: crate::comparator::SharedComparator,
expected_algo: Option<crate::runtime_config::ChecksumAlgorithm>,
) -> crate::Result<()> {
use super::block::ParsedItem;
let mut probe = KvDigestProbe::open(footer_wrapped, header, expected_algo)?;
let mut inner_header = header;
inner_header.block_flags &= !super::block::header::block_flags::KV_CHECKSUM_FOOTER;
let inner_len = probe.inner_len();
#[expect(
clippy::cast_possible_truncation,
reason = "split.inner is a prefix of the footered payload, which fits u32"
)]
{
inner_header.uncompressed_length = inner_len as u32;
}
let inner = Self::new(Block {
header: inner_header,
data: footer_wrapped.slice(..inner_len),
});
let inner_data = inner.inner.data.clone();
for parsed in inner.try_iter(comparator)? {
probe.observe(&parsed.materialize(&inner_data))?;
}
probe.finish()
}
}
#[cfg_attr(
not(feature = "std"),
expect(
dead_code,
reason = "core+alloc per-KV scrub; the verify/scrub consumers are std-gated"
)
)]
pub(crate) struct KvDigestProbe<'a> {
split: super::block::kv_checksum::SplitFull<'a>,
seen: usize,
}
#[cfg_attr(
not(feature = "std"),
expect(
dead_code,
reason = "core+alloc per-KV scrub; the verify/scrub consumers are std-gated"
)
)]
impl<'a> KvDigestProbe<'a> {
pub(crate) fn open(
footer_wrapped: &'a Slice,
header: super::block::Header,
expected_algo: Option<crate::runtime_config::ChecksumAlgorithm>,
) -> crate::Result<Self> {
let split = super::block::kv_checksum::split_full(footer_wrapped)?;
if let Some(expected) = expected_algo
&& split.algo != expected
{
return Err(crate::Error::InvalidTrailer);
}
if header.block_type != super::block::BlockType::Data {
return Err(crate::Error::InvalidTag((
"BlockType",
header.block_type.into(),
)));
}
Ok(Self { split, seen: 0 })
}
pub(crate) fn inner_len(&self) -> usize {
self.split.inner.len()
}
pub(crate) fn observe(&mut self, item: &crate::InternalValue) -> crate::Result<()> {
let Some(stored) = self.split.digest(self.seen) else {
return Err(crate::Error::InvalidTrailer);
};
let Some(recomputed) = super::block::kv_checksum::kv_digest(item, self.split.algo) else {
return Err(crate::Error::FeatureUnsupported("kv-checksum-algorithm"));
};
if recomputed != stored {
return Err(crate::Error::ChecksumMismatch {
got: crate::Checksum::from_raw(u128::from(recomputed)),
expected: crate::Checksum::from_raw(u128::from(stored)),
});
}
self.seen += 1;
Ok(())
}
pub(crate) fn finish(self) -> crate::Result<()> {
if self.seen == self.split.count() {
Ok(())
} else {
Err(crate::Error::InvalidTrailer)
}
}
}
#[cfg(test)]
#[expect(clippy::expect_used, clippy::indexing_slicing)]
mod tests;