use super::{SidxEntry, ENTRY_SIZE, SIDX_MAGIC, SIDX_MAGIC_LEGACY_SDX2};
use crate::store::StoreError;
use std::io::{Read, Seek, SeekFrom};
pub(super) const TRAILER_SIZE: u64 = 16;
const TRAILER_SIZE_USIZE: usize = 16;
pub(super) const SIDX_CRC_LEN: u64 = 4;
const SIDX_CRC_LEN_USIZE: usize = 4;
pub(super) fn sidx_crc_len_usize() -> usize {
SIDX_CRC_LEN_USIZE
}
pub(super) struct FooterLayout {
pub(super) string_table_offset: u64,
pub(super) string_table_len: u64,
pub(super) entry_count: usize,
}
pub(super) fn trailer_size_usize() -> usize {
TRAILER_SIZE_USIZE
}
pub(super) fn read_layout<R: Read + Seek>(
reader: &mut R,
segment_id: u64,
) -> Result<Option<FooterLayout>, StoreError> {
let file_len = reader.seek(SeekFrom::End(0)).map_err(StoreError::Io)?;
if file_len < TRAILER_SIZE {
return Ok(None);
}
reader
.seek(SeekFrom::End(-(TRAILER_SIZE as i64)))
.map_err(StoreError::Io)?;
let mut trailer = [0u8; 16];
reader.read_exact(&mut trailer).map_err(StoreError::Io)?;
if &trailer[12..16] != SIDX_MAGIC {
return Ok(None);
}
let string_table_offset = read_trailer_u64(&trailer[0..8], segment_id)?;
let entry_count = read_trailer_u32(&trailer[8..12], segment_id)? as usize;
let entries_block_len = (entry_count as u64)
.checked_mul(ENTRY_SIZE as u64)
.ok_or_else(|| StoreError::CorruptSegment {
segment_id,
detail: "SIDX entry_count × ENTRY_SIZE overflows u64".into(),
})?;
let entries_start = file_len
.checked_sub(TRAILER_SIZE)
.and_then(|n| n.checked_sub(SIDX_CRC_LEN))
.and_then(|n| n.checked_sub(entries_block_len))
.ok_or_else(|| StoreError::CorruptSegment {
segment_id,
detail: "SIDX entry block extends before the beginning of the file".into(),
})?;
if string_table_offset > entries_start {
return Err(StoreError::CorruptSegment {
segment_id,
detail: format!(
"SIDX string_table_offset {string_table_offset} is past entries_start {entries_start}"
),
});
}
let string_table_len = entries_start
.checked_sub(string_table_offset)
.ok_or_else(|| StoreError::CorruptSegment {
segment_id,
detail: "SIDX string table length underflows".into(),
})?;
let covered_end = entries_start
.checked_add(entries_block_len)
.ok_or_else(|| StoreError::CorruptSegment {
segment_id,
detail: "SIDX covered region end overflows u64".into(),
})?;
let covered_len = string_table_len
.checked_add(entries_block_len)
.ok_or_else(|| StoreError::CorruptSegment {
segment_id,
detail: "SIDX covered region length overflows u64".into(),
})?;
reader
.seek(SeekFrom::Start(string_table_offset))
.map_err(StoreError::Io)?;
let mut hasher = crc32fast::Hasher::new();
let mut remaining = covered_len;
let mut chunk = [0u8; 8192];
while remaining > 0 {
let take = usize::try_from(remaining)
.unwrap_or(chunk.len())
.min(chunk.len());
reader
.read_exact(&mut chunk[..take])
.map_err(StoreError::Io)?;
hasher.update(&chunk[..take]);
remaining -= take as u64;
}
reader
.seek(SeekFrom::Start(covered_end))
.map_err(StoreError::Io)?;
let mut stored_crc = [0u8; 4];
reader.read_exact(&mut stored_crc).map_err(StoreError::Io)?;
if hasher.finalize() != u32::from_le_bytes(stored_crc) {
return Ok(None);
}
Ok(Some(FooterLayout {
string_table_offset,
string_table_len,
entry_count,
}))
}
pub(super) fn read_entries_unauthenticated<R: Read + Seek>(
reader: &mut R,
segment_id: u64,
) -> Result<Vec<SidxEntry>, StoreError> {
let file_len = reader.seek(SeekFrom::End(0)).map_err(StoreError::Io)?;
if file_len < TRAILER_SIZE {
return Ok(Vec::new());
}
reader
.seek(SeekFrom::End(-(TRAILER_SIZE as i64)))
.map_err(StoreError::Io)?;
let mut trailer = [0u8; 16];
reader.read_exact(&mut trailer).map_err(StoreError::Io)?;
let magic = &trailer[12..16];
if magic != SIDX_MAGIC && magic != SIDX_MAGIC_LEGACY_SDX2 {
return Ok(Vec::new());
}
let string_table_offset = u64::from_le_bytes([
trailer[0], trailer[1], trailer[2], trailer[3], trailer[4], trailer[5], trailer[6],
trailer[7],
]);
let entry_count = u32::from_le_bytes([trailer[8], trailer[9], trailer[10], trailer[11]]) as u64;
let Some(entries_block_len) = entry_count.checked_mul(ENTRY_SIZE as u64) else {
return Ok(Vec::new());
};
let Some(entries_start) = file_len
.checked_sub(TRAILER_SIZE)
.and_then(|n| n.checked_sub(SIDX_CRC_LEN))
.and_then(|n| n.checked_sub(entries_block_len))
else {
return Ok(Vec::new());
};
if string_table_offset > entries_start {
return Ok(Vec::new());
}
reader
.seek(SeekFrom::Start(entries_start))
.map_err(StoreError::Io)?;
let count = usize::try_from(entry_count).unwrap_or(usize::MAX);
let mut entries = Vec::with_capacity(count.min(1024));
let mut buf = [0u8; ENTRY_SIZE];
for _ in 0..entry_count {
if let Err(e) = reader.read_exact(&mut buf) {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
return Ok(Vec::new());
}
return Err(StoreError::Io(e));
}
match SidxEntry::decode_from(&buf, segment_id) {
Ok(entry) => entries.push(entry),
Err(_) => return Ok(Vec::new()),
}
}
Ok(entries)
}
fn read_trailer_u64(bytes: &[u8], segment_id: u64) -> Result<u64, StoreError> {
let bytes: [u8; 8] = bytes.try_into().map_err(|_| StoreError::CorruptFrame {
segment_id,
offset: 0,
reason: "trailer truncated: string_table_offset bytes not readable".into(),
})?;
Ok(u64::from_le_bytes(bytes))
}
fn read_trailer_u32(bytes: &[u8], segment_id: u64) -> Result<u32, StoreError> {
let bytes: [u8; 4] = bytes.try_into().map_err(|_| StoreError::CorruptFrame {
segment_id,
offset: 0,
reason: "trailer truncated: entry_count bytes not readable".into(),
})?;
Ok(u32::from_le_bytes(bytes))
}
#[cfg(test)]
mod tests {
use super::{
read_entries_unauthenticated, read_layout, sidx_crc_len_usize, trailer_size_usize,
ENTRY_SIZE, SIDX_MAGIC, SIDX_MAGIC_LEGACY_SDX2,
};
use crate::store::StoreError;
use std::io::{Cursor, Read, Seek, SeekFrom};
struct FaultOnEntriesRead {
data: Vec<u8>,
pos: u64,
fault_below: u64,
}
impl Read for FaultOnEntriesRead {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.pos < self.fault_below {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"injected: entries read fault",
));
}
let start = usize::try_from(self.pos)
.expect("pos fits usize")
.min(self.data.len());
let available = &self.data[start..];
let n = available.len().min(buf.len());
buf[..n].copy_from_slice(&available[..n]);
self.pos += n as u64;
Ok(n)
}
}
impl Seek for FaultOnEntriesRead {
fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
let len = self.data.len() as i64;
let target = match from {
SeekFrom::Start(n) => n as i64,
SeekFrom::End(n) => len + n,
SeekFrom::Current(n) => self.pos as i64 + n,
};
if target < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"seek before start",
));
}
self.pos =
u64::try_from(target).expect("seek target is non-negative after the guard above");
Ok(self.pos)
}
}
fn footer_one_zero_entry(magic: &[u8; 4]) -> Vec<u8> {
let mut bytes = vec![0u8; ENTRY_SIZE];
bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(magic);
bytes
}
#[test]
fn footer_constant_helpers_report_the_exact_on_disk_byte_widths() {
assert_eq!(
sidx_crc_len_usize(),
4,
"the SIDX footer CRC is exactly 4 bytes (a u32 LE)"
);
assert_eq!(
trailer_size_usize(),
16,
"the SIDX trailer is exactly 16 bytes (offset8 + count4 + magic4)"
);
}
#[test]
fn read_entries_unauthenticated_returns_empty_for_a_subtrailer_file() {
let bytes = vec![0xEEu8; 10]; let mut cursor = Cursor::new(bytes);
let parsed = read_entries_unauthenticated(&mut cursor, 7)
.expect("a sub-trailer file must yield an empty manifest, never an error");
assert!(
parsed.is_empty(),
"a file too small to hold the 16-byte trailer decodes to zero entries"
);
}
#[test]
fn read_entries_unauthenticated_accepts_the_legacy_sdx2_magic() {
let bytes = footer_one_zero_entry(SIDX_MAGIC_LEGACY_SDX2);
let mut cursor = Cursor::new(bytes);
let parsed = read_entries_unauthenticated(&mut cursor, 7).expect("must not error");
assert_eq!(
parsed.len(),
1,
"a legacy SDX2 footer's entry table must parse (decode_from is CRC-independent)"
);
assert_eq!(
parsed[0].frame_offset, 0,
"the decoded (zeroed) entry preserves its frame_offset field"
);
}
#[test]
fn read_entries_unauthenticated_admits_offset_equal_to_entries_start() {
let bytes = footer_one_zero_entry(SIDX_MAGIC);
let mut cursor = Cursor::new(bytes);
let parsed = read_entries_unauthenticated(&mut cursor, 7).expect("must not error");
assert_eq!(
parsed.len(),
1,
"offset == entries_start (empty string table) is legal geometry and must parse its entry"
);
}
#[test]
fn read_entries_unauthenticated_propagates_a_non_eof_io_fault_on_the_entries_read() {
let data = footer_one_zero_entry(SIDX_MAGIC); let fault_below = (data.len() as u64) - 16; let mut reader = FaultOnEntriesRead {
data,
pos: 0,
fault_below,
};
let err = read_entries_unauthenticated(&mut reader, 7)
.expect_err("a non-EOF IO fault on the entries read must propagate, not fall back");
assert!(
matches!(err, StoreError::Io(ref e) if e.kind() == std::io::ErrorKind::PermissionDenied),
"a real IO error must surface as StoreError::Io(PermissionDenied), not be swallowed as \
an empty manifest; got {err:?}"
);
}
#[test]
fn read_layout_parses_a_trailer_only_file_then_rejects_it() {
let mut buf = Vec::with_capacity(16);
buf.extend_from_slice(&0u64.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(SIDX_MAGIC); assert_eq!(
buf.len(),
16,
"trailer-only fixture must be exactly TRAILER_SIZE"
);
let is_err = read_layout(&mut Cursor::new(buf), 0).is_err();
assert!(
is_err,
"a magic-bearing file of exactly TRAILER_SIZE must be parsed and rejected as \
corrupt (no room for entries/CRC), not short-circuited as Ok(None)"
);
}
}