Skip to main content

loonfs_api/
sst_blocks.rs

1//! Block-granular encoding for metadata SST and derived-index segments.
2//!
3//! A segment object is a sequence of independently readable sections:
4//! data blocks, then one filter block, then one index block. There is no
5//! footer — the manifest's segment descriptor carries the index and filter
6//! handles, so the descriptor is the only entry point into the object.
7//! Readers fetch the byte range a handle names, verify its CRC32C, and
8//! decode just that section; nothing here performs IO.
9//!
10//! The block grammar is row-payload-agnostic: the builder and decoders
11//! carry any CBOR row type, and the segment's descriptor family says which
12//! one to expect — [`MetadataRow`] for metadata tables, `IndexRow` for gram
13//! index segments. The section framing, key compression, filter hashing,
14//! and checksums are identical either way.
15//!
16//! Durable layout, frozen by this module:
17//!
18//! - A **data block** holds prefix-compressed entries: each entry stores
19//!   `(shared_prefix_len, key_suffix_len)` as LEB128 varints, the key
20//!   suffix bytes, then the row as a CBOR-encoded payload length-
21//!   prefixed with a varint. Every [`RESTART_INTERVAL`]th entry is a
22//!   restart point storing its full key (shared prefix length zero). The
23//!   block ends with the restart offsets as little-endian `u32`s and their
24//!   count. The block payload is zstd-compressed.
25//! - The **index block** is a zstd-compressed CBOR list with one entry per
26//!   data block: the block's last row key and its [`BlockHandle`].
27//! - The **filter block** is a bloom filter over caller-chosen filter keys
28//!   (per-family lookup prefixes): `n_hashes` as a little-endian `u32`,
29//!   the bit length as a little-endian `u64`, then the bit bytes. Filter
30//!   bits do not compress, so the payload is stored raw.
31//! - Every section's CRC32C is computed over its stored bytes and lives in
32//!   the handle that names it (index entries for data blocks; the segment
33//!   descriptor for the index and filter), never inside the section.
34//! - Bloom hashing is two xxh64 passes with fixed seeds combined by double
35//!   hashing. The seeds, like the CRC and hash algorithm choices, are
36//!   frozen durable-format constants.
37
38use crate::wire::manifest::MetadataRow;
39use serde::{Deserialize, Serialize};
40use std::io::Read;
41use std::num::NonZeroUsize;
42use thiserror::Error;
43use xxhash_rust::xxh64::xxh64;
44
45/// Target uncompressed size of one data block, in bytes. Sized for direct
46/// object-store reads: request round-trips dominate transfer time at this
47/// scale, so bulk read paths (directory listings read most rows of several
48/// families) want few large ranged GETs, and a lookup fetching one block
49/// still moves trivial bytes. Benchmarked over 8 KiB, which priced a full
50/// listing at one GET per tiny block.
51pub const DEFAULT_TARGET_BLOCK_BYTES: usize = 64 * 1024;
52/// Number of level-zero runs that triggers reorganization.
53pub const DEFAULT_MAX_L0_RUNS: usize = 8;
54/// Target number of rows in one immutable segment.
55pub const DEFAULT_MAX_ROWS_PER_SEGMENT: usize = 65_536;
56/// Maximum number of runs read by one reorganization step.
57pub const DEFAULT_MAX_REORGANIZATION_INPUT_RUNS: usize = 8;
58/// Maximum number of decoded rows read by one reorganization step.
59pub const DEFAULT_MAX_REORGANIZATION_INPUT_ROWS: usize = 131_072;
60/// Maximum decoded input size for one build or reorganization step.
61pub const DEFAULT_MAX_REORGANIZATION_INPUT_BYTES: usize = 64 * 1024 * 1024;
62/// Maximum stored filter size embedded in a segment descriptor.
63pub const DEFAULT_INLINE_FILTER_MAX_BYTES: u32 = 1024;
64/// Entries between restart points inside a data block.
65pub const RESTART_INTERVAL: usize = 16;
66/// Bloom filter sizing: bits reserved per inserted filter key.
67pub const FILTER_BITS_PER_KEY: usize = 10;
68/// Bloom filter probe count, chosen for [`FILTER_BITS_PER_KEY`].
69pub const FILTER_HASH_COUNT: u32 = 7;
70
71const FILTER_HASH_SEED_ONE: u64 = 0;
72const FILTER_HASH_SEED_TWO: u64 = 0x9e37_79b9_7f4a_7c15;
73/// One compression level for every zstd-compressed durable artifact (SST
74/// blocks and WAL segment envelopes). 3 is also the library default, so
75/// this pins in a name what an implicit `0` would choose silently.
76pub(crate) const ZSTD_LEVEL: i32 = 3;
77
78/// Where one stored section lives inside a segment object, and how to
79/// verify it: the CRC32C of the stored bytes and their decoded length.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub struct BlockHandle {
82    /// Zero-based byte offset of the section within its immutable segment object.
83    pub offset: u64,
84    /// Number of bytes to range-read and checksum before decoding.
85    pub stored_len: u32,
86    /// Expected byte length after optional section decompression.
87    pub decoded_len: u32,
88    /// CRC32C over the exact `stored_len` bytes at `offset`.
89    pub crc32c: u32,
90}
91
92/// One index entry: the last row key of a data block plus its handle.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct SegmentIndexEntry {
95    /// Greatest row key in `block`, used to binary-search candidate blocks.
96    pub last_key: String,
97    /// Data-section location and integrity metadata.
98    pub block: BlockHandle,
99}
100
101/// A finished segment: the object bytes plus everything the manifest
102/// descriptor must carry to read them back.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct BuiltSegmentBlocks {
105    /// Complete immutable object body, with data sections followed by filter and index sections.
106    pub bytes: Vec<u8>,
107    /// Handle callers persist in the segment descriptor to bootstrap reads.
108    pub index: BlockHandle,
109    /// Handle callers persist for negative point-lookup filtering.
110    pub filter: BlockHandle,
111    /// Number of rows accepted by the builder, including adjacent duplicate keys.
112    pub row_count: u64,
113    /// Least row key in the non-empty segment.
114    pub min_key: String,
115    /// Greatest row key in the non-empty segment.
116    pub max_key: String,
117}
118
119/// One decoded data block: row keys and rows, parallel and in key order.
120/// The row type defaults to [`MetadataRow`]; index segments decode their
121/// own row payload through [`decode_data_block_rows`].
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct DecodedDataBlock<R = MetadataRow> {
124    /// Reconstructed row keys in the same ascending order as `rows`.
125    pub row_keys: Vec<String>,
126    /// Decoded row payloads positionally paired with `row_keys`.
127    pub rows: Vec<R>,
128}
129
130/// A decoded bloom filter; answers "definitely absent" or "maybe present".
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct SegmentFilter {
133    n_hashes: u32,
134    bit_len: u64,
135    bits: Vec<u8>,
136}
137
138/// Describes a violation encountered while building or validating an SST section.
139///
140/// See [metadata segments](../../../docs/specs/format.md#421-metadata-segments).
141#[derive(Debug, Clone, PartialEq, Eq, Error)]
142#[non_exhaustive]
143pub enum SstBlockCodecError {
144    /// Reports a request to finish a segment before any row was supplied.
145    #[error("segment must contain at least one row")]
146    EmptySegment,
147    /// Reports a builder input that would violate durable ascending row-key order.
148    #[error("row key `{offered}` is not in ascending order after `{previous}`")]
149    RowKeysOutOfOrder {
150        /// Last key the builder accepted.
151        previous: String,
152        /// Descending key rejected by the builder.
153        offered: String,
154    },
155    /// Reports a range-read body whose byte count disagrees with its handle.
156    #[error("stored bytes length {actual} does not match handle length {expected}")]
157    StoredLengthMismatch {
158        /// Stored byte count recorded in the persisted `BlockHandle`.
159        expected: u32,
160        /// Byte count returned to the decoder.
161        actual: usize,
162    },
163    /// Reports stored section bytes that fail the CRC32C recorded in their handle.
164    #[error("block checksum mismatch: expected {expected:#010x}, actual {actual:#010x}")]
165    ChecksumMismatch {
166        /// CRC32C recorded in the persisted `BlockHandle`.
167        expected: u32,
168        /// CRC32C recomputed from the supplied stored bytes.
169        actual: u32,
170    },
171    /// Reports a section whose decompressed size disagrees with its handle.
172    #[error("decoded length {actual} does not match handle length {expected}")]
173    DecodedLengthMismatch {
174        /// Decoded byte count recorded in the persisted `BlockHandle`.
175        expected: u32,
176        /// Byte count produced by section decompression.
177        actual: usize,
178    },
179    /// Reports structurally invalid framing, ordering, UTF-8, or filter metadata.
180    #[error("malformed block: {0}")]
181    Malformed(String),
182    /// Reports a CBOR or zstd failure while encoding or decoding a section.
183    #[error("block codec error: {0}")]
184    Codec(String),
185}
186
187/// Builds one segment's blocks from rows fed in ascending row-key order.
188#[derive(Debug)]
189#[must_use]
190pub struct SegmentBlocksBuilder {
191    target_block_bytes: usize,
192    entries: Vec<u8>,
193    restarts: Vec<u32>,
194    entry_count: usize,
195    /// Last row key the builder accepted. It anchors prefix compression
196    /// inside a block, floors the ascending-order guard, and becomes the
197    /// segment's max key. A block's first entry stores its key in full
198    /// regardless, because a restart point always begins a block.
199    previous_key: String,
200    finished_blocks: Vec<(String, Vec<u8>)>,
201    filter_hashes: Vec<(u64, u64)>,
202    row_count: u64,
203    min_key: String,
204}
205
206impl Default for SegmentBlocksBuilder {
207    fn default() -> Self {
208        Self::new(const { NonZeroUsize::new(DEFAULT_TARGET_BLOCK_BYTES).unwrap() })
209    }
210}
211
212impl SegmentBlocksBuilder {
213    /// Creates a builder that closes a data block after reaching the target decoded byte size.
214    pub fn new(target_block_bytes: NonZeroUsize) -> Self {
215        Self {
216            target_block_bytes: target_block_bytes.get(),
217            entries: Vec::new(),
218            restarts: Vec::new(),
219            entry_count: 0,
220            previous_key: String::new(),
221            finished_blocks: Vec::new(),
222            filter_hashes: Vec::new(),
223            row_count: 0,
224            min_key: String::new(),
225        }
226    }
227
228    /// Appends one row. `filter_key` is the lookup prefix point reads will
229    /// probe for this row; the caller derives it per family. The row is any
230    /// CBOR payload; a segment must hold one row type throughout, named by
231    /// the descriptor family that references it.
232    pub fn push<R: Serialize>(
233        &mut self,
234        row_key: &str,
235        filter_key: &str,
236        row: &R,
237    ) -> Result<(), SstBlockCodecError> {
238        if self.row_count > 0 && row_key < self.previous_key.as_str() {
239            return Err(SstBlockCodecError::RowKeysOutOfOrder {
240                previous: self.previous_key.clone(),
241                offered: row_key.to_owned(),
242            });
243        }
244        if self.row_count == 0 {
245            self.min_key = row_key.to_owned();
246        }
247        self.filter_hashes.push(filter_key_hashes(filter_key));
248
249        let restart = self.entry_count % RESTART_INTERVAL == 0;
250        if restart {
251            self.restarts.push(self.entries.len() as u32);
252        }
253        let shared_len = if restart {
254            0
255        } else {
256            shared_prefix_len(&self.previous_key, row_key)
257        };
258        let suffix = &row_key.as_bytes()[shared_len..];
259        let mut row_bytes = Vec::new();
260        ciborium::ser::into_writer(row, &mut row_bytes)
261            .map_err(|error| SstBlockCodecError::Codec(error.to_string()))?;
262        write_varint(&mut self.entries, shared_len as u64);
263        write_varint(&mut self.entries, suffix.len() as u64);
264        self.entries.extend_from_slice(suffix);
265        write_varint(&mut self.entries, row_bytes.len() as u64);
266        self.entries.extend_from_slice(&row_bytes);
267
268        self.entry_count += 1;
269        self.row_count += 1;
270        self.previous_key.clear();
271        self.previous_key.push_str(row_key);
272        if self.entries.len() >= self.target_block_bytes {
273            self.finish_data_block();
274        }
275        Ok(())
276    }
277
278    fn finish_data_block(&mut self) {
279        if self.entries.is_empty() {
280            return;
281        }
282        let mut payload = std::mem::take(&mut self.entries);
283        for restart in &self.restarts {
284            payload.extend_from_slice(&restart.to_le_bytes());
285        }
286        payload.extend_from_slice(&(self.restarts.len() as u32).to_le_bytes());
287        self.restarts.clear();
288        self.entry_count = 0;
289        // The block copies the last key it holds. Taking it would leave the
290        // builder without one, and the builder still needs it: as the prefix
291        // anchor inside the next block, as the order guard's floor across the
292        // boundary, and as the segment's max key once every row is in.
293        self.finished_blocks
294            .push((self.previous_key.clone(), payload));
295    }
296
297    /// Encodes the remaining rows and assembles the object bytes.
298    pub fn finish(mut self) -> Result<BuiltSegmentBlocks, SstBlockCodecError> {
299        if self.row_count == 0 {
300            return Err(SstBlockCodecError::EmptySegment);
301        }
302        self.finish_data_block();
303
304        let mut bytes = Vec::new();
305        let mut index = Vec::with_capacity(self.finished_blocks.len());
306        for (last_key, payload) in std::mem::take(&mut self.finished_blocks) {
307            let block = append_section(&mut bytes, &payload, true)?;
308            index.push(SegmentIndexEntry { last_key, block });
309        }
310
311        let filter_payload = build_filter_payload(&self.filter_hashes);
312        let filter = append_section(&mut bytes, &filter_payload, false)?;
313
314        let mut index_payload = Vec::new();
315        ciborium::ser::into_writer(&index, &mut index_payload)
316            .map_err(|error| SstBlockCodecError::Codec(error.to_string()))?;
317        let index = append_section(&mut bytes, &index_payload, true)?;
318
319        Ok(BuiltSegmentBlocks {
320            bytes,
321            index,
322            filter,
323            row_count: self.row_count,
324            min_key: self.min_key,
325            max_key: self.previous_key,
326        })
327    }
328}
329
330/// Decodes the index block from exactly the bytes its handle names.
331pub fn decode_index_block(
332    stored: &[u8],
333    handle: &BlockHandle,
334) -> Result<Vec<SegmentIndexEntry>, SstBlockCodecError> {
335    let payload = decode_section(stored, handle, true)?;
336    let entries: Vec<SegmentIndexEntry> = ciborium::de::from_reader(payload.as_slice())
337        .map_err(|error| SstBlockCodecError::Codec(error.to_string()))?;
338    // Index keys must be sorted, and block ranges must tile their region:
339    // every range fits in `u64`, and each block starts exactly where its
340    // predecessor ends — the builder writes blocks back to back. Range
341    // lookup relies on key order; span loading and its bulk-read budget
342    // rely on contiguous, overflow-free ranges.
343    let mut previous: Option<(&String, u64)> = None;
344    for entry in &entries {
345        let end = entry
346            .block
347            .offset
348            .checked_add(u64::from(entry.block.stored_len))
349            .ok_or_else(|| {
350                SstBlockCodecError::Malformed(format!(
351                    "index block `{}` byte range overflows",
352                    entry.last_key
353                ))
354            })?;
355        if let Some((previous_key, previous_end)) = previous {
356            if previous_key > &entry.last_key {
357                return Err(SstBlockCodecError::Malformed(format!(
358                    "index blocks out of key order: `{}` follows `{previous_key}`",
359                    entry.last_key
360                )));
361            }
362            if entry.block.offset != previous_end {
363                return Err(SstBlockCodecError::Malformed(format!(
364                    "index block `{}` does not start where `{previous_key}` ends",
365                    entry.last_key
366                )));
367            }
368        }
369        previous = Some((&entry.last_key, end));
370    }
371    Ok(entries)
372}
373
374/// Decodes one data block from exactly the bytes its handle names.
375pub fn decode_data_block(
376    stored: &[u8],
377    handle: &BlockHandle,
378) -> Result<DecodedDataBlock, SstBlockCodecError> {
379    decode_data_block_rows::<MetadataRow>(stored, handle)
380}
381
382/// Decodes one data block whose rows are `R`, for segment families whose
383/// row payload is not [`MetadataRow`] (gram index segments).
384pub fn decode_data_block_rows<R: serde::de::DeserializeOwned>(
385    stored: &[u8],
386    handle: &BlockHandle,
387) -> Result<DecodedDataBlock<R>, SstBlockCodecError> {
388    let payload = decode_section(stored, handle, true)?;
389    if payload.len() < 4 {
390        return Err(SstBlockCodecError::Malformed(
391            "data block shorter than its restart count".to_owned(),
392        ));
393    }
394    let (body, restart_count_bytes) = payload.split_at(payload.len() - 4);
395    let restart_count = u32::from_le_bytes(
396        restart_count_bytes
397            .try_into()
398            .expect("split_at should leave exactly four bytes"),
399    ) as usize;
400    let restarts_len = restart_count
401        .checked_mul(4)
402        .filter(|len| *len <= body.len())
403        .ok_or_else(|| SstBlockCodecError::Malformed("restart array exceeds block".to_owned()))?;
404    let entries = &body[..body.len() - restarts_len];
405
406    let mut row_keys = Vec::new();
407    let mut rows = Vec::new();
408    let mut cursor = 0usize;
409    let mut previous_key = String::new();
410    while cursor < entries.len() {
411        let shared_len = read_varint(entries, &mut cursor)? as usize;
412        let suffix_len = read_varint(entries, &mut cursor)? as usize;
413        if shared_len > previous_key.len() || !previous_key.is_char_boundary(shared_len) {
414            return Err(SstBlockCodecError::Malformed(
415                "shared prefix exceeds previous key".to_owned(),
416            ));
417        }
418        let suffix = take_slice(entries, &mut cursor, suffix_len)?;
419        let suffix = std::str::from_utf8(suffix)
420            .map_err(|_| SstBlockCodecError::Malformed("row key is not utf-8".to_owned()))?;
421        let mut key = String::with_capacity(shared_len + suffix.len());
422        key.push_str(&previous_key[..shared_len]);
423        key.push_str(suffix);
424        let row_len = read_varint(entries, &mut cursor)? as usize;
425        let row_bytes = take_slice(entries, &mut cursor, row_len)?;
426        let row: R = ciborium::de::from_reader(row_bytes)
427            .map_err(|error| SstBlockCodecError::Codec(error.to_string()))?;
428        // Ascending row-key order is a format requirement; readers
429        // binary-search on it, so an out-of-order block is malformed.
430        if key.as_str() < previous_key.as_str() {
431            return Err(SstBlockCodecError::Malformed(format!(
432                "rows out of row-key order: `{key}` follows `{previous_key}`"
433            )));
434        }
435        previous_key.clear();
436        previous_key.push_str(&key);
437        row_keys.push(key);
438        rows.push(row);
439    }
440    Ok(DecodedDataBlock { row_keys, rows })
441}
442
443/// Decodes the filter block from exactly the bytes its handle names.
444pub fn decode_filter_block(
445    stored: &[u8],
446    handle: &BlockHandle,
447) -> Result<SegmentFilter, SstBlockCodecError> {
448    let payload = decode_section(stored, handle, false)?;
449    if payload.len() < 12 {
450        return Err(SstBlockCodecError::Malformed(
451            "filter block shorter than its header".to_owned(),
452        ));
453    }
454    let n_hashes = u32::from_le_bytes(
455        payload[0..4]
456            .try_into()
457            .expect("header length should be checked above"),
458    );
459    let bit_len = u64::from_le_bytes(
460        payload[4..12]
461            .try_into()
462            .expect("header length should be checked above"),
463    );
464    let bits = payload[12..].to_vec();
465    if bit_len.div_ceil(8) != bits.len() as u64 {
466        return Err(SstBlockCodecError::Malformed(
467            "filter bit length disagrees with its bytes".to_owned(),
468        ));
469    }
470    Ok(SegmentFilter {
471        n_hashes,
472        bit_len,
473        bits,
474    })
475}
476
477impl SegmentFilter {
478    /// False means no row with this filter key is in the segment; true
479    /// means one may be.
480    pub fn may_contain(&self, filter_key: &str) -> bool {
481        if self.bit_len == 0 {
482            return false;
483        }
484        let (h1, h2) = filter_key_hashes(filter_key);
485        for probe in 0..u64::from(self.n_hashes) {
486            let bit = h1.wrapping_add(probe.wrapping_mul(h2)) % self.bit_len;
487            let byte = self.bits[(bit / 8) as usize];
488            if byte & (1 << (bit % 8)) == 0 {
489                return false;
490            }
491        }
492        true
493    }
494}
495
496/// The exclusive upper bound for every row key beginning with `prefix`.
497///
498/// Row keys are ordered as byte strings, so a prefix scan is the range
499/// `[prefix, string_prefix_upper_bound(prefix))`. `None` means the prefix is
500/// all `0xff` bytes and nothing sorts above it, so the scan runs to the end.
501pub fn string_prefix_upper_bound(prefix: &str) -> Option<String> {
502    let mut bytes = prefix.as_bytes().to_vec();
503    for index in (0..bytes.len()).rev() {
504        if bytes[index] != u8::MAX {
505            bytes[index] += 1;
506            bytes.truncate(index + 1);
507            return String::from_utf8(bytes).ok();
508        }
509    }
510    None
511}
512
513/// Index positions of the blocks that can hold keys in
514/// `[lower_bound, upper_bound)`; `None` bounds the range at the last block.
515pub fn index_blocks_for_key_range(
516    index: &[SegmentIndexEntry],
517    lower_bound: &str,
518    upper_bound: Option<&str>,
519) -> std::ops::Range<usize> {
520    let start = index.partition_point(|entry| entry.last_key.as_str() < lower_bound);
521    let end = upper_bound.map_or(index.len(), |upper_bound| {
522        // A block whose last key equals the exclusive upper bound can still
523        // hold keys below it, so the bound block itself is included.
524        index
525            .partition_point(|entry| entry.last_key.as_str() < upper_bound)
526            .saturating_add(1)
527            .min(index.len())
528    });
529    start..end.max(start)
530}
531
532fn append_section(
533    bytes: &mut Vec<u8>,
534    payload: &[u8],
535    compress: bool,
536) -> Result<BlockHandle, SstBlockCodecError> {
537    let stored = if compress {
538        zstd::bulk::compress(payload, ZSTD_LEVEL)
539            .map_err(|error| SstBlockCodecError::Codec(error.to_string()))?
540    } else {
541        payload.to_vec()
542    };
543    let handle = BlockHandle {
544        offset: bytes.len() as u64,
545        stored_len: stored.len() as u32,
546        decoded_len: payload.len() as u32,
547        crc32c: crc32c::crc32c(&stored),
548    };
549    bytes.extend_from_slice(&stored);
550    Ok(handle)
551}
552
553fn decode_section(
554    stored: &[u8],
555    handle: &BlockHandle,
556    compressed: bool,
557) -> Result<Vec<u8>, SstBlockCodecError> {
558    if stored.len() != handle.stored_len as usize {
559        return Err(SstBlockCodecError::StoredLengthMismatch {
560            expected: handle.stored_len,
561            actual: stored.len(),
562        });
563    }
564    let actual = crc32c::crc32c(stored);
565    if actual != handle.crc32c {
566        return Err(SstBlockCodecError::ChecksumMismatch {
567            expected: handle.crc32c,
568            actual,
569        });
570    }
571    let payload = if compressed {
572        let mut payload = Vec::with_capacity(handle.decoded_len as usize);
573        zstd::Decoder::new(stored)
574            .and_then(|mut decoder| decoder.read_to_end(&mut payload))
575            .map_err(|error| SstBlockCodecError::Codec(error.to_string()))?;
576        payload
577    } else {
578        stored.to_vec()
579    };
580    if payload.len() != handle.decoded_len as usize {
581        return Err(SstBlockCodecError::DecodedLengthMismatch {
582            expected: handle.decoded_len,
583            actual: payload.len(),
584        });
585    }
586    Ok(payload)
587}
588
589fn build_filter_payload(hashes: &[(u64, u64)]) -> Vec<u8> {
590    let bit_len = (hashes.len() * FILTER_BITS_PER_KEY).max(64) as u64;
591    let mut bits = vec![0u8; bit_len.div_ceil(8) as usize];
592    for (h1, h2) in hashes {
593        for probe in 0..u64::from(FILTER_HASH_COUNT) {
594            let bit = h1.wrapping_add(probe.wrapping_mul(*h2)) % bit_len;
595            bits[(bit / 8) as usize] |= 1 << (bit % 8);
596        }
597    }
598    let mut payload = Vec::with_capacity(12 + bits.len());
599    payload.extend_from_slice(&FILTER_HASH_COUNT.to_le_bytes());
600    payload.extend_from_slice(&bit_len.to_le_bytes());
601    payload.extend_from_slice(&bits);
602    payload
603}
604
605fn filter_key_hashes(filter_key: &str) -> (u64, u64) {
606    (
607        xxh64(filter_key.as_bytes(), FILTER_HASH_SEED_ONE),
608        xxh64(filter_key.as_bytes(), FILTER_HASH_SEED_TWO),
609    )
610}
611
612fn shared_prefix_len(previous: &str, current: &str) -> usize {
613    let mut len = previous
614        .as_bytes()
615        .iter()
616        .zip(current.as_bytes())
617        .take_while(|(a, b)| a == b)
618        .count();
619    // Both inputs are valid UTF-8 strings; back the byte-wise prefix off to
620    // a character boundary so key reconstruction can slice the previous key.
621    while !current.is_char_boundary(len) {
622        len -= 1;
623    }
624    len
625}
626
627pub(crate) fn write_varint(bytes: &mut Vec<u8>, mut value: u64) {
628    loop {
629        let byte = (value & 0x7f) as u8;
630        value >>= 7;
631        if value == 0 {
632            bytes.push(byte);
633            return;
634        }
635        bytes.push(byte | 0x80);
636    }
637}
638
639pub(crate) fn read_varint(bytes: &[u8], cursor: &mut usize) -> Result<u64, SstBlockCodecError> {
640    let mut value = 0u64;
641    let mut shift = 0u32;
642    loop {
643        let byte = *bytes.get(*cursor).ok_or_else(|| {
644            SstBlockCodecError::Malformed("varint runs past the block".to_owned())
645        })?;
646        *cursor += 1;
647        if shift >= 64 {
648            return Err(SstBlockCodecError::Malformed(
649                "varint exceeds 64 bits".to_owned(),
650            ));
651        }
652        value |= u64::from(byte & 0x7f) << shift;
653        if byte & 0x80 == 0 {
654            return Ok(value);
655        }
656        shift += 7;
657    }
658}
659
660fn take_slice<'a>(
661    bytes: &'a [u8],
662    cursor: &mut usize,
663    len: usize,
664) -> Result<&'a [u8], SstBlockCodecError> {
665    let end = cursor.checked_add(len).filter(|end| *end <= bytes.len());
666    match end {
667        Some(end) => {
668            let slice = &bytes[*cursor..end];
669            *cursor = end;
670            Ok(slice)
671        }
672        None => Err(SstBlockCodecError::Malformed(
673            "entry runs past the block".to_owned(),
674        )),
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use super::*;
681    use crate::{ChangeSeq, InodeId, InodeKind};
682
683    fn inode_row(inode_id: u64) -> (String, String, MetadataRow) {
684        let row = MetadataRow::Inode {
685            inode_id: InodeId(inode_id),
686            inode_kind: InodeKind::File,
687            created_seq: ChangeSeq(inode_id),
688            created_by: crate::ActorRef::loonfs_system(),
689            created_at_ms: inode_id,
690        };
691        let key = row.row_key();
692        (key.clone(), key, row)
693    }
694
695    fn build_segment(rows: usize) -> BuiltSegmentBlocks {
696        let mut builder = SegmentBlocksBuilder::default();
697        for index in 0..rows {
698            let (key, filter_key, row) = inode_row(index as u64);
699            builder.push(&key, &filter_key, &row).expect("push row");
700        }
701        builder.finish().expect("finish segment")
702    }
703
704    fn section<'a>(bytes: &'a [u8], handle: &BlockHandle) -> &'a [u8] {
705        &bytes[handle.offset as usize..handle.offset as usize + handle.stored_len as usize]
706    }
707
708    fn encode_index(entries: &[SegmentIndexEntry]) -> (Vec<u8>, BlockHandle) {
709        let mut payload = Vec::new();
710        ciborium::ser::into_writer(entries, &mut payload).expect("encode index");
711        let mut bytes = Vec::new();
712        let handle = append_section(&mut bytes, &payload, true).expect("append section");
713        (bytes, handle)
714    }
715
716    fn index_entry(last_key: &str, offset: u64, stored_len: u32) -> SegmentIndexEntry {
717        SegmentIndexEntry {
718            last_key: last_key.to_owned(),
719            block: BlockHandle {
720                offset,
721                stored_len,
722                decoded_len: stored_len,
723                crc32c: 0,
724            },
725        }
726    }
727
728    #[test]
729    fn segment_round_trips_every_row_through_index_and_blocks() {
730        let rows = 5_000;
731        let built = build_segment(rows);
732        let index =
733            decode_index_block(section(&built.bytes, &built.index), &built.index).expect("index");
734        assert!(index.len() > 1, "5k inode rows should span several blocks");
735
736        let mut recovered = Vec::new();
737        for entry in &index {
738            let block = decode_data_block(section(&built.bytes, &entry.block), &entry.block)
739                .expect("data block");
740            assert_eq!(block.row_keys.len(), block.rows.len());
741            assert_eq!(
742                block.row_keys.last().expect("blocks are never empty"),
743                &entry.last_key
744            );
745            recovered.extend(block.row_keys.iter().cloned());
746        }
747        let expected: Vec<String> = (0..rows).map(|i| inode_row(i as u64).0).collect();
748        assert_eq!(recovered, expected);
749        assert_eq!(built.row_count, rows as u64);
750        assert_eq!(built.min_key, expected[0]);
751        assert_eq!(&built.max_key, expected.last().expect("rows"));
752    }
753
754    #[test]
755    fn index_narrows_point_lookups_to_one_block() {
756        let built = build_segment(5_000);
757        let index =
758            decode_index_block(section(&built.bytes, &built.index), &built.index).expect("index");
759        let (key, _, row) = inode_row(3_217);
760        let upper = format!("{key}\0");
761        let range = index_blocks_for_key_range(&index, &key, Some(&upper));
762        assert_eq!(range.len(), 1, "a point lookup should touch one block");
763        let entry = &index[range.start];
764        let block =
765            decode_data_block(section(&built.bytes, &entry.block), &entry.block).expect("block");
766        let position = block
767            .row_keys
768            .binary_search_by(|candidate| candidate.as_str().cmp(key.as_str()))
769            .expect("row should be present");
770        assert_eq!(block.rows[position], row);
771    }
772
773    #[test]
774    fn key_range_scan_covers_exactly_the_matching_blocks() {
775        let built = build_segment(5_000);
776        let index =
777            decode_index_block(section(&built.bytes, &built.index), &built.index).expect("index");
778        let lower = inode_row(1_000).0;
779        let upper = inode_row(1_500).0;
780        let range = index_blocks_for_key_range(&index, &lower, Some(&upper));
781        let mut keys = Vec::new();
782        for entry in &index[range] {
783            let block = decode_data_block(section(&built.bytes, &entry.block), &entry.block)
784                .expect("block");
785            keys.extend(block.row_keys);
786        }
787        let keys: Vec<&String> = keys
788            .iter()
789            .filter(|key| key.as_str() >= lower.as_str() && key.as_str() < upper.as_str())
790            .collect();
791        assert_eq!(keys.len(), 500);
792    }
793
794    #[test]
795    fn out_of_order_and_empty_segments_are_rejected() {
796        let mut builder = SegmentBlocksBuilder::default();
797        let (key_b, filter_b, row_b) = inode_row(2);
798        let (key_a, filter_a, row_a) = inode_row(1);
799        builder.push(&key_b, &filter_b, &row_b).expect("first row");
800        let error = builder
801            .push(&key_a, &filter_a, &row_a)
802            .expect_err("descending key should be rejected");
803        assert!(matches!(
804            error,
805            SstBlockCodecError::RowKeysOutOfOrder { .. }
806        ));
807
808        let error = SegmentBlocksBuilder::default()
809            .finish()
810            .expect_err("empty segment should be rejected");
811        assert!(matches!(error, SstBlockCodecError::EmptySegment));
812    }
813
814    /// The builder closes a data block from inside `push` as soon as the
815    /// block reaches its target size, so the last row of a segment can be
816    /// the row that closes one. The segment's max key must survive that:
817    /// an empty max key sorts below every bound, so a keyed scan would
818    /// prune the whole segment away and report the rows missing.
819    #[test]
820    fn max_key_survives_a_last_row_that_closes_its_block() {
821        // Calibrate the target so the crossing lands on the final row.
822        // Building the same rows as one block reports how many entry bytes
823        // they occupy: a block payload is the entries, then one `u32` per
824        // restart point, then the restart count.
825        let rows = 100usize;
826        let single_block = build_segment(rows);
827        let calibration = decode_index_block(
828            section(&single_block.bytes, &single_block.index),
829            &single_block.index,
830        )
831        .expect("index");
832        assert_eq!(calibration.len(), 1, "the calibration segment is one block");
833        let restarts = rows.div_ceil(RESTART_INTERVAL);
834        let entry_bytes = calibration[0].block.decoded_len as usize - 4 * restarts - 4;
835
836        let mut builder =
837            SegmentBlocksBuilder::new(NonZeroUsize::new(entry_bytes).expect("positive target"));
838        for index in 0..rows {
839            let (key, filter_key, row) = inode_row(index as u64);
840            builder.push(&key, &filter_key, &row).expect("push row");
841        }
842        let built = builder.finish().expect("finish segment");
843
844        let expected_max = inode_row((rows - 1) as u64).0;
845        assert_eq!(built.min_key, inode_row(0).0);
846        assert_eq!(built.max_key, expected_max);
847        assert_eq!(built.row_count, rows as u64);
848        // The last push closed the block, so `finish` appended nothing. The
849        // object must still be the same bytes a larger target produces.
850        assert_eq!(built.bytes, single_block.bytes);
851        let index =
852            decode_index_block(section(&built.bytes, &built.index), &built.index).expect("index");
853        assert_eq!(index.len(), 1);
854        assert_eq!(index[0].last_key, expected_max);
855    }
856
857    /// A segment's key range describes its rows, not its block geometry, so
858    /// the same rows must report the same range at every target size.
859    #[test]
860    fn block_geometry_does_not_change_the_segment_key_range() {
861        let rows = 400usize;
862        let expected: Vec<String> = (0..rows).map(|index| inode_row(index as u64).0).collect();
863        for target in [1usize, 64, 257, 1_024, 4_096, 65_536] {
864            let mut builder =
865                SegmentBlocksBuilder::new(NonZeroUsize::new(target).expect("positive target"));
866            for index in 0..rows {
867                let (key, filter_key, row) = inode_row(index as u64);
868                builder.push(&key, &filter_key, &row).expect("push row");
869            }
870            let built = builder.finish().expect("finish segment");
871            assert_eq!(built.min_key, expected[0], "target {target}");
872            assert_eq!(
873                &built.max_key,
874                expected.last().expect("rows"),
875                "target {target}"
876            );
877            assert_eq!(built.row_count, rows as u64, "target {target}");
878
879            let index = decode_index_block(section(&built.bytes, &built.index), &built.index)
880                .expect("index");
881            let mut recovered = Vec::new();
882            for entry in &index {
883                let block = decode_data_block(section(&built.bytes, &entry.block), &entry.block)
884                    .expect("data block");
885                // Each block still names its own last key, so the index the
886                // reader binary-searches keeps its shape.
887                assert_eq!(
888                    block.row_keys.last().expect("blocks are never empty"),
889                    &entry.last_key,
890                    "target {target}"
891                );
892                recovered.extend(block.row_keys);
893            }
894            assert_eq!(recovered, expected, "target {target}");
895            assert_eq!(
896                index.last().expect("blocks").last_key,
897                built.max_key,
898                "target {target}"
899            );
900        }
901    }
902
903    /// Closing a block clears the prefix anchor, so the order guard has to
904    /// read the last accepted row key instead. Every push closes a block
905    /// here, which puts the offered row first in a fresh block.
906    #[test]
907    fn a_descending_row_after_a_block_boundary_is_rejected() {
908        let mut builder = SegmentBlocksBuilder::new(NonZeroUsize::MIN);
909        let (key_high, filter_high, row_high) = inode_row(9);
910        builder
911            .push(&key_high, &filter_high, &row_high)
912            .expect("first row");
913        let (key_low, filter_low, row_low) = inode_row(3);
914        let error = builder
915            .push(&key_low, &filter_low, &row_low)
916            .expect_err("a descending key across a block boundary should be rejected");
917        assert!(
918            matches!(
919                &error,
920                SstBlockCodecError::RowKeysOutOfOrder { previous, offered }
921                    if previous == &key_high && offered == &key_low
922            ),
923            "unexpected error: {error}"
924        );
925    }
926
927    #[test]
928    fn adjacent_equal_keys_are_permitted() {
929        let mut builder = SegmentBlocksBuilder::default();
930        let (key, filter_key, row) = inode_row(7);
931        builder.push(&key, &filter_key, &row).expect("first copy");
932        builder.push(&key, &filter_key, &row).expect("second copy");
933        let built = builder.finish().expect("finish");
934        assert_eq!(built.row_count, 2);
935    }
936
937    #[test]
938    fn corrupted_sections_fail_their_checksums() {
939        let built = build_segment(200);
940        let index =
941            decode_index_block(section(&built.bytes, &built.index), &built.index).expect("index");
942
943        let mut corrupted = built.bytes.clone();
944        let target = index[0].block.offset as usize + 3;
945        corrupted[target] ^= 0xff;
946        let error = decode_data_block(section(&corrupted, &index[0].block), &index[0].block)
947            .expect_err("corrupted data block should fail");
948        assert!(matches!(error, SstBlockCodecError::ChecksumMismatch { .. }));
949
950        let mut corrupted = built.bytes.clone();
951        let target = built.index.offset as usize + 3;
952        corrupted[target] ^= 0xff;
953        let error = decode_index_block(section(&corrupted, &built.index), &built.index)
954            .expect_err("corrupted index should fail");
955        assert!(matches!(error, SstBlockCodecError::ChecksumMismatch { .. }));
956
957        let mut corrupted = built.bytes.clone();
958        let target = built.filter.offset as usize + 12;
959        corrupted[target] ^= 0xff;
960        let error = decode_filter_block(section(&corrupted, &built.filter), &built.filter)
961            .expect_err("corrupted filter should fail");
962        assert!(matches!(error, SstBlockCodecError::ChecksumMismatch { .. }));
963    }
964
965    #[test]
966    fn filter_has_no_false_negatives_and_few_false_positives() {
967        let rows = 2_000;
968        let built = build_segment(rows);
969        let filter = decode_filter_block(section(&built.bytes, &built.filter), &built.filter)
970            .expect("filter");
971        for index in 0..rows {
972            let (key, _, _) = inode_row(index as u64);
973            assert!(filter.may_contain(&key), "inserted key must stay positive");
974        }
975        let mut false_positives = 0usize;
976        let probes = 10_000usize;
977        for index in 0..probes {
978            let (absent, _, _) = inode_row((rows + 10_000 + index) as u64);
979            if filter.may_contain(&absent) {
980                false_positives += 1;
981            }
982        }
983        let rate = false_positives as f64 / probes as f64;
984        assert!(rate < 0.02, "false positive rate {rate} exceeds 2%");
985    }
986
987    #[test]
988    fn durable_encoding_is_deterministic() {
989        let first = build_segment(300);
990        let second = build_segment(300);
991        assert_eq!(first.bytes, second.bytes);
992        assert_eq!(first.index, second.index);
993        assert_eq!(first.filter, second.filter);
994    }
995
996    #[test]
997    fn decoding_rejects_out_of_order_rows_in_a_block() {
998        // A hostile block with descending keys and a valid CRC: encode two
999        // full-key entries in the wrong order through the private helpers.
1000        let mut entries = Vec::new();
1001        for inode in [9u64, 3u64] {
1002            let (key, _, row) = inode_row(inode);
1003            let mut row_bytes = Vec::new();
1004            ciborium::ser::into_writer(&row, &mut row_bytes).expect("encode row");
1005            write_varint(&mut entries, 0);
1006            write_varint(&mut entries, key.len() as u64);
1007            entries.extend_from_slice(key.as_bytes());
1008            write_varint(&mut entries, row_bytes.len() as u64);
1009            entries.extend_from_slice(&row_bytes);
1010        }
1011        let mut payload = entries;
1012        payload.extend_from_slice(&0u32.to_le_bytes());
1013        payload.extend_from_slice(&0u32.to_le_bytes());
1014        let mut bytes = Vec::new();
1015        let handle = append_section(&mut bytes, &payload, true).expect("append section");
1016
1017        let error =
1018            decode_data_block(&bytes, &handle).expect_err("descending rows should be rejected");
1019        assert!(
1020            matches!(&error, SstBlockCodecError::Malformed(message) if message.contains("row-key order")),
1021            "unexpected error: {error}"
1022        );
1023    }
1024
1025    #[test]
1026    fn decoding_rejects_a_shared_prefix_inside_a_utf8_code_point() {
1027        let (_, _, row) = inode_row(1);
1028        let mut row_bytes = Vec::new();
1029        ciborium::ser::into_writer(&row, &mut row_bytes).expect("encode row");
1030        let mut payload = Vec::new();
1031        write_varint(&mut payload, 0);
1032        write_varint(&mut payload, "é".len() as u64);
1033        payload.extend_from_slice("é".as_bytes());
1034        write_varint(&mut payload, row_bytes.len() as u64);
1035        payload.extend_from_slice(&row_bytes);
1036        write_varint(&mut payload, 1);
1037        write_varint(&mut payload, 0);
1038        write_varint(&mut payload, row_bytes.len() as u64);
1039        payload.extend_from_slice(&row_bytes);
1040        payload.extend_from_slice(&0u32.to_le_bytes());
1041
1042        let mut bytes = Vec::new();
1043        let handle = append_section(&mut bytes, &payload, true).expect("append section");
1044        let error = decode_data_block(&bytes, &handle)
1045            .expect_err("a partial utf-8 prefix should be rejected");
1046        assert!(matches!(
1047            &error,
1048            SstBlockCodecError::Malformed(message)
1049                if message == "shared prefix exceeds previous key"
1050        ));
1051    }
1052
1053    #[test]
1054    fn decoding_rejects_out_of_order_index_entries() {
1055        let entries = vec![
1056            index_entry("inode-00000000000000000009", 0, 1),
1057            index_entry("inode-00000000000000000003", 0, 1),
1058        ];
1059        let (bytes, handle) = encode_index(&entries);
1060
1061        let error =
1062            decode_index_block(&bytes, &handle).expect_err("descending index should be rejected");
1063        assert!(
1064            matches!(&error, SstBlockCodecError::Malformed(message) if message.contains("key order")),
1065            "unexpected error: {error}"
1066        );
1067    }
1068
1069    #[test]
1070    fn decoding_rejects_out_of_order_index_offsets() {
1071        let entries = [index_entry("a", 10, 1), index_entry("b", 5, 1)];
1072        let (bytes, handle) = encode_index(&entries);
1073
1074        let error = decode_index_block(&bytes, &handle)
1075            .expect_err("descending block offsets should be rejected");
1076        assert!(
1077            matches!(&error, SstBlockCodecError::Malformed(message) if message.contains("does not start where")),
1078            "unexpected error: {error}"
1079        );
1080    }
1081
1082    #[test]
1083    fn decoding_rejects_overlapping_index_ranges() {
1084        let entries = [index_entry("a", 10, 5), index_entry("b", 14, 1)];
1085        let (bytes, handle) = encode_index(&entries);
1086
1087        let error = decode_index_block(&bytes, &handle)
1088            .expect_err("overlapping block ranges should be rejected");
1089        assert!(
1090            matches!(&error, SstBlockCodecError::Malformed(message) if message.contains("does not start where")),
1091            "unexpected error: {error}"
1092        );
1093    }
1094
1095    #[test]
1096    fn decoding_rejects_a_gap_between_index_blocks() {
1097        let entries = [index_entry("a", 0, 10), index_entry("b", 20, 1)];
1098        let (bytes, handle) = encode_index(&entries);
1099
1100        let error = decode_index_block(&bytes, &handle)
1101            .expect_err("a gap between block ranges should be rejected");
1102        assert!(
1103            matches!(&error, SstBlockCodecError::Malformed(message) if message.contains("does not start where")),
1104            "unexpected error: {error}"
1105        );
1106    }
1107
1108    #[test]
1109    fn decoding_rejects_a_single_block_range_past_the_integer_edge() {
1110        let entries = [index_entry("a", u64::MAX - 10, 100)];
1111        let (bytes, handle) = encode_index(&entries);
1112
1113        let error = decode_index_block(&bytes, &handle)
1114            .expect_err("an overflowing single range should be rejected");
1115        assert!(
1116            matches!(&error, SstBlockCodecError::Malformed(message) if message.contains("byte range overflows")),
1117            "unexpected error: {error}"
1118        );
1119    }
1120
1121    #[test]
1122    fn decoding_rejects_a_final_block_range_past_the_integer_edge() {
1123        let entries = [
1124            index_entry("a", u64::MAX - 110, 100),
1125            index_entry("b", u64::MAX - 10, 100),
1126        ];
1127        let (bytes, handle) = encode_index(&entries);
1128
1129        let error = decode_index_block(&bytes, &handle)
1130            .expect_err("a trailing overflowing range should be rejected");
1131        assert!(
1132            matches!(&error, SstBlockCodecError::Malformed(message) if message.contains("byte range overflows")),
1133            "unexpected error: {error}"
1134        );
1135    }
1136}