use crate::bytes::{le_u32, le_u64};
use crate::error::UrnaError;
use crate::layout::SECTION_BLOB_DATA;
pub const BLOB_DATA_PAYLOAD_VERSION: u32 = 1;
const HEADER_SIZE: usize = 12;
const ENTRY_SIZE: usize = 16;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlobDataTable {
pub entries: Vec<(u64, u64)>,
pub data_start: usize,
}
fn malformed(reason: impl Into<String>) -> UrnaError {
UrnaError::MalformedSectionPayload {
section_id: SECTION_BLOB_DATA,
reason: reason.into(),
}
}
pub fn encode_blob_data(blobs: &[Option<&[u8]>]) -> Result<Vec<u8>, UrnaError> {
let data_len: usize = blobs.iter().flatten().map(|b| b.len()).sum();
let mut out = Vec::with_capacity(HEADER_SIZE + blobs.len() * ENTRY_SIZE + data_len);
out.extend_from_slice(&BLOB_DATA_PAYLOAD_VERSION.to_le_bytes());
out.extend_from_slice(&(blobs.len() as u64).to_le_bytes());
let mut offset = 0u64;
for b in blobs {
match b {
Some(bytes) => {
out.extend_from_slice(&offset.to_le_bytes());
out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
offset += bytes.len() as u64;
}
None => out.extend_from_slice(&[0u8; ENTRY_SIZE]),
}
}
for b in blobs.iter().flatten() {
out.extend_from_slice(b);
}
Ok(out)
}
pub fn decode_blob_data_table(bytes: &[u8]) -> Result<BlobDataTable, UrnaError> {
if bytes.len() < HEADER_SIZE {
return Err(malformed("blob_data: payload shorter than header"));
}
let version = le_u32(&bytes[0..4])?;
if version != BLOB_DATA_PAYLOAD_VERSION {
return Err(UrnaError::UnsupportedSectionVersion {
section_id: SECTION_BLOB_DATA,
version,
});
}
let n = le_u64(&bytes[4..12])? as usize;
if n > (bytes.len() - HEADER_SIZE) / ENTRY_SIZE {
return Err(malformed("blob_data: entry count exceeds payload"));
}
let data_start = HEADER_SIZE + n * ENTRY_SIZE;
let data_len = (bytes.len() - data_start) as u64;
let mut entries = Vec::with_capacity(n);
for i in 0..n {
let at = HEADER_SIZE + i * ENTRY_SIZE;
let offset = le_u64(&bytes[at..at + 8])?;
let len = le_u64(&bytes[at + 8..at + 16])?;
let end = offset
.checked_add(len)
.ok_or_else(|| malformed(format!("blob_data: entry {} offset overflow", i)))?;
if end > data_len {
return Err(malformed(format!(
"blob_data: entry {} spans past payload ({} > {})",
i, end, data_len
)));
}
entries.push((offset, len));
}
Ok(BlobDataTable {
entries,
data_start,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_with_gaps() {
let a = b"av1-shard-bytes".as_slice();
let b = b"x".as_slice();
let payload = encode_blob_data(&[Some(a), None, Some(b)]).unwrap();
let table = decode_blob_data_table(&payload).unwrap();
assert_eq!(table.entries, vec![(0, 15), (0, 0), (15, 1)]);
let data = &payload[table.data_start..];
assert_eq!(&data[0..15], a);
assert_eq!(&data[15..16], b);
}
#[test]
fn empty_table_roundtrips() {
let payload = encode_blob_data(&[]).unwrap();
let table = decode_blob_data_table(&payload).unwrap();
assert!(table.entries.is_empty());
assert_eq!(table.data_start, payload.len());
}
#[test]
fn hostile_count_is_rejected() {
let mut payload = encode_blob_data(&[Some(b"abc".as_slice())]).unwrap();
payload[4..12].copy_from_slice(&u64::MAX.to_le_bytes());
assert!(decode_blob_data_table(&payload).is_err());
}
#[test]
fn entry_past_payload_is_rejected() {
let mut payload = encode_blob_data(&[Some(b"abc".as_slice())]).unwrap();
payload[20..28].copy_from_slice(&1000u64.to_le_bytes());
assert!(decode_blob_data_table(&payload).is_err());
}
#[test]
fn bad_version_is_typed() {
let mut payload = encode_blob_data(&[]).unwrap();
payload[0..4].copy_from_slice(&9u32.to_le_bytes());
assert!(matches!(
decode_blob_data_table(&payload),
Err(UrnaError::UnsupportedSectionVersion { .. })
));
}
}