mod reader;
mod writer;
pub use reader::{Reader, SignatureStatus, VerifyReport};
pub use writer::{PackStats, Writer};
use cavs_hash::ChunkHash;
pub const MAGIC: [u8; 4] = *b"CAVS";
pub const VERSION_MAJOR: u16 = 1;
pub const VERSION_MINOR: u16 = 0;
pub const SUPERBLOCK_LEN: u64 = 64;
pub const SECTION_DIR_ENTRY_LEN: usize = 52;
pub const MAX_CHUNK_RAW: u64 = 256 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum SectionType {
Tracks = 1,
Dict = 2,
Chunks = 3,
Segments = 4,
Data = 5,
Integrity = 6,
Meta = 7,
}
impl SectionType {
pub fn from_u32(v: u32) -> Option<Self> {
Some(match v {
1 => SectionType::Tracks,
2 => SectionType::Dict,
3 => SectionType::Chunks,
4 => SectionType::Segments,
5 => SectionType::Data,
6 => SectionType::Integrity,
7 => SectionType::Meta,
_ => return None,
})
}
}
pub const COMPRESSION_NONE: u8 = 0;
pub const COMPRESSION_ZSTD: u8 = 1;
pub const CHUNK_FLAG_ZSTD: u32 = 1 << 0;
pub const SEGMENT_FLAG_RANDOM_ACCESS: u32 = 1 << 0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TrackKind {
Video = 0,
Audio = 1,
Subtitle = 2,
Data = 3,
}
impl TrackKind {
pub fn from_u8(v: u8) -> Option<Self> {
Some(match v {
0 => TrackKind::Video,
1 => TrackKind::Audio,
2 => TrackKind::Subtitle,
3 => TrackKind::Data,
_ => return None,
})
}
pub fn label(&self) -> &'static str {
match self {
TrackKind::Video => "video",
TrackKind::Audio => "audio",
TrackKind::Subtitle => "subtitle",
TrackKind::Data => "data",
}
}
}
#[derive(Debug, Clone)]
pub struct Superblock {
pub version_major: u16,
pub version_minor: u16,
pub feature_flags: u32,
pub hash_algo: u8,
pub compression_algo: u8,
pub asset_uuid: [u8; 16],
pub timescale: u32,
pub section_count: u32,
pub section_dir_offset: u64,
pub file_size: u64,
}
#[derive(Debug, Clone)]
pub struct SectionEntry {
pub section_type: SectionType,
pub offset: u64,
pub length: u64,
pub hash: ChunkHash,
}
#[derive(Debug, Clone)]
pub struct ChunkRecord {
pub hash: ChunkHash,
pub data_offset: u64,
pub len_raw: u32,
pub len_stored: u32,
pub flags: u32,
}
#[derive(Debug, Clone)]
pub struct TrackRecord {
pub track_id: u32,
pub kind: TrackKind,
pub flags: u8,
pub codec: String,
pub name: String,
pub timescale: u32,
pub init_chunks: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct SegmentRecord {
pub segment_id: u64,
pub track_id: u32,
pub pts_start: u64,
pub duration: u32,
pub flags: u32,
pub chunks: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct Integrity {
pub merkle_root: ChunkHash,
pub chunk_count: u64,
pub total_raw: u64,
pub total_stored: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum FormatError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("not a CAVS file (bad magic)")]
BadMagic,
#[error("unsupported CAVS major version {0}")]
UnsupportedVersion(u16),
#[error("truncated or malformed {0} section")]
Malformed(&'static str),
#[error("missing required section {0:?}")]
MissingSection(SectionType),
#[error("unknown enum value {value} for {what}")]
UnknownValue { what: &'static str, value: u32 },
#[error("chunk {index} hash mismatch (corrupted payload)")]
ChunkHashMismatch { index: u32 },
#[error("section {0:?} hash mismatch (corrupted section)")]
SectionHashMismatch(SectionType),
#[error("merkle root mismatch (chunk table tampered)")]
MerkleMismatch,
#[error("embedded content signature is invalid")]
SignatureInvalid,
#[error("chunk index {0} out of range")]
ChunkIndexOutOfRange(u32),
#[error("track {0} not found")]
TrackNotFound(u32),
#[error("zstd error: {0}")]
Zstd(std::io::Error),
}
pub type Result<T> = std::result::Result<T, FormatError>;
pub(crate) mod wire {
use super::{FormatError, Result};
pub fn put_u16(buf: &mut Vec<u8>, v: u16) {
buf.extend_from_slice(&v.to_le_bytes());
}
pub fn put_u32(buf: &mut Vec<u8>, v: u32) {
buf.extend_from_slice(&v.to_le_bytes());
}
pub fn put_u64(buf: &mut Vec<u8>, v: u64) {
buf.extend_from_slice(&v.to_le_bytes());
}
pub fn put_str(buf: &mut Vec<u8>, s: &str) {
let bytes = s.as_bytes();
assert!(bytes.len() <= u16::MAX as usize, "string too long for wire");
put_u16(buf, bytes.len() as u16);
buf.extend_from_slice(bytes);
}
pub fn put_bytes32(buf: &mut Vec<u8>, b: &[u8]) {
put_u32(buf, b.len() as u32);
buf.extend_from_slice(b);
}
pub struct Cursor<'a> {
buf: &'a [u8],
pos: usize,
what: &'static str,
}
impl<'a> Cursor<'a> {
pub fn new(buf: &'a [u8], what: &'static str) -> Self {
Self { buf, pos: 0, what }
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
if self.pos + n > self.buf.len() {
return Err(FormatError::Malformed(self.what));
}
let s = &self.buf[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
pub fn u8(&mut self) -> Result<u8> {
Ok(self.take(1)?[0])
}
pub fn u16(&mut self) -> Result<u16> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
pub fn u32(&mut self) -> Result<u32> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
pub fn u64(&mut self) -> Result<u64> {
Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
}
pub fn hash(&mut self) -> Result<[u8; 32]> {
Ok(self.take(32)?.try_into().unwrap())
}
pub fn str16(&mut self) -> Result<String> {
let len = self.u16()? as usize;
let bytes = self.take(len)?;
String::from_utf8(bytes.to_vec()).map_err(|_| FormatError::Malformed(self.what))
}
pub fn bytes32(&mut self) -> Result<Vec<u8>> {
let len = self.u32()? as usize;
Ok(self.take(len)?.to_vec())
}
}
}