Skip to main content

loonfs_api/
sst_blocks.rs

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