use crate::error::{Error, Result};
use crate::parser::vint::{parse_vuint, zigzag_decode};
const NB_DELETION_TIME_SIZE: usize = 12;
const INDEX_INFO_WIDTH_BASE: i64 = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PromotedDeletionTime {
pub local_deletion_time: i32,
pub marked_for_delete_at: i64,
}
impl PromotedDeletionTime {
pub fn is_live(&self) -> bool {
self.local_deletion_time == i32::MAX && self.marked_for_delete_at == i64::MIN
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedIndexInfo {
pub first_name: Vec<u8>,
pub last_name: Vec<u8>,
pub offset: u64,
pub width: u64,
pub end_open_marker: Option<(i32, i64)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedPromotedIndex {
pub header_length: u64,
pub deletion_time: PromotedDeletionTime,
pub count: u32,
pub entries: Vec<DecodedIndexInfo>,
pub offsets: Vec<i32>,
}
pub type PrefixLen<'a> = dyn Fn(&[u8]) -> Result<usize> + 'a;
fn take_be_i32(input: &[u8]) -> Result<(&[u8], i32)> {
if input.len() < 4 {
return Err(Error::Corruption(
"promoted index: truncated i32 (need 4 bytes)".to_string(),
));
}
let (head, rest) = input.split_at(4);
let v = i32::from_be_bytes([head[0], head[1], head[2], head[3]]);
Ok((rest, v))
}
fn take_be_i64(input: &[u8]) -> Result<(&[u8], i64)> {
if input.len() < 8 {
return Err(Error::Corruption(
"promoted index: truncated i64 (need 8 bytes)".to_string(),
));
}
let (head, rest) = input.split_at(8);
let mut buf = [0u8; 8];
buf.copy_from_slice(head);
Ok((rest, i64::from_be_bytes(buf)))
}
fn decode_deletion_time(input: &[u8]) -> Result<(&[u8], PromotedDeletionTime)> {
let (input, ldt) = take_be_i32(input)?;
let (input, mfda) = take_be_i64(input)?;
Ok((
input,
PromotedDeletionTime {
local_deletion_time: ldt,
marked_for_delete_at: mfda,
},
))
}
fn decode_vuint(input: &[u8], what: &str) -> Result<(usize, u64)> {
match parse_vuint(input) {
Ok((rest, value)) => Ok((input.len() - rest.len(), value)),
Err(_) => Err(Error::Corruption(format!(
"promoted index: truncated/invalid VInt while reading {what}"
))),
}
}
fn decode_index_info<'a>(
input: &'a [u8],
prefix_len: &PrefixLen<'_>,
) -> Result<(&'a [u8], DecodedIndexInfo)> {
let first_len = prefix_len(input)?;
if input.len() < first_len {
return Err(Error::Corruption(
"promoted index: firstName clustering prefix exceeds block".to_string(),
));
}
let (first_name, input) = input.split_at(first_len);
let last_len = prefix_len(input)?;
if input.len() < last_len {
return Err(Error::Corruption(
"promoted index: lastName clustering prefix exceeds block".to_string(),
));
}
let (last_name, input) = input.split_at(last_len);
let (consumed, offset) = decode_vuint(input, "IndexInfo.offset")?;
let input = &input[consumed..];
let (consumed, width_delta_u) = decode_vuint(input, "IndexInfo.width")?;
let input = &input[consumed..];
let width = INDEX_INFO_WIDTH_BASE
.checked_add(zigzag_decode(width_delta_u))
.filter(|w| *w >= 0)
.ok_or_else(|| {
Error::Corruption("promoted index: IndexInfo.width out of range".to_string())
})? as u64;
let (&marker_byte, input) = input
.split_first()
.ok_or_else(|| Error::Corruption("promoted index: missing endOpenMarker".to_string()))?;
let (input, end_open_marker) = match marker_byte {
0 => (input, None),
1 => {
let (rest, dt) = decode_deletion_time(input)?;
(
rest,
Some((dt.local_deletion_time, dt.marked_for_delete_at)),
)
}
other => {
return Err(Error::Corruption(format!(
"promoted index: invalid endOpenMarker byte {other:#x} (expected 0 or 1)"
)))
}
};
Ok((
input,
DecodedIndexInfo {
first_name: first_name.to_vec(),
last_name: last_name.to_vec(),
offset,
width,
end_open_marker,
},
))
}
pub fn decode_promoted_index(
payload: &[u8],
prefix_len: &PrefixLen<'_>,
) -> Result<DecodedPromotedIndex> {
let (consumed, header_length) = decode_vuint(payload, "headerLength")?;
let input = &payload[consumed..];
if input.len() < NB_DELETION_TIME_SIZE {
return Err(Error::Corruption(
"promoted index: truncated partition DeletionTime".to_string(),
));
}
let (input, deletion_time) = decode_deletion_time(input)?;
let (consumed, count_u64) = decode_vuint(input, "count")?;
let input = &input[consumed..];
let count = u32::try_from(count_u64)
.map_err(|_| Error::Corruption("promoted index: count too large".to_string()))?;
if count as usize > input.len() {
return Err(Error::Corruption(format!(
"promoted index: declared block count {count} exceeds remaining payload \
length {} bytes (corrupt: each block needs >= 1 byte plus a 4-byte \
trailing offset entry)",
input.len()
)));
}
let mut entries = Vec::with_capacity(count as usize);
let mut info = input;
for i in 0..count {
let (rest, entry) = decode_index_info(info, prefix_len)
.map_err(|e| Error::Corruption(format!("promoted index: IndexInfo block {i}: {e}")))?;
debug_assert!(rest.len() < info.len(), "IndexInfo decode must advance");
entries.push(entry);
info = rest;
}
let mut offsets = Vec::with_capacity(count as usize);
let mut rest = info;
for i in 0..count {
let (next, off) = take_be_i32(rest)
.map_err(|e| Error::Corruption(format!("promoted index: offsets[{i}]: {e}")))?;
offsets.push(off);
rest = next;
}
if !rest.is_empty() {
return Err(Error::Corruption(format!(
"promoted index: {} trailing bytes after offsets array",
rest.len()
)));
}
Ok(DecodedPromotedIndex {
header_length,
deletion_time,
count,
entries,
offsets,
})
}
pub fn peek_block_count(payload: &[u8]) -> Result<u32> {
let (consumed, _header_length) = decode_vuint(payload, "headerLength")?;
let input = &payload[consumed..];
if input.len() < NB_DELETION_TIME_SIZE {
return Err(Error::Corruption(
"promoted index: truncated DeletionTime in peek_block_count".to_string(),
));
}
let input = &input[NB_DELETION_TIME_SIZE..];
let (consumed, count_u64) = decode_vuint(input, "count")?;
let count = u32::try_from(count_u64)
.map_err(|_| Error::Corruption("promoted index: count too large".to_string()))?;
let remaining = &input[consumed..];
if count as usize > remaining.len() {
return Err(Error::Corruption(format!(
"promoted index: declared block count {count} exceeds remaining payload \
length {} bytes (corrupt: each block needs >= 1 byte plus a 4-byte \
trailing offset entry)",
remaining.len()
)));
}
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::vint::encode_vuint;
fn encode_unsigned(value: u64, buf: &mut Vec<u8>) {
buf.extend_from_slice(&encode_vuint(value));
}
fn encode_signed(value: i64, buf: &mut Vec<u8>) {
let zz = ((value << 1) ^ (value >> 63)) as u64;
encode_unsigned(zz, buf);
}
fn fixed_prefix_len(value_bytes: usize) -> impl Fn(&[u8]) -> Result<usize> {
move |slice: &[u8]| {
let need = 1 + value_bytes;
if slice.len() < need {
return Err(Error::Corruption(
"test prefix_len: slice shorter than prefix".to_string(),
));
}
Ok(need)
}
}
fn ck(value_bytes: &[u8]) -> Vec<u8> {
let mut v = vec![0x00u8]; v.extend_from_slice(value_bytes);
v
}
#[allow(clippy::type_complexity)]
fn build_payload(
header_length: u64,
blocks: &[(Vec<u8>, Vec<u8>, u64, u64, Option<(i32, i64)>)],
) -> Vec<u8> {
let mut info: Vec<u8> = Vec::new();
let mut starts: Vec<u32> = Vec::new();
for (first, last, offset, width, marker) in blocks {
starts.push(info.len() as u32);
info.extend_from_slice(first);
info.extend_from_slice(last);
encode_unsigned(*offset, &mut info);
encode_signed((*width as i64) - (64 * 1024), &mut info);
match marker {
None => info.push(0x00),
Some((ldt, mfda)) => {
info.push(0x01);
info.extend_from_slice(&ldt.to_be_bytes());
info.extend_from_slice(&mfda.to_be_bytes());
}
}
}
let mut payload = Vec::new();
encode_unsigned(header_length, &mut payload);
payload.extend_from_slice(&i32::MAX.to_be_bytes()); payload.extend_from_slice(&i64::MIN.to_be_bytes()); encode_unsigned(blocks.len() as u64, &mut payload);
payload.extend_from_slice(&info);
for s in &starts {
payload.extend_from_slice(&(*s as i32).to_be_bytes());
}
payload
}
#[test]
fn test_zigzag_decode_inverts_encode_signed() {
for v in [-1_000_000i64, -65, -1, 0, 1, 63, 64, 1_000_000] {
let mut buf = Vec::new();
encode_signed(v, &mut buf);
let (_, u) = decode_vuint(&buf, "test").unwrap();
assert_eq!(
zigzag_decode(u),
v,
"zigzag_decode must invert encode_signed"
);
}
}
#[test]
fn test_decode_handbuilt_two_blocks_with_end_open_marker() {
let blocks = vec![
(ck(b"aa"), ck(b"am"), 0u64, 50_000u64, None),
(
ck(b"an"),
ck(b"zz"),
70_000u64,
100_000u64,
Some((123, -456)),
),
];
let payload = build_payload(18, &blocks);
let decoded = decode_promoted_index(&payload, &fixed_prefix_len(2)).unwrap();
assert_eq!(decoded.header_length, 18);
assert!(decoded.deletion_time.is_live());
assert_eq!(decoded.count, 2);
assert_eq!(decoded.entries[0].first_name, ck(b"aa"));
assert_eq!(decoded.entries[0].last_name, ck(b"am"));
assert_eq!(decoded.entries[0].offset, 0);
assert_eq!(decoded.entries[0].width, 50_000);
assert_eq!(decoded.entries[0].end_open_marker, None);
assert_eq!(decoded.entries[1].first_name, ck(b"an"));
assert_eq!(decoded.entries[1].last_name, ck(b"zz"));
assert_eq!(decoded.entries[1].offset, 70_000);
assert_eq!(decoded.entries[1].width, 100_000);
assert_eq!(decoded.entries[1].end_open_marker, Some((123, -456)));
assert_eq!(decoded.offsets.len(), 2);
assert_eq!(decoded.offsets[0], 0);
assert!(decoded.offsets[1] > decoded.offsets[0]);
assert!(decoded.entries[1].offset > decoded.entries[0].offset);
}
#[test]
fn test_decode_handbuilt_width_exactly_base_zero_delta() {
let blocks = vec![
(ck(b""), ck(b""), 0u64, 64 * 1024u64, None),
(ck(b""), ck(b""), 64 * 1024u64, 64 * 1024u64, None),
];
let payload = build_payload(18, &blocks);
let decoded = decode_promoted_index(&payload, &fixed_prefix_len(0)).unwrap();
assert_eq!(decoded.entries[0].width, 64 * 1024);
assert_eq!(decoded.entries[1].width, 64 * 1024);
}
#[test]
fn test_truncated_payload_returns_err_not_panic() {
let blocks = vec![
(ck(b"aa"), ck(b"bb"), 0u64, 70_000u64, None),
(ck(b"cc"), ck(b"dd"), 70_000u64, 70_000u64, None),
];
let payload = build_payload(18, &blocks);
for cut in 0..payload.len() {
let res = decode_promoted_index(&payload[..cut], &fixed_prefix_len(2));
assert!(
res.is_err(),
"truncated payload (len {cut}) must return Err, not Ok"
);
}
assert!(decode_promoted_index(&payload, &fixed_prefix_len(2)).is_ok());
}
#[test]
fn test_huge_count_short_body_returns_err_no_alloc_abort() {
let mut payload = Vec::new();
encode_unsigned(18, &mut payload); payload.extend_from_slice(&i32::MAX.to_be_bytes()); payload.extend_from_slice(&i64::MIN.to_be_bytes()); encode_unsigned(u32::MAX as u64, &mut payload); payload.extend_from_slice(&[0x00, 0x01, 0x02]);
let res = decode_promoted_index(&payload, &fixed_prefix_len(0));
assert!(
res.is_err(),
"huge declared count with a short body must return Err (no alloc abort)"
);
match res {
Err(Error::Corruption(msg)) => assert!(
msg.contains("exceeds remaining payload"),
"expected bound-check corruption message, got: {msg}"
),
other => panic!("expected Error::Corruption, got {other:?}"),
}
}
#[test]
fn test_count_exactly_at_payload_bound_then_short_returns_err() {
let mut payload = Vec::new();
encode_unsigned(18, &mut payload);
payload.extend_from_slice(&i32::MAX.to_be_bytes());
payload.extend_from_slice(&i64::MIN.to_be_bytes());
let body = [0x00u8; 4];
encode_unsigned(body.len() as u64, &mut payload); payload.extend_from_slice(&body);
let res = decode_promoted_index(&payload, &fixed_prefix_len(0));
assert!(
res.is_err(),
"count == remaining but unfillable body must Err"
);
}
#[test]
fn test_peek_block_count_schema_free() {
let blocks = vec![
(ck(b"aa"), ck(b"bb"), 0u64, 70_000u64, None),
(ck(b"cc"), ck(b"dd"), 70_000u64, 70_000u64, None),
];
let payload = build_payload(18, &blocks);
assert_eq!(peek_block_count(&payload).unwrap(), 2);
assert!(peek_block_count(&payload[..2]).is_err());
}
#[test]
fn test_peek_block_count_huge_count_short_body_returns_err() {
let mut payload = Vec::new();
encode_unsigned(18, &mut payload); payload.extend_from_slice(&i32::MAX.to_be_bytes()); payload.extend_from_slice(&i64::MIN.to_be_bytes()); encode_unsigned(u32::MAX as u64, &mut payload); payload.extend_from_slice(&[0x00, 0x01, 0x02]);
let res = peek_block_count(&payload);
match res {
Err(Error::Corruption(msg)) => assert!(
msg.contains("exceeds remaining payload"),
"expected bound-check corruption message, got: {msg}"
),
other => panic!("expected Error::Corruption, got {other:?}"),
}
}
#[test]
fn test_trailing_garbage_rejected() {
let blocks = vec![
(ck(b"a"), ck(b"b"), 0u64, 70_000u64, None),
(ck(b"c"), ck(b"d"), 70_000u64, 70_000u64, None),
];
let mut payload = build_payload(18, &blocks);
payload.push(0xFF); let res = decode_promoted_index(&payload, &fixed_prefix_len(1));
assert!(res.is_err(), "trailing bytes must be rejected");
}
#[test]
fn test_invalid_end_open_marker_byte_rejected() {
let mut info: Vec<u8> = Vec::new();
info.extend_from_slice(&ck(b"x")); info.extend_from_slice(&ck(b"y")); encode_unsigned(0, &mut info); encode_signed(0, &mut info); info.push(0x02); let mut payload = Vec::new();
encode_unsigned(18, &mut payload);
payload.extend_from_slice(&i32::MAX.to_be_bytes());
payload.extend_from_slice(&i64::MIN.to_be_bytes());
encode_unsigned(1, &mut payload); payload.extend_from_slice(&info);
payload.extend_from_slice(&0i32.to_be_bytes()); let res = decode_promoted_index(&payload, &fixed_prefix_len(1));
assert!(res.is_err(), "invalid endOpenMarker byte must be rejected");
}
#[cfg(feature = "write-support")]
mod writer_roundtrip {
use super::*;
use crate::storage::sstable::writer::{
serialize_promoted_index_for_test, PromotedIndexBlock,
};
#[test]
fn test_writer_roundtrip_two_blocks_varied_widths() {
let block1 = PromotedIndexBlock {
first_name: ck(b"aa"),
last_name: ck(b"am"),
offset: 0,
width: 50_000, oss50_separator: None,
};
let block2 = PromotedIndexBlock {
first_name: ck(b"an"),
last_name: ck(b"zz"),
offset: 70_000,
width: 100_000, oss50_separator: None,
};
let payload = serialize_promoted_index_for_test(&[block1.clone(), block2.clone()], 4);
let decoded = decode_promoted_index(&payload, &fixed_prefix_len(2)).unwrap();
assert_eq!(decoded.header_length, 18);
assert!(decoded.deletion_time.is_live());
assert_eq!(decoded.count, 2);
assert_eq!(decoded.entries[0].first_name, block1.first_name);
assert_eq!(decoded.entries[0].last_name, block1.last_name);
assert_eq!(decoded.entries[0].offset, block1.offset);
assert_eq!(decoded.entries[0].width, block1.width);
assert_eq!(decoded.entries[0].end_open_marker, None);
assert_eq!(decoded.entries[1].first_name, block2.first_name);
assert_eq!(decoded.entries[1].last_name, block2.last_name);
assert_eq!(decoded.entries[1].offset, block2.offset);
assert_eq!(decoded.entries[1].width, block2.width);
assert_eq!(decoded.offsets[0], 0);
assert!(decoded.offsets[1] > decoded.offsets[0]);
}
#[test]
fn test_writer_roundtrip_width_exactly_base() {
let block1 = PromotedIndexBlock {
first_name: ck(b""),
last_name: ck(b""),
offset: 0,
width: 64 * 1024,
oss50_separator: None,
};
let block2 = PromotedIndexBlock {
first_name: ck(b""),
last_name: ck(b""),
offset: 64 * 1024,
width: 64 * 1024,
oss50_separator: None,
};
let payload = serialize_promoted_index_for_test(&[block1, block2], 4);
let decoded = decode_promoted_index(&payload, &fixed_prefix_len(0)).unwrap();
assert_eq!(decoded.entries[0].width, 64 * 1024);
assert_eq!(decoded.entries[1].width, 64 * 1024);
}
}
}