Skip to main content

cavs_format/
reader.rs

1//! Reader / verifier for CAVS-1 files.
2
3use crate::wire::Cursor;
4use crate::{
5    ChunkRecord, FormatError, Integrity, Result, SectionEntry, SectionType, SegmentRecord,
6    Superblock, TrackKind, TrackRecord, CHUNK_FLAG_ZSTD, MAGIC, SECTION_DIR_ENTRY_LEN,
7    SUPERBLOCK_LEN, VERSION_MAJOR,
8};
9use cavs_hash::{content_signature_message, hash_chunk, merkle_root, Hasher};
10use std::fs::File;
11use std::io::{Read as _, Seek, SeekFrom};
12use std::path::Path;
13
14pub struct Reader {
15    file: File,
16    superblock: Superblock,
17    sections: Vec<SectionEntry>,
18    tracks: Vec<TrackRecord>,
19    dict: Vec<u32>,
20    chunks: Vec<ChunkRecord>,
21    segments: Vec<SegmentRecord>,
22    meta: Vec<(String, String)>,
23    integrity: Integrity,
24    data_offset: u64,
25    data_len: u64,
26}
27
28/// Result of a full-file verification pass.
29#[derive(Debug, Clone)]
30pub struct VerifyReport {
31    pub chunks_verified: u64,
32    pub bytes_verified: u64,
33    pub merkle_ok: bool,
34    pub data_section_ok: bool,
35}
36
37/// Outcome of a content-signature check.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum SignatureStatus {
40    /// No signature embedded.
41    Unsigned,
42    /// Signature verified; the signer's Ed25519 public key.
43    Valid([u8; 32]),
44}
45
46impl Reader {
47    pub fn open(path: &Path) -> Result<Self> {
48        let mut file = File::open(path)?;
49        // Real file length: every offset/length parsed below is validated
50        // against it so a crafted header can never trigger a huge allocation.
51        let file_len = file.metadata()?.len();
52
53        let mut sb_bytes = [0u8; SUPERBLOCK_LEN as usize];
54        file.read_exact(&mut sb_bytes)?;
55        if sb_bytes[0..4] != MAGIC {
56            return Err(FormatError::BadMagic);
57        }
58        let mut cur = Cursor::new(&sb_bytes[4..], "superblock");
59        let superblock = Superblock {
60            version_major: cur.u16()?,
61            version_minor: cur.u16()?,
62            feature_flags: cur.u32()?,
63            hash_algo: cur.u8()?,
64            compression_algo: cur.u8()?,
65            asset_uuid: {
66                cur.u16()?; // reserved
67                let mut uuid = [0u8; 16];
68                for b in uuid.iter_mut() {
69                    *b = cur.u8()?;
70                }
71                uuid
72            },
73            timescale: cur.u32()?,
74            section_count: cur.u32()?,
75            section_dir_offset: cur.u64()?,
76            file_size: cur.u64()?,
77        };
78        if superblock.version_major != VERSION_MAJOR {
79            return Err(FormatError::UnsupportedVersion(superblock.version_major));
80        }
81        // v0.5.0 hardening: reject values a correct writer never produces.
82        // (The rest of the superblock — uuid, timescale, reserved fields —
83        // is intentionally unauthenticated metadata; content integrity is
84        // carried by the section hashes, chunk hashes and Merkle root.)
85        if cavs_hash::HashAlgo::from_u8(superblock.hash_algo).is_none() {
86            return Err(FormatError::UnknownValue {
87                what: "hash algorithm",
88                value: superblock.hash_algo as u32,
89            });
90        }
91        if superblock.compression_algo > crate::COMPRESSION_ZSTD {
92            return Err(FormatError::UnknownValue {
93                what: "compression algorithm",
94                value: superblock.compression_algo as u32,
95            });
96        }
97        if superblock.file_size != file_len {
98            return Err(FormatError::Malformed("declared file size"));
99        }
100
101        // Section directory. Validate offset + size against the file before
102        // allocating, so a bogus section_count can't ask for gigabytes.
103        let dir_len = superblock.section_count as u64 * SECTION_DIR_ENTRY_LEN as u64;
104        if superblock.section_dir_offset > file_len
105            || dir_len > file_len - superblock.section_dir_offset
106        {
107            return Err(FormatError::Malformed("section directory"));
108        }
109        file.seek(SeekFrom::Start(superblock.section_dir_offset))?;
110        let mut dir_bytes = vec![0u8; dir_len as usize];
111        file.read_exact(&mut dir_bytes)?;
112        let mut cur = Cursor::new(&dir_bytes, "section directory");
113        let mut sections = Vec::with_capacity(superblock.section_count as usize);
114        for _ in 0..superblock.section_count {
115            let ty_raw = cur.u32()?;
116            let section_type = SectionType::from_u32(ty_raw).ok_or(FormatError::UnknownValue {
117                what: "section type",
118                value: ty_raw,
119            })?;
120            sections.push(SectionEntry {
121                section_type,
122                offset: cur.u64()?,
123                length: cur.u64()?,
124                hash: cur.hash()?,
125            });
126        }
127
128        let read_section = |file: &mut File, ty: SectionType| -> Result<Vec<u8>> {
129            let entry = sections
130                .iter()
131                .find(|s| s.section_type == ty)
132                .ok_or(FormatError::MissingSection(ty))?;
133            if entry.offset > file_len || entry.length > file_len - entry.offset {
134                return Err(FormatError::Malformed("section bounds"));
135            }
136            file.seek(SeekFrom::Start(entry.offset))?;
137            let mut buf = vec![0u8; entry.length as usize];
138            file.read_exact(&mut buf)?;
139            // Table sections are small; verify their hash eagerly.
140            if hash_chunk(&buf) != entry.hash {
141                return Err(FormatError::SectionHashMismatch(ty));
142            }
143            Ok(buf)
144        };
145
146        let tracks = decode_tracks(&read_section(&mut file, SectionType::Tracks)?)?;
147        let dict = decode_dict(&read_section(&mut file, SectionType::Dict)?)?;
148        let chunks = decode_chunks(&read_section(&mut file, SectionType::Chunks)?)?;
149        let segments = decode_segments(&read_section(&mut file, SectionType::Segments)?)?;
150        let meta = decode_meta(&read_section(&mut file, SectionType::Meta)?)?;
151        let integrity = decode_integrity(&read_section(&mut file, SectionType::Integrity)?)?;
152
153        let data_entry = sections
154            .iter()
155            .find(|s| s.section_type == SectionType::Data)
156            .ok_or(FormatError::MissingSection(SectionType::Data))?;
157        let (data_offset, data_len) = (data_entry.offset, data_entry.length);
158
159        Ok(Self {
160            file,
161            superblock,
162            sections,
163            tracks,
164            dict,
165            chunks,
166            segments,
167            meta,
168            integrity,
169            data_offset,
170            data_len,
171        })
172    }
173
174    pub fn superblock(&self) -> &Superblock {
175        &self.superblock
176    }
177    pub fn sections(&self) -> &[SectionEntry] {
178        &self.sections
179    }
180    pub fn tracks(&self) -> &[TrackRecord] {
181        &self.tracks
182    }
183    pub fn dict(&self) -> &[u32] {
184        &self.dict
185    }
186    pub fn chunks(&self) -> &[ChunkRecord] {
187        &self.chunks
188    }
189    pub fn segments(&self) -> &[SegmentRecord] {
190        &self.segments
191    }
192    pub fn meta(&self) -> &[(String, String)] {
193        &self.meta
194    }
195    pub fn integrity(&self) -> &Integrity {
196        &self.integrity
197    }
198
199    pub fn track(&self, track_id: u32) -> Result<&TrackRecord> {
200        self.tracks
201            .iter()
202            .find(|t| t.track_id == track_id)
203            .ok_or(FormatError::TrackNotFound(track_id))
204    }
205
206    /// Segments of one track, ordered by presentation time.
207    pub fn segments_for_track(&self, track_id: u32) -> Vec<&SegmentRecord> {
208        let mut segs: Vec<&SegmentRecord> = self
209            .segments
210            .iter()
211            .filter(|s| s.track_id == track_id)
212            .collect();
213        segs.sort_by_key(|s| (s.pts_start, s.segment_id));
214        segs
215    }
216
217    /// Read one chunk payload exactly as stored (possibly zstd-compressed),
218    /// without decompressing or verifying. Returns (stored bytes, flags,
219    /// len_raw). Intended for wire passthrough: the receiver decompresses
220    /// and verifies the BLAKE3 identity against the raw bytes.
221    pub fn read_chunk_stored(&mut self, index: u32) -> Result<(Vec<u8>, u32, u32)> {
222        let rec = self
223            .chunks
224            .get(index as usize)
225            .ok_or(FormatError::ChunkIndexOutOfRange(index))?
226            .clone();
227        // The chunk's stored bytes must lie fully within the DATA section.
228        if rec.data_offset > self.data_len
229            || rec.len_stored as u64 > self.data_len - rec.data_offset
230            || rec.len_raw as u64 > crate::MAX_CHUNK_RAW
231        {
232            return Err(FormatError::Malformed("chunk bounds"));
233        }
234        self.file
235            .seek(SeekFrom::Start(self.data_offset + rec.data_offset))?;
236        let mut stored = vec![0u8; rec.len_stored as usize];
237        self.file.read_exact(&mut stored)?;
238        Ok((stored, rec.flags, rec.len_raw))
239    }
240
241    /// Read, decompress and verify one chunk payload.
242    pub fn read_chunk(&mut self, index: u32) -> Result<Vec<u8>> {
243        let (stored, flags, len_raw) = self.read_chunk_stored(index)?;
244        let raw = if flags & CHUNK_FLAG_ZSTD != 0 {
245            // len_raw was bounded by MAX_CHUNK_RAW in read_chunk_stored, so
246            // the decompression capacity hint is safe.
247            zstd::bulk::decompress(&stored, len_raw as usize).map_err(FormatError::Zstd)?
248        } else {
249            stored
250        };
251        let rec = &self.chunks[index as usize];
252        if raw.len() != len_raw as usize || hash_chunk(&raw) != rec.hash {
253            return Err(FormatError::ChunkHashMismatch { index });
254        }
255        Ok(raw)
256    }
257
258    /// Reconstruct a segment payload: ordered concatenation of its chunks.
259    pub fn segment_bytes(&mut self, segment: &SegmentRecord) -> Result<Vec<u8>> {
260        let mut out = Vec::new();
261        for &c in &segment.chunks {
262            out.extend_from_slice(&self.read_chunk(c)?);
263        }
264        Ok(out)
265    }
266
267    /// Reconstruct a track's init payload (e.g. CMAF init segment).
268    pub fn track_init_bytes(&mut self, track_id: u32) -> Result<Vec<u8>> {
269        let init_chunks = self.track(track_id)?.init_chunks.clone();
270        let mut out = Vec::new();
271        for c in init_chunks {
272            out.extend_from_slice(&self.read_chunk(c)?);
273        }
274        Ok(out)
275    }
276
277    /// Embedded content signature (sig, pubkey) if present, parsed from meta.
278    pub fn embedded_signature(&self) -> Option<([u8; 64], [u8; 32])> {
279        let hex_bytes = |key: &str, len: usize| -> Option<Vec<u8>> {
280            let value = self.meta.iter().find(|(k, _)| k == key).map(|(_, v)| v)?;
281            if value.len() != len * 2 {
282                return None;
283            }
284            (0..len)
285                .map(|i| u8::from_str_radix(&value[i * 2..i * 2 + 2], 16).ok())
286                .collect()
287        };
288        let sig: [u8; 64] = hex_bytes("sig.ed25519", 64)?.try_into().ok()?;
289        let pk: [u8; 32] = hex_bytes("sig.pubkey", 32)?.try_into().ok()?;
290        Some((sig, pk))
291    }
292
293    /// Check the embedded Ed25519 content signature, if any. Returns
294    /// `Unsigned` when absent, `Valid(pubkey)` when it verifies, and an error
295    /// when present but invalid. Callers decide whether the returned pubkey
296    /// is trusted.
297    pub fn verify_signature(&self) -> Result<SignatureStatus> {
298        let Some((sig, pk)) = self.embedded_signature() else {
299            return Ok(SignatureStatus::Unsigned);
300        };
301        let key = ed25519_dalek::VerifyingKey::from_bytes(&pk)
302            .map_err(|_| FormatError::SignatureInvalid)?;
303        let message =
304            content_signature_message(&self.integrity.merkle_root, self.integrity.chunk_count);
305        use ed25519_dalek::Verifier;
306        key.verify(&message, &ed25519_dalek::Signature::from_bytes(&sig))
307            .map_err(|_| FormatError::SignatureInvalid)?;
308        Ok(SignatureStatus::Valid(pk))
309    }
310
311    /// Full verification: every chunk hash, the Merkle root against the
312    /// integrity section, and the DATA section hash from the directory.
313    pub fn verify(&mut self) -> Result<VerifyReport> {
314        let mut bytes = 0u64;
315        for i in 0..self.chunks.len() as u32 {
316            bytes += self.read_chunk(i)?.len() as u64;
317        }
318
319        let hashes: Vec<_> = self.chunks.iter().map(|c| c.hash).collect();
320        if merkle_root(&hashes) != self.integrity.merkle_root
321            || self.integrity.chunk_count != self.chunks.len() as u64
322        {
323            return Err(FormatError::MerkleMismatch);
324        }
325
326        // Stream-hash the DATA section against its directory entry.
327        let data_entry_hash = self
328            .sections
329            .iter()
330            .find(|s| s.section_type == SectionType::Data)
331            .map(|s| s.hash)
332            .ok_or(FormatError::MissingSection(SectionType::Data))?;
333        self.file.seek(SeekFrom::Start(self.data_offset))?;
334        let mut hasher = Hasher::new();
335        let mut remaining = self.data_len;
336        let mut buf = vec![0u8; 1 << 20];
337        while remaining > 0 {
338            let n = remaining.min(buf.len() as u64) as usize;
339            self.file.read_exact(&mut buf[..n])?;
340            hasher.update(&buf[..n]);
341            remaining -= n as u64;
342        }
343        if hasher.finalize() != data_entry_hash {
344            return Err(FormatError::SectionHashMismatch(SectionType::Data));
345        }
346
347        Ok(VerifyReport {
348            chunks_verified: self.chunks.len() as u64,
349            bytes_verified: bytes,
350            merkle_ok: true,
351            data_section_ok: true,
352        })
353    }
354}
355
356fn decode_tracks(buf: &[u8]) -> Result<Vec<TrackRecord>> {
357    let mut cur = Cursor::new(buf, "tracks");
358    let count = cur.u32()?;
359    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
360    for _ in 0..count {
361        let track_id = cur.u32()?;
362        let kind_raw = cur.u8()?;
363        let kind = TrackKind::from_u8(kind_raw).ok_or(FormatError::UnknownValue {
364            what: "track kind",
365            value: kind_raw as u32,
366        })?;
367        let flags = cur.u8()?;
368        let codec = cur.str16()?;
369        let name = cur.str16()?;
370        let timescale = cur.u32()?;
371        let n = cur.u32()?;
372        let mut init_chunks = Vec::with_capacity((n as usize).min(buf.len()));
373        for _ in 0..n {
374            init_chunks.push(cur.u32()?);
375        }
376        out.push(TrackRecord {
377            track_id,
378            kind,
379            flags,
380            codec,
381            name,
382            timescale,
383            init_chunks,
384        });
385    }
386    Ok(out)
387}
388
389fn decode_dict(buf: &[u8]) -> Result<Vec<u32>> {
390    let mut cur = Cursor::new(buf, "dict");
391    let count = cur.u32()?;
392    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
393    for _ in 0..count {
394        out.push(cur.u32()?);
395    }
396    Ok(out)
397}
398
399fn decode_chunks(buf: &[u8]) -> Result<Vec<ChunkRecord>> {
400    let mut cur = Cursor::new(buf, "chunks");
401    let count = cur.u32()?;
402    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
403    for _ in 0..count {
404        out.push(ChunkRecord {
405            hash: cur.hash()?,
406            data_offset: cur.u64()?,
407            len_raw: cur.u32()?,
408            len_stored: cur.u32()?,
409            flags: cur.u32()?,
410        });
411    }
412    Ok(out)
413}
414
415fn decode_segments(buf: &[u8]) -> Result<Vec<SegmentRecord>> {
416    let mut cur = Cursor::new(buf, "segments");
417    let count = cur.u32()?;
418    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
419    for _ in 0..count {
420        let segment_id = cur.u64()?;
421        let track_id = cur.u32()?;
422        let pts_start = cur.u64()?;
423        let duration = cur.u32()?;
424        let flags = cur.u32()?;
425        let n = cur.u32()?;
426        let mut chunks = Vec::with_capacity((n as usize).min(buf.len()));
427        for _ in 0..n {
428            chunks.push(cur.u32()?);
429        }
430        out.push(SegmentRecord {
431            segment_id,
432            track_id,
433            pts_start,
434            duration,
435            flags,
436            chunks,
437        });
438    }
439    Ok(out)
440}
441
442fn decode_meta(buf: &[u8]) -> Result<Vec<(String, String)>> {
443    let mut cur = Cursor::new(buf, "meta");
444    let count = cur.u32()?;
445    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
446    for _ in 0..count {
447        let key = cur.str16()?;
448        let value_bytes = cur.bytes32()?;
449        let value = String::from_utf8(value_bytes).map_err(|_| FormatError::Malformed("meta"))?;
450        out.push((key, value));
451    }
452    Ok(out)
453}
454
455fn decode_integrity(buf: &[u8]) -> Result<Integrity> {
456    let mut cur = Cursor::new(buf, "integrity");
457    Ok(Integrity {
458        merkle_root: cur.hash()?,
459        chunk_count: cur.u64()?,
460        total_raw: cur.u64()?,
461        total_stored: cur.u64()?,
462    })
463}