use bytes::{Buf, BufMut, Bytes, BytesMut};
use thiserror::Error;
pub const INDEX_MAGIC: &[u8; 4] = b"S4IX";
pub const INDEX_VERSION: u32 = 3;
pub const INDEX_VERSION_V2: u32 = 2;
pub const INDEX_VERSION_V1: u32 = 1;
pub const HEADER_FIXED_V1: usize = 4 + 4 + 8 + 8 + 8; pub const HEADER_FIXED_V2: usize = HEADER_FIXED_V1 + 8 + 4; pub const SSE_BLOCK_V3: usize = 4 + 4 + 2 + 8 + 8 + 4; #[deprecated(
since = "0.8.16",
note = "INDEX_HEADER_BYTES was an off-by-4 typo; use HEADER_FIXED_V1 or HEADER_FIXED_V2 instead"
)]
pub const INDEX_HEADER_BYTES: usize = HEADER_FIXED_V2;
pub const ENTRY_BYTES: usize = 8 + 8 + 8 + 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameIndexEntry {
pub original_offset: u64,
pub original_size: u64,
pub compressed_offset: u64,
pub compressed_size: u64,
}
impl FrameIndexEntry {
pub fn original_end(&self) -> u64 {
self.original_offset.saturating_add(self.original_size)
}
pub fn compressed_end(&self) -> u64 {
self.compressed_offset.saturating_add(self.compressed_size)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FrameIndex {
pub total_padded_size: u64,
pub entries: Vec<FrameIndexEntry>,
pub source_etag: Option<String>,
pub source_compressed_size: Option<u64>,
pub sse_v3: Option<SseChunkBinding>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SseChunkBinding {
pub enc_chunk_size: u32,
pub enc_chunk_count: u32,
pub enc_key_id: u16,
pub enc_salt: [u8; 8],
pub enc_plaintext_len: u64,
pub enc_header_bytes: u32,
}
impl FrameIndex {
pub fn total_original_size(&self) -> u64 {
self.entries.last().map(|e| e.original_end()).unwrap_or(0)
}
pub fn lookup_range(&self, start: u64, end_exclusive: u64) -> Option<RangePlan> {
if self.entries.is_empty() || start >= end_exclusive {
return None;
}
let total = self.total_original_size();
if start >= total {
return None;
}
let clamped_end = end_exclusive.min(total);
let first_idx = match self.entries.binary_search_by(|e| {
if e.original_end() <= start {
std::cmp::Ordering::Less
} else if e.original_offset > start {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}) {
Ok(i) => i,
Err(_) => return None,
};
let last_inclusive = clamped_end - 1;
let last_idx = match self.entries.binary_search_by(|e| {
if e.original_end() <= last_inclusive {
std::cmp::Ordering::Less
} else if e.original_offset > last_inclusive {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}) {
Ok(i) => i,
Err(_) => return None,
};
let byte_start = self.entries[first_idx].compressed_offset;
let byte_end_exclusive = self.entries[last_idx].compressed_end();
Some(RangePlan {
first_frame_idx: first_idx,
last_frame_idx_inclusive: last_idx,
byte_start,
byte_end_exclusive,
slice_start_in_combined: start - self.entries[first_idx].original_offset,
slice_end_in_combined: clamped_end - self.entries[first_idx].original_offset,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangePlan {
pub first_frame_idx: usize,
pub last_frame_idx_inclusive: usize,
pub byte_start: u64,
pub byte_end_exclusive: u64,
pub slice_start_in_combined: u64,
pub slice_end_in_combined: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptedRangePlan {
pub chunk_idx_start: u32,
pub chunk_idx_last_inclusive: u32,
pub enc_byte_start: u64,
pub enc_byte_end_exclusive: u64,
pub pre_encrypt_slice_start_in_concat: u64,
pub pre_encrypt_slice_end_in_concat: u64,
}
impl SseChunkBinding {
pub fn enc_chunk_stride(&self) -> u64 {
self.enc_chunk_size as u64 + 16
}
pub fn enc_chunk_on_disk_size(&self, chunk_idx: u32) -> u64 {
if chunk_idx + 1 < self.enc_chunk_count {
self.enc_chunk_stride()
} else {
let prior = (chunk_idx as u64).saturating_mul(self.enc_chunk_size as u64);
let final_pt = self.enc_plaintext_len.saturating_sub(prior);
final_pt + 16
}
}
pub fn enc_chunk_byte_offset(&self, chunk_idx: u32) -> u64 {
self.enc_header_bytes as u64 + (chunk_idx as u64).saturating_mul(self.enc_chunk_stride())
}
}
impl FrameIndex {
pub fn encrypted_lookup(&self, plan: &RangePlan) -> Option<EncryptedRangePlan> {
let sse = self.sse_v3.as_ref()?;
if sse.enc_chunk_size == 0 || sse.enc_chunk_count == 0 {
return None;
}
if plan.byte_end_exclusive > sse.enc_plaintext_len
|| plan.byte_start >= plan.byte_end_exclusive
{
return None;
}
let chunk_size = sse.enc_chunk_size as u64;
let chunk_idx_start_u64 = plan.byte_start / chunk_size;
let chunk_idx_last_u64 = (plan.byte_end_exclusive - 1) / chunk_size;
if chunk_idx_last_u64 >= sse.enc_chunk_count as u64 {
return None;
}
let chunk_idx_start = chunk_idx_start_u64 as u32;
let chunk_idx_last_inclusive = chunk_idx_last_u64 as u32;
let enc_byte_start = sse.enc_chunk_byte_offset(chunk_idx_start);
let enc_byte_end_exclusive = sse.enc_chunk_byte_offset(chunk_idx_last_inclusive)
+ sse.enc_chunk_on_disk_size(chunk_idx_last_inclusive);
let pre_encrypt_slice_start_in_concat =
plan.byte_start - (chunk_idx_start as u64) * chunk_size;
let pre_encrypt_slice_end_in_concat =
plan.byte_end_exclusive - (chunk_idx_start as u64) * chunk_size;
Some(EncryptedRangePlan {
chunk_idx_start,
chunk_idx_last_inclusive,
enc_byte_start,
enc_byte_end_exclusive,
pre_encrypt_slice_start_in_concat,
pre_encrypt_slice_end_in_concat,
})
}
}
#[derive(Debug, Error)]
pub enum IndexError {
#[error("index too short: {0} bytes")]
TooShort(usize),
#[error("bad index magic: {got:?}")]
BadMagic { got: [u8; 4] },
#[error("unsupported index version {0} (this build supports {INDEX_VERSION})")]
UnsupportedVersion(u32),
#[error("entry count {claimed} doesn't match buffer remaining {remaining}")]
EntryCountMismatch { claimed: u64, remaining: usize },
#[error(
"frame index entry overflows: original_offset={ooff}, original_size={osize}, \
compressed_offset={coff}, compressed_size={csize}"
)]
EntryOverflow {
ooff: u64,
osize: u64,
coff: u64,
csize: u64,
},
#[error("frame index entry count {got} exceeds MAX_FRAMES={max}")]
TooManyFrames { got: u64, max: u64 },
#[error("sidecar etag_len {got} exceeds MAX_ETAG_BYTES={max}")]
EtagTooLong { got: u32, max: u32 },
#[error(
"frame index entries out of order: prev_original_end={prev_original_end}, \
curr_original_offset={curr_original_offset}, prev_compressed_end={prev_compressed_end}, \
curr_compressed_offset={curr_compressed_offset}"
)]
NonMonotonicEntries {
prev_original_end: u64,
curr_original_offset: u64,
prev_compressed_end: u64,
curr_compressed_offset: u64,
},
}
pub const MAX_FRAMES: u64 = 16 * 1024 * 1024;
pub const MAX_ETAG_BYTES: u32 = 4096;
pub fn encode_index(idx: &FrameIndex) -> Bytes {
let etag_bytes = idx.source_etag.as_deref().unwrap_or("").as_bytes();
let (version, fixed_header) = if idx.sse_v3.is_some() {
(INDEX_VERSION, HEADER_FIXED_V2 + SSE_BLOCK_V3)
} else {
(INDEX_VERSION_V2, HEADER_FIXED_V2)
};
let mut buf =
BytesMut::with_capacity(fixed_header + etag_bytes.len() + idx.entries.len() * ENTRY_BYTES);
buf.put_slice(INDEX_MAGIC);
buf.put_u32_le(version);
buf.put_u64_le(idx.entries.len() as u64);
buf.put_u64_le(idx.total_original_size());
buf.put_u64_le(idx.total_padded_size);
buf.put_u64_le(idx.source_compressed_size.unwrap_or(0));
buf.put_u32_le(etag_bytes.len() as u32);
buf.put_slice(etag_bytes);
if let Some(sse) = idx.sse_v3.as_ref() {
buf.put_u32_le(sse.enc_chunk_size);
buf.put_u32_le(sse.enc_chunk_count);
buf.put_u16_le(sse.enc_key_id);
buf.put_slice(&sse.enc_salt);
buf.put_u64_le(sse.enc_plaintext_len);
buf.put_u32_le(sse.enc_header_bytes);
}
for e in &idx.entries {
buf.put_u64_le(e.original_offset);
buf.put_u64_le(e.original_size);
buf.put_u64_le(e.compressed_offset);
buf.put_u64_le(e.compressed_size);
}
buf.freeze()
}
#[doc(hidden)]
pub fn encode_index_v1_for_test(idx: &FrameIndex) -> Bytes {
let mut buf = BytesMut::with_capacity(HEADER_FIXED_V1 + idx.entries.len() * ENTRY_BYTES);
buf.put_slice(INDEX_MAGIC);
buf.put_u32_le(INDEX_VERSION_V1);
buf.put_u64_le(idx.entries.len() as u64);
buf.put_u64_le(idx.total_original_size());
buf.put_u64_le(idx.total_padded_size);
for e in &idx.entries {
buf.put_u64_le(e.original_offset);
buf.put_u64_le(e.original_size);
buf.put_u64_le(e.compressed_offset);
buf.put_u64_le(e.compressed_size);
}
buf.freeze()
}
pub fn decode_index(mut input: Bytes) -> Result<FrameIndex, IndexError> {
if input.len() < HEADER_FIXED_V1 {
return Err(IndexError::TooShort(input.len()));
}
let mut magic = [0u8; 4];
magic.copy_from_slice(&input[..4]);
if &magic != INDEX_MAGIC {
return Err(IndexError::BadMagic { got: magic });
}
input.advance(4);
let version = input.get_u32_le();
let n = input.get_u64_le();
let _total_original = input.get_u64_le();
let total_padded_size = input.get_u64_le();
if n > MAX_FRAMES {
return Err(IndexError::TooManyFrames {
got: n,
max: MAX_FRAMES,
});
}
let (source_compressed_size, source_etag, sse_v3) = match version {
v if v == INDEX_VERSION_V1 => (None, None, None),
v if v == INDEX_VERSION_V2 || v == INDEX_VERSION => {
if input.len() < 8 + 4 {
return Err(IndexError::TooShort(input.len()));
}
let scs = input.get_u64_le();
let etag_len_u32 = input.get_u32_le();
if etag_len_u32 > MAX_ETAG_BYTES {
return Err(IndexError::EtagTooLong {
got: etag_len_u32,
max: MAX_ETAG_BYTES,
});
}
let etag_len = etag_len_u32 as usize;
if input.len() < etag_len {
return Err(IndexError::TooShort(input.len()));
}
let etag_bytes = input.split_to(etag_len);
let etag = if etag_len == 0 {
None
} else {
std::str::from_utf8(&etag_bytes).ok().map(str::to_owned)
};
let sse_binding = if v == INDEX_VERSION {
if input.len() < SSE_BLOCK_V3 {
return Err(IndexError::TooShort(input.len()));
}
let enc_chunk_size = input.get_u32_le();
let enc_chunk_count = input.get_u32_le();
let enc_key_id = input.get_u16_le();
let mut enc_salt = [0u8; 8];
input.copy_to_slice(&mut enc_salt);
let enc_plaintext_len = input.get_u64_le();
let enc_header_bytes = input.get_u32_le();
if enc_chunk_size == 0 || enc_chunk_count == 0 {
None
} else {
Some(SseChunkBinding {
enc_chunk_size,
enc_chunk_count,
enc_key_id,
enc_salt,
enc_plaintext_len,
enc_header_bytes,
})
}
} else {
None
};
(if scs == 0 { None } else { Some(scs) }, etag, sse_binding)
}
other => return Err(IndexError::UnsupportedVersion(other)),
};
let expected_remaining = (n as usize).saturating_mul(ENTRY_BYTES);
if input.len() != expected_remaining {
return Err(IndexError::EntryCountMismatch {
claimed: n,
remaining: input.len(),
});
}
const BOOTSTRAP_ENTRIES: usize = 4096;
let initial_cap = (n as usize).min(BOOTSTRAP_ENTRIES);
let mut entries = Vec::with_capacity(initial_cap);
for _ in 0..n {
let original_offset = input.get_u64_le();
let original_size = input.get_u64_le();
let compressed_offset = input.get_u64_le();
let compressed_size = input.get_u64_le();
if original_offset.checked_add(original_size).is_none()
|| compressed_offset.checked_add(compressed_size).is_none()
{
return Err(IndexError::EntryOverflow {
ooff: original_offset,
osize: original_size,
coff: compressed_offset,
csize: compressed_size,
});
}
entries.push(FrameIndexEntry {
original_offset,
original_size,
compressed_offset,
compressed_size,
});
}
for win in entries.windows(2) {
let prev = &win[0];
let curr = &win[1];
if curr.original_offset < prev.original_end()
|| curr.compressed_offset < prev.compressed_end()
{
return Err(IndexError::NonMonotonicEntries {
prev_original_end: prev.original_end(),
curr_original_offset: curr.original_offset,
prev_compressed_end: prev.compressed_end(),
curr_compressed_offset: curr.compressed_offset,
});
}
}
Ok(FrameIndex {
total_padded_size,
entries,
source_etag,
source_compressed_size,
sse_v3,
})
}
pub fn build_index_from_body(body: &Bytes) -> Result<FrameIndex, crate::multipart::FrameError> {
let mut entries = Vec::new();
let mut original_off: u64 = 0;
let mut cursor = 0usize;
let mut iter_buf = body.clone();
while cursor < body.len() {
if cursor + 4 <= body.len() && &body[cursor..cursor + 4] == crate::multipart::PADDING_MAGIC
{
if cursor + crate::multipart::PADDING_HEADER_BYTES > body.len() {
break;
}
let pad_len = u64::from_le_bytes(body[cursor + 4..cursor + 12].try_into().unwrap());
let pad_len_usize = usize::try_from(pad_len)
.map_err(|_| crate::multipart::FrameError::PayloadTooLarge(pad_len))?;
let next_cursor = cursor
.checked_add(crate::multipart::PADDING_HEADER_BYTES)
.and_then(|n| n.checked_add(pad_len_usize))
.ok_or(crate::multipart::FrameError::PayloadTooLarge(pad_len))?;
cursor = next_cursor;
if cursor > body.len() {
break;
}
iter_buf = body.slice(cursor..);
continue;
}
if cursor + crate::multipart::FRAME_HEADER_BYTES > body.len() {
break;
}
let (header, _payload, rest) = crate::multipart::read_frame(iter_buf.clone())?;
let compressed_size_usize = usize::try_from(header.compressed_size)
.map_err(|_| crate::multipart::FrameError::PayloadTooLarge(header.compressed_size))?;
let frame_total = crate::multipart::FRAME_HEADER_BYTES
.checked_add(compressed_size_usize)
.ok_or(crate::multipart::FrameError::PayloadTooLarge(
header.compressed_size,
))?;
entries.push(FrameIndexEntry {
original_offset: original_off,
original_size: header.original_size,
compressed_offset: cursor as u64,
compressed_size: frame_total as u64,
});
original_off = original_off.checked_add(header.original_size).ok_or(
crate::multipart::FrameError::PayloadTooLarge(header.original_size),
)?;
cursor = cursor.checked_add(frame_total).ok_or(
crate::multipart::FrameError::PayloadTooLarge(header.compressed_size),
)?;
iter_buf = rest;
}
Ok(FrameIndex {
total_padded_size: body.len() as u64,
entries,
source_etag: None,
source_compressed_size: None,
sse_v3: None,
})
}
pub fn sidecar_key(object_key: &str) -> String {
format!("{object_key}{SIDECAR_SUFFIX}")
}
pub const SIDECAR_SUFFIX: &str = ".s4index";
pub fn is_reserved_sidecar_key(object_key: &str) -> bool {
object_key.ends_with(SIDECAR_SUFFIX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CodecKind;
use crate::multipart::{FrameHeader, pad_to_minimum, write_frame};
fn sample_index() -> FrameIndex {
FrameIndex {
total_padded_size: 200,
entries: vec![
FrameIndexEntry {
original_offset: 0,
original_size: 100,
compressed_offset: 0,
compressed_size: 50,
},
FrameIndexEntry {
original_offset: 100,
original_size: 80,
compressed_offset: 60, compressed_size: 40,
},
FrameIndexEntry {
original_offset: 180,
original_size: 50,
compressed_offset: 100,
compressed_size: 30,
},
],
source_etag: None,
source_compressed_size: None,
sse_v3: None,
}
}
#[test]
fn encode_decode_roundtrip() {
let idx = sample_index();
let bytes = encode_index(&idx);
let decoded = decode_index(bytes).unwrap();
assert_eq!(decoded, idx);
}
#[test]
fn encode_decode_roundtrip_v2_with_source_binding() {
let mut idx = sample_index();
idx.source_etag = Some("\"deadbeefcafe\"".into());
idx.source_compressed_size = Some(987_654);
let bytes = encode_index(&idx);
assert_eq!(&bytes[..4], INDEX_MAGIC);
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(
version, INDEX_VERSION_V2,
"writer must emit v2 when no SSE binding is attached"
);
let decoded = decode_index(bytes).unwrap();
assert_eq!(decoded, idx);
}
#[test]
fn encode_decode_roundtrip_v3_with_sse_binding() {
let mut idx = sample_index();
idx.source_etag = Some("\"abc123\"".into());
idx.source_compressed_size = Some(2048);
idx.sse_v3 = Some(SseChunkBinding {
enc_chunk_size: 1024,
enc_chunk_count: 2,
enc_key_id: 7,
enc_salt: [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88],
enc_plaintext_len: 2048,
enc_header_bytes: 24,
});
let bytes = encode_index(&idx);
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(
version, INDEX_VERSION,
"writer must emit v3 when SSE binding is attached"
);
let decoded = decode_index(bytes).unwrap();
assert_eq!(decoded, idx);
assert!(decoded.sse_v3.is_some());
}
#[test]
fn v3_with_zero_enc_chunk_size_decodes_as_no_sse() {
let mut bytes_mut = bytes::BytesMut::new();
bytes_mut.put_slice(INDEX_MAGIC);
bytes_mut.put_u32_le(INDEX_VERSION); bytes_mut.put_u64_le(0); bytes_mut.put_u64_le(0); bytes_mut.put_u64_le(0); bytes_mut.put_u64_le(0); bytes_mut.put_u32_le(0); bytes_mut.put_u32_le(0); bytes_mut.put_u32_le(0); bytes_mut.put_u16_le(0); bytes_mut.put_slice(&[0u8; 8]); bytes_mut.put_u64_le(0); bytes_mut.put_u32_le(0); let decoded = decode_index(bytes_mut.freeze()).unwrap();
assert!(decoded.sse_v3.is_none());
}
#[test]
fn v2_sidecar_decoded_by_v3_reader_with_no_sse_binding() {
let mut idx = sample_index();
idx.source_etag = Some("\"v2-only\"".into());
idx.source_compressed_size = Some(123);
let v2_bytes = encode_index(&idx); let v2_version = u32::from_le_bytes(v2_bytes[4..8].try_into().unwrap());
assert_eq!(v2_version, INDEX_VERSION_V2);
let decoded = decode_index(v2_bytes).unwrap();
assert!(decoded.sse_v3.is_none());
assert_eq!(decoded.source_etag.as_deref(), Some("\"v2-only\""));
}
#[test]
fn encrypted_lookup_single_chunk() {
let idx = FrameIndex {
total_padded_size: 0,
entries: vec![],
source_etag: None,
source_compressed_size: None,
sse_v3: Some(SseChunkBinding {
enc_chunk_size: 1024,
enc_chunk_count: 4,
enc_key_id: 1,
enc_salt: [0u8; 8],
enc_plaintext_len: 4096,
enc_header_bytes: 24,
}),
};
let plan = RangePlan {
first_frame_idx: 0,
last_frame_idx_inclusive: 0,
byte_start: 100,
byte_end_exclusive: 500,
slice_start_in_combined: 0,
slice_end_in_combined: 400,
};
let enc = idx.encrypted_lookup(&plan).unwrap();
assert_eq!(enc.chunk_idx_start, 0);
assert_eq!(enc.chunk_idx_last_inclusive, 0);
assert_eq!(enc.enc_byte_start, 24);
assert_eq!(enc.enc_byte_end_exclusive, 24 + 1040);
assert_eq!(enc.pre_encrypt_slice_start_in_concat, 100);
assert_eq!(enc.pre_encrypt_slice_end_in_concat, 500);
}
#[test]
fn encrypted_lookup_crossing_chunk_boundary() {
let idx = FrameIndex {
total_padded_size: 0,
entries: vec![],
source_etag: None,
source_compressed_size: None,
sse_v3: Some(SseChunkBinding {
enc_chunk_size: 1024,
enc_chunk_count: 4,
enc_key_id: 1,
enc_salt: [0u8; 8],
enc_plaintext_len: 4096,
enc_header_bytes: 24,
}),
};
let plan = RangePlan {
first_frame_idx: 0,
last_frame_idx_inclusive: 0,
byte_start: 900, byte_end_exclusive: 1200, slice_start_in_combined: 0,
slice_end_in_combined: 300,
};
let enc = idx.encrypted_lookup(&plan).unwrap();
assert_eq!(enc.chunk_idx_start, 0);
assert_eq!(enc.chunk_idx_last_inclusive, 1);
assert_eq!(enc.enc_byte_start, 24);
assert_eq!(enc.enc_byte_end_exclusive, 24 + 2 * 1040);
assert_eq!(enc.pre_encrypt_slice_start_in_concat, 900);
assert_eq!(enc.pre_encrypt_slice_end_in_concat, 1200);
}
#[test]
fn encrypted_lookup_final_chunk_uses_residual_size() {
let idx = FrameIndex {
total_padded_size: 0,
entries: vec![],
source_etag: None,
source_compressed_size: None,
sse_v3: Some(SseChunkBinding {
enc_chunk_size: 1024,
enc_chunk_count: 4,
enc_key_id: 1,
enc_salt: [0u8; 8],
enc_plaintext_len: 3572,
enc_header_bytes: 24,
}),
};
let plan = RangePlan {
first_frame_idx: 0,
last_frame_idx_inclusive: 0,
byte_start: 3100,
byte_end_exclusive: 3500,
slice_start_in_combined: 0,
slice_end_in_combined: 400,
};
let enc = idx.encrypted_lookup(&plan).unwrap();
assert_eq!(enc.chunk_idx_start, 3);
assert_eq!(enc.chunk_idx_last_inclusive, 3);
let expected_start = 24 + 3 * 1040;
assert_eq!(enc.enc_byte_start, expected_start);
assert_eq!(enc.enc_byte_end_exclusive, expected_start + 516);
}
#[test]
fn encrypted_lookup_without_binding_returns_none() {
let idx = sample_index();
let plan = RangePlan {
first_frame_idx: 0,
last_frame_idx_inclusive: 0,
byte_start: 0,
byte_end_exclusive: 10,
slice_start_in_combined: 0,
slice_end_in_combined: 10,
};
assert!(idx.encrypted_lookup(&plan).is_none());
}
#[test]
fn sidecar_header_back_compat_old_format_no_source_etag() {
let v2_idx = {
let mut idx = sample_index();
idx.source_etag = Some("\"unused\"".into());
idx.source_compressed_size = Some(42);
idx
};
let v1_bytes = encode_index_v1_for_test(&v2_idx);
let version = u32::from_le_bytes(v1_bytes[4..8].try_into().unwrap());
assert_eq!(version, INDEX_VERSION_V1);
let decoded = decode_index(v1_bytes).expect("v1 sidecar must still decode");
assert_eq!(decoded.entries, v2_idx.entries);
assert_eq!(decoded.total_padded_size, v2_idx.total_padded_size);
assert_eq!(decoded.source_etag, None);
assert_eq!(decoded.source_compressed_size, None);
assert!(decoded.sse_v3.is_none());
}
#[test]
fn lookup_range_within_single_frame() {
let idx = sample_index();
let plan = idx.lookup_range(10, 50).unwrap();
assert_eq!(plan.first_frame_idx, 0);
assert_eq!(plan.last_frame_idx_inclusive, 0);
assert_eq!(plan.byte_start, 0);
assert_eq!(plan.byte_end_exclusive, 50); assert_eq!(plan.slice_start_in_combined, 10);
assert_eq!(plan.slice_end_in_combined, 50);
}
#[test]
fn lookup_range_spans_frames() {
let idx = sample_index();
let plan = idx.lookup_range(50, 150).unwrap();
assert_eq!(plan.first_frame_idx, 0);
assert_eq!(plan.last_frame_idx_inclusive, 1);
assert_eq!(plan.byte_start, 0);
assert_eq!(plan.byte_end_exclusive, 100); assert_eq!(plan.slice_start_in_combined, 50);
assert_eq!(plan.slice_end_in_combined, 150);
}
#[test]
fn lookup_range_at_end_clamps() {
let idx = sample_index();
let plan = idx.lookup_range(200, 1000).unwrap();
assert_eq!(plan.first_frame_idx, 2);
assert_eq!(plan.last_frame_idx_inclusive, 2);
assert_eq!(plan.byte_start, 100);
assert_eq!(plan.byte_end_exclusive, 130);
}
#[test]
fn lookup_range_out_of_bounds_returns_none() {
let idx = sample_index();
assert!(idx.lookup_range(500, 600).is_none());
}
#[test]
fn build_index_from_real_body_skips_padding() {
let mut buf = BytesMut::new();
let p1 = Bytes::from_static(b"AAAA");
write_frame(
&mut buf,
FrameHeader {
codec: CodecKind::Passthrough,
original_size: 100,
compressed_size: p1.len() as u64,
crc32c: 0,
},
&p1,
);
let frame1_end = buf.len();
pad_to_minimum(&mut buf, 5000);
let pad_end = buf.len();
let p2 = Bytes::from_static(b"BBBB");
write_frame(
&mut buf,
FrameHeader {
codec: CodecKind::Passthrough,
original_size: 80,
compressed_size: p2.len() as u64,
crc32c: 0,
},
&p2,
);
let idx = build_index_from_body(&buf.freeze()).unwrap();
assert_eq!(idx.entries.len(), 2);
assert_eq!(idx.entries[0].original_offset, 0);
assert_eq!(idx.entries[0].compressed_offset, 0);
assert_eq!(idx.entries[0].original_size, 100);
assert_eq!(idx.entries[0].compressed_size, frame1_end as u64);
assert_eq!(idx.entries[1].original_offset, 100);
assert_eq!(idx.entries[1].compressed_offset, pad_end as u64);
assert_eq!(idx.entries[1].original_size, 80);
assert_eq!(idx.total_original_size(), 180);
}
}