use crate::segment::error::{Result, SegmentError};
use crate::segment::to_array;
pub const MAGIC: [u8; 4] = *b"CXSG";
pub const VERSION: u16 = crate::format::SEGMENT_FORMAT_VERSION;
pub const HEADER_SIZE: usize = 4 + 2 + 2 + 8 + 8 + 8 + 8 + 2 + 4 + 1 + 1;
pub const FOOTER_SIZE: usize = 8 + 4 + 4 + 4 + 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SegmentHeader {
pub version: u16,
pub flags: u16,
pub created_at: i64,
pub min_timestamp: i64,
pub max_timestamp: i64,
pub row_count: u64,
pub column_count: u16,
pub series_count: u32,
pub compression: u8,
pub sort_order: u8,
}
impl SegmentHeader {
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(HEADER_SIZE);
buf.extend_from_slice(&MAGIC);
buf.extend_from_slice(&self.version.to_le_bytes());
buf.extend_from_slice(&self.flags.to_le_bytes());
buf.extend_from_slice(&self.created_at.to_le_bytes());
buf.extend_from_slice(&self.min_timestamp.to_le_bytes());
buf.extend_from_slice(&self.max_timestamp.to_le_bytes());
buf.extend_from_slice(&self.row_count.to_le_bytes());
buf.extend_from_slice(&self.column_count.to_le_bytes());
buf.extend_from_slice(&self.series_count.to_le_bytes());
buf.push(self.compression);
buf.push(self.sort_order);
buf
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < HEADER_SIZE {
return Err(SegmentError::CorruptFile {
detail: format!(
"header too short: expected {HEADER_SIZE}, got {}",
data.len()
),
});
}
if data[..4] != MAGIC {
return Err(SegmentError::InvalidMagic);
}
let version = u16::from_le_bytes([data[4], data[5]]);
if version != VERSION {
return Err(SegmentError::UnsupportedVersion {
version,
detail: crate::format::version_mismatch(
"this `.csx` segment",
u64::from(version),
u64::from(VERSION),
),
});
}
let flags = u16::from_le_bytes([data[6], data[7]]);
let created_at = i64::from_le_bytes(to_array!(data[8..16], "header created_at")?);
let min_timestamp = i64::from_le_bytes(to_array!(data[16..24], "header min_timestamp")?);
let max_timestamp = i64::from_le_bytes(to_array!(data[24..32], "header max_timestamp")?);
let row_count = u64::from_le_bytes(to_array!(data[32..40], "header row_count")?);
let column_count = u16::from_le_bytes([data[40], data[41]]);
let series_count = u32::from_le_bytes(to_array!(data[42..46], "header series_count")?);
let compression = data[46];
let sort_order = data[47];
Ok(Self {
version,
flags,
created_at,
min_timestamp,
max_timestamp,
row_count,
column_count,
series_count,
compression,
sort_order,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SegmentFooter {
pub metadata_offset: u64,
pub row_group_count: u32,
pub checksum: u32,
pub metadata_checksum: u32,
}
impl SegmentFooter {
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(FOOTER_SIZE);
buf.extend_from_slice(&self.metadata_offset.to_le_bytes());
buf.extend_from_slice(&self.row_group_count.to_le_bytes());
buf.extend_from_slice(&self.checksum.to_le_bytes());
buf.extend_from_slice(&self.metadata_checksum.to_le_bytes());
buf.extend_from_slice(&MAGIC);
buf
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < FOOTER_SIZE {
return Err(SegmentError::CorruptFile {
detail: format!(
"footer too short: expected {FOOTER_SIZE}, got {}",
data.len()
),
});
}
let metadata_offset = u64::from_le_bytes(to_array!(data[..8], "footer metadata_offset")?);
let row_group_count = u32::from_le_bytes(to_array!(data[8..12], "footer row_group_count")?);
let checksum = u32::from_le_bytes(to_array!(data[12..16], "footer checksum")?);
let metadata_checksum =
u32::from_le_bytes(to_array!(data[16..20], "footer metadata_checksum")?);
if data[20..24] != MAGIC {
return Err(SegmentError::InvalidMagic);
}
Ok(Self {
metadata_offset,
row_group_count,
checksum,
metadata_checksum,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_roundtrip() {
let header = SegmentHeader {
version: VERSION,
flags: 0,
created_at: 1_700_000_000_000_000_000,
min_timestamp: 1_000_000,
max_timestamp: 2_000_000,
row_count: 10_000,
column_count: 5,
series_count: 42,
compression: 0,
sort_order: 1,
};
let bytes = header.to_bytes();
assert_eq!(bytes.len(), HEADER_SIZE);
let recovered = SegmentHeader::from_bytes(&bytes).unwrap();
assert_eq!(header, recovered);
}
#[test]
fn header_invalid_magic() {
let mut bytes = SegmentHeader {
version: VERSION,
flags: 0,
created_at: 0,
min_timestamp: 0,
max_timestamp: 0,
row_count: 0,
column_count: 0,
series_count: 0,
compression: 0,
sort_order: 0,
}
.to_bytes();
bytes[0] = b'X';
assert!(matches!(
SegmentHeader::from_bytes(&bytes),
Err(SegmentError::InvalidMagic)
));
}
#[test]
fn footer_roundtrip() {
let footer = SegmentFooter {
metadata_offset: 1024,
row_group_count: 3,
checksum: 0xDEAD_BEEF,
metadata_checksum: 0xCAFE_BABE,
};
let bytes = footer.to_bytes();
assert_eq!(bytes.len(), FOOTER_SIZE);
let recovered = SegmentFooter::from_bytes(&bytes).unwrap();
assert_eq!(footer, recovered);
}
#[test]
fn footer_invalid_magic() {
let mut bytes = SegmentFooter {
metadata_offset: 0,
row_group_count: 0,
checksum: 0,
metadata_checksum: 0,
}
.to_bytes();
let len = bytes.len();
bytes[len - 1] = b'X';
assert!(matches!(
SegmentFooter::from_bytes(&bytes),
Err(SegmentError::InvalidMagic)
));
}
#[test]
fn header_too_short() {
assert!(SegmentHeader::from_bytes(&[0; 10]).is_err());
}
#[test]
fn footer_too_short() {
assert!(SegmentFooter::from_bytes(&[0; 5]).is_err());
}
#[test]
fn header_version_is_refused_in_both_directions() {
let header = SegmentHeader {
version: VERSION,
flags: 0,
created_at: 0,
min_timestamp: 0,
max_timestamp: 0,
row_count: 0,
column_count: 0,
series_count: 0,
compression: 0,
sort_order: 0,
};
for wrong in [VERSION - 1, VERSION + 1, 999] {
let mut bytes = header.to_bytes();
bytes[4..6].copy_from_slice(&wrong.to_le_bytes());
assert!(
matches!(
SegmentHeader::from_bytes(&bytes),
Err(SegmentError::UnsupportedVersion { .. })
),
"version {wrong} must be refused, not read as {VERSION}"
);
}
}
}