use serde::Serialize;
use crate::utils::wire;
use super::execution::FileExecutionSettingsV1;
use super::{CHUNK_PAYLOAD_CODEC_V1, CatalogError, MAX_NDIM};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkIndexHeaderV1 {
pub magic: &'static [u8; 4],
pub header_len: usize,
pub version: u32,
}
pub const CHUNK_INDEX_HEADER_V1: ChunkIndexHeaderV1 = ChunkIndexHeaderV1 {
magic: b"TIDX",
header_len: 32,
version: 1,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct ChunkIndexEntryV1 {
pub dataset_id: u64,
pub chunk_index: [u64; MAX_NDIM],
pub payload_offset: u64,
pub raw_byte_len: u64,
pub stored_byte_len: u64,
pub codec: u32,
}
impl ChunkIndexEntryV1 {
#[must_use]
pub(crate) fn padded_chunk_index(coord: &[u64], ndim: usize) -> [u64; MAX_NDIM] {
let mut chunk_index = [0u64; MAX_NDIM];
chunk_index[..ndim].copy_from_slice(&coord[..ndim]);
chunk_index
}
pub const WIRE_LEN: usize = 8 + MAX_NDIM * 8 + 8 + 8 + 8 + 4 + 4;
pub const WIRE_PAYLOAD_OFFSET: usize = 8 + MAX_NDIM * 8;
pub const WIRE_RAW_BYTE_LEN_OFFSET: usize = Self::WIRE_PAYLOAD_OFFSET + 8;
pub const WIRE_STORED_BYTE_LEN_OFFSET: usize = Self::WIRE_RAW_BYTE_LEN_OFFSET + 8;
pub const WIRE_CODEC_OFFSET: usize = Self::WIRE_STORED_BYTE_LEN_OFFSET + 8;
pub(super) fn to_bytes(&self) -> [u8; Self::WIRE_LEN] {
let mut out = [0u8; Self::WIRE_LEN];
let mut o = 0usize;
wire::put_u64_le(&mut out, &mut o, self.dataset_id);
for c in &self.chunk_index {
wire::put_u64_le(&mut out, &mut o, *c);
}
wire::put_u64_le(&mut out, &mut o, self.payload_offset);
wire::put_u64_le(&mut out, &mut o, self.raw_byte_len);
wire::put_u64_le(&mut out, &mut o, self.stored_byte_len);
wire::put_u32_le(&mut out, &mut o, self.codec);
wire::put_u32_le(&mut out, &mut o, 0);
debug_assert_eq!(o, Self::WIRE_LEN);
out
}
pub(super) fn from_bytes(data: &[u8], off: usize) -> Result<Self, CatalogError> {
let need = off + Self::WIRE_LEN;
if data.len() < need {
return Err(CatalogError::TooShort {
need,
got: data.len(),
});
}
let mut o = off;
let dataset_id = wire::take_u64_le(data, &mut o);
let mut chunk_index = [0u64; MAX_NDIM];
for slot in &mut chunk_index {
*slot = wire::take_u64_le(data, &mut o);
}
let payload_offset = wire::take_u64_le(data, &mut o);
let raw_byte_len = wire::take_u64_le(data, &mut o);
let stored_byte_len = wire::take_u64_le(data, &mut o);
let codec = wire::take_u32_le(data, &mut o);
let _pad = wire::take_u32_le(data, &mut o);
Ok(Self {
dataset_id,
chunk_index,
payload_offset,
raw_byte_len,
stored_byte_len,
codec,
})
}
}
pub(super) fn parse_chunk_index(
idx_bytes: &[u8],
) -> Result<(Vec<ChunkIndexEntryV1>, FileExecutionSettingsV1), CatalogError> {
let hdr = CHUNK_INDEX_HEADER_V1;
if idx_bytes.len() < hdr.header_len {
return Err(CatalogError::TooShort {
need: hdr.header_len,
got: idx_bytes.len(),
});
}
let mut m = [0u8; 4];
m.copy_from_slice(&idx_bytes[0..4]);
if &m != hdr.magic {
return Err(CatalogError::BadIndexMagic(m));
}
let ver = wire::u32_le_at(idx_bytes, 4);
if ver != hdr.version {
return Err(CatalogError::UnsupportedIndexVersion(ver));
}
let file_execution =
FileExecutionSettingsV1::from_index_header_tail(&idx_bytes[16..hdr.header_len]);
let count = wire::u64_le_at(idx_bytes, 8);
let entries_bytes = count
.checked_mul(ChunkIndexEntryV1::WIRE_LEN as u64)
.ok_or(CatalogError::BadIndexLength {
count,
region: idx_bytes.len() as u64,
})?;
let need =
(hdr.header_len as u64)
.checked_add(entries_bytes)
.ok_or(CatalogError::BadIndexLength {
count,
region: idx_bytes.len() as u64,
})?;
if need != idx_bytes.len() as u64 {
return Err(CatalogError::BadIndexLength {
count,
region: idx_bytes.len() as u64,
});
}
let n = usize::try_from(count).map_err(|_| CatalogError::TooLargeForPlatform {
field: "chunk_index_entry_count",
value: count,
})?;
let mut out = Vec::with_capacity(n);
let mut off = hdr.header_len;
for _ in 0..n {
let e = ChunkIndexEntryV1::from_bytes(idx_bytes, off)?;
off += ChunkIndexEntryV1::WIRE_LEN;
out.push(e);
}
Ok((out, file_execution))
}
pub(super) fn write_chunk_index_header(
out: &mut Vec<u8>,
entry_count: u64,
file_execution: FileExecutionSettingsV1,
) {
let hdr = CHUNK_INDEX_HEADER_V1;
out.extend_from_slice(hdr.magic);
out.extend_from_slice(&hdr.version.to_le_bytes());
out.extend_from_slice(&entry_count.to_le_bytes());
out.resize(hdr.header_len, 0);
file_execution.write_index_header_tail(&mut out[16..hdr.header_len]);
}
pub fn validate_chunk_payloads(
chunks: &[ChunkIndexEntryV1],
file_len: u64,
) -> Result<(), CatalogError> {
for c in chunks {
if !CHUNK_PAYLOAD_CODEC_V1.is_supported(c.codec) {
return Err(CatalogError::UnsupportedCodec { codec: c.codec });
}
if CHUNK_PAYLOAD_CODEC_V1.is_raw(c.codec) && c.raw_byte_len != c.stored_byte_len {
return Err(CatalogError::RawStoredMismatch {
raw: c.raw_byte_len,
stored: c.stored_byte_len,
});
}
ensure_span_in_file(c.payload_offset, c.stored_byte_len, file_len)?;
}
Ok(())
}
fn ensure_span_in_file(offset: u64, len: u64, file_len: u64) -> Result<(), CatalogError> {
match wire::checked_u64_byte_span(offset, len, file_len) {
Ok(()) => Ok(()),
Err(wire::SpanError::AddOverflow) => Err(CatalogError::PayloadOutOfBounds {
file_len,
start: offset,
end: u64::MAX,
}),
Err(wire::SpanError::OutOfBounds { end }) => Err(CatalogError::PayloadOutOfBounds {
file_len,
start: offset,
end,
}),
}
}