use crate::table::block::BlockOffset;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub fn encode_block_layouts(out: &mut Vec<u8>, layouts: &[(BlockOffset, Vec<u32>)]) {
#[expect(
clippy::cast_possible_truncation,
reason = "data-block count is bounded well within u32"
)]
let count = layouts.len() as u32;
out.extend_from_slice(&count.to_le_bytes());
for (offset, ends) in layouts {
out.extend_from_slice(&offset.0.to_le_bytes());
#[expect(
clippy::cast_possible_truncation,
reason = "inner-block count is bounded well within u32"
)]
let inner = ends.len() as u32;
out.extend_from_slice(&inner.to_le_bytes());
for &end in ends {
out.extend_from_slice(&end.to_le_bytes());
}
}
}
#[derive(Debug, Default, Clone)]
pub struct BlockLayoutMap {
entries: Vec<(u64, Vec<u32>)>,
}
impl BlockLayoutMap {
pub fn decode(bytes: &[u8]) -> crate::Result<Self> {
const ERR: crate::Error = crate::Error::InvalidHeader("BlockLayout");
fn take<'a>(r: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
if r.len() < n {
return None;
}
let (head, tail) = r.split_at(n);
*r = tail;
Some(head)
}
fn read_u32(r: &mut &[u8]) -> Option<u32> {
let b: [u8; 4] = take(r, 4)?.try_into().ok()?;
Some(u32::from_le_bytes(b))
}
fn read_u64(r: &mut &[u8]) -> Option<u64> {
let b: [u8; 8] = take(r, 8)?.try_into().ok()?;
Some(u64::from_le_bytes(b))
}
let mut r = bytes;
let count = read_u32(&mut r).ok_or(ERR)?;
let mut entries = Vec::with_capacity(count as usize);
let mut prev: Option<u64> = None;
for _ in 0..count {
let offset = read_u64(&mut r).ok_or(ERR)?;
if prev.is_some_and(|p| offset <= p) {
return Err(ERR);
}
prev = Some(offset);
let inner = read_u32(&mut r).ok_or(ERR)?;
if inner < 2 {
return Err(ERR);
}
let mut ends = Vec::with_capacity(inner as usize);
for _ in 0..inner {
ends.push(read_u32(&mut r).ok_or(ERR)?);
}
entries.push((offset, ends));
}
Ok(Self { entries })
}
#[cfg(feature = "zstd")]
pub fn ends_for(&self, offset: u64) -> Option<&[u32]> {
let idx = self
.entries
.binary_search_by_key(&offset, |(o, _)| *o)
.ok()?;
self.entries.get(idx).map(|(_, ends)| ends.as_slice())
}
pub fn len(&self) -> usize {
self.entries.len()
}
#[cfg(all(test, feature = "zstd"))]
pub(crate) fn offsets(&self) -> Vec<u64> {
self.entries.iter().map(|(o, _)| *o).collect()
}
}
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "consumed by range-query partial decode (next slice)"
)
)]
pub fn map_byte_range_to_blocks(ends: &[u32], lower: u32, upper: u32) -> Option<(u32, u32)> {
let total = *ends.last()?;
if lower >= total {
return None;
}
let start = ends.partition_point(|&e| e <= lower);
let upper_clamped = upper.min(total);
let last = if upper_clamped == 0 {
0
} else {
ends.partition_point(|&e| e < upper_clamped)
};
let end = (last + 1).min(ends.len());
#[expect(
clippy::cast_possible_truncation,
reason = "inner-block indices bounded by ends.len(), well within u32"
)]
Some((start as u32, end as u32))
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test code")]
mod tests {
use super::*;
#[test]
fn encode_decode_roundtrips_entries() {
let layouts = vec![
(BlockOffset(0), vec![100, 250, 400]),
(BlockOffset(512), vec![80, 160]),
];
let mut buf = Vec::new();
encode_block_layouts(&mut buf, &layouts);
let map = BlockLayoutMap::decode(&buf).expect("decode");
assert_eq!(map.len(), 2);
#[cfg(feature = "zstd")]
{
assert_eq!(map.ends_for(0), Some([100u32, 250, 400].as_slice()));
assert_eq!(map.ends_for(512), Some([80u32, 160].as_slice()));
assert_eq!(map.ends_for(999), None, "unrecorded offset → None");
}
}
#[test]
fn decode_rejects_inner_count_less_than_two() {
let mut buf = Vec::new();
buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&100u32.to_le_bytes()); assert!(
matches!(
BlockLayoutMap::decode(&buf),
Err(crate::Error::InvalidHeader("BlockLayout"))
),
"inner_count < 2 must surface as InvalidHeader(\"BlockLayout\")",
);
}
#[test]
fn decode_rejects_non_ascending_offsets() {
let layouts = vec![
(BlockOffset(512), vec![80, 160]),
(BlockOffset(0), vec![100, 250]),
];
let mut buf = Vec::new();
encode_block_layouts(&mut buf, &layouts);
assert!(
BlockLayoutMap::decode(&buf).is_err(),
"non-ascending offsets must be rejected",
);
}
#[test]
fn empty_layouts_decode_to_empty_map() {
let mut buf = Vec::new();
encode_block_layouts(&mut buf, &[]);
let map = BlockLayoutMap::decode(&buf).expect("decode empty");
assert_eq!(map.len(), 0);
#[cfg(feature = "zstd")]
assert_eq!(map.ends_for(0), None);
}
#[test]
fn map_byte_range_single_block_subset() {
let ends = [100u32, 250, 400];
assert_eq!(map_byte_range_to_blocks(&ends, 10, 50), Some((0, 1)));
assert_eq!(map_byte_range_to_blocks(&ends, 120, 200), Some((1, 2)));
assert_eq!(map_byte_range_to_blocks(&ends, 50, 150), Some((0, 2)));
assert_eq!(map_byte_range_to_blocks(&ends, 120, 400), Some((1, 3)));
assert_eq!(map_byte_range_to_blocks(&ends, 0, 400), Some((0, 3)));
}
#[test]
fn map_byte_range_past_end_returns_none() {
let ends = [100u32, 250, 400];
assert_eq!(map_byte_range_to_blocks(&ends, 400, 500), None);
assert_eq!(map_byte_range_to_blocks(&[], 0, 10), None);
}
#[test]
fn map_byte_range_boundary_is_exclusive_end() {
let ends = [100u32, 250, 400];
assert_eq!(map_byte_range_to_blocks(&ends, 0, 100), Some((0, 1)));
assert_eq!(map_byte_range_to_blocks(&ends, 100, 250), Some((1, 2)));
}
}