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
82        // Section directory. Validate offset + size against the file before
83        // allocating, so a bogus section_count can't ask for gigabytes.
84        let dir_len = superblock.section_count as u64 * SECTION_DIR_ENTRY_LEN as u64;
85        if superblock.section_dir_offset > file_len
86            || dir_len > file_len - superblock.section_dir_offset
87        {
88            return Err(FormatError::Malformed("section directory"));
89        }
90        file.seek(SeekFrom::Start(superblock.section_dir_offset))?;
91        let mut dir_bytes = vec![0u8; dir_len as usize];
92        file.read_exact(&mut dir_bytes)?;
93        let mut cur = Cursor::new(&dir_bytes, "section directory");
94        let mut sections = Vec::with_capacity(superblock.section_count as usize);
95        for _ in 0..superblock.section_count {
96            let ty_raw = cur.u32()?;
97            let section_type = SectionType::from_u32(ty_raw).ok_or(FormatError::UnknownValue {
98                what: "section type",
99                value: ty_raw,
100            })?;
101            sections.push(SectionEntry {
102                section_type,
103                offset: cur.u64()?,
104                length: cur.u64()?,
105                hash: cur.hash()?,
106            });
107        }
108
109        let read_section = |file: &mut File, ty: SectionType| -> Result<Vec<u8>> {
110            let entry = sections
111                .iter()
112                .find(|s| s.section_type == ty)
113                .ok_or(FormatError::MissingSection(ty))?;
114            if entry.offset > file_len || entry.length > file_len - entry.offset {
115                return Err(FormatError::Malformed("section bounds"));
116            }
117            file.seek(SeekFrom::Start(entry.offset))?;
118            let mut buf = vec![0u8; entry.length as usize];
119            file.read_exact(&mut buf)?;
120            // Table sections are small; verify their hash eagerly.
121            if hash_chunk(&buf) != entry.hash {
122                return Err(FormatError::SectionHashMismatch(ty));
123            }
124            Ok(buf)
125        };
126
127        let tracks = decode_tracks(&read_section(&mut file, SectionType::Tracks)?)?;
128        let dict = decode_dict(&read_section(&mut file, SectionType::Dict)?)?;
129        let chunks = decode_chunks(&read_section(&mut file, SectionType::Chunks)?)?;
130        let segments = decode_segments(&read_section(&mut file, SectionType::Segments)?)?;
131        let meta = decode_meta(&read_section(&mut file, SectionType::Meta)?)?;
132        let integrity = decode_integrity(&read_section(&mut file, SectionType::Integrity)?)?;
133
134        let data_entry = sections
135            .iter()
136            .find(|s| s.section_type == SectionType::Data)
137            .ok_or(FormatError::MissingSection(SectionType::Data))?;
138        let (data_offset, data_len) = (data_entry.offset, data_entry.length);
139
140        Ok(Self {
141            file,
142            superblock,
143            sections,
144            tracks,
145            dict,
146            chunks,
147            segments,
148            meta,
149            integrity,
150            data_offset,
151            data_len,
152        })
153    }
154
155    pub fn superblock(&self) -> &Superblock {
156        &self.superblock
157    }
158    pub fn sections(&self) -> &[SectionEntry] {
159        &self.sections
160    }
161    pub fn tracks(&self) -> &[TrackRecord] {
162        &self.tracks
163    }
164    pub fn dict(&self) -> &[u32] {
165        &self.dict
166    }
167    pub fn chunks(&self) -> &[ChunkRecord] {
168        &self.chunks
169    }
170    pub fn segments(&self) -> &[SegmentRecord] {
171        &self.segments
172    }
173    pub fn meta(&self) -> &[(String, String)] {
174        &self.meta
175    }
176    pub fn integrity(&self) -> &Integrity {
177        &self.integrity
178    }
179
180    pub fn track(&self, track_id: u32) -> Result<&TrackRecord> {
181        self.tracks
182            .iter()
183            .find(|t| t.track_id == track_id)
184            .ok_or(FormatError::TrackNotFound(track_id))
185    }
186
187    /// Segments of one track, ordered by presentation time.
188    pub fn segments_for_track(&self, track_id: u32) -> Vec<&SegmentRecord> {
189        let mut segs: Vec<&SegmentRecord> = self
190            .segments
191            .iter()
192            .filter(|s| s.track_id == track_id)
193            .collect();
194        segs.sort_by_key(|s| (s.pts_start, s.segment_id));
195        segs
196    }
197
198    /// Read one chunk payload exactly as stored (possibly zstd-compressed),
199    /// without decompressing or verifying. Returns (stored bytes, flags,
200    /// len_raw). Intended for wire passthrough: the receiver decompresses
201    /// and verifies the BLAKE3 identity against the raw bytes.
202    pub fn read_chunk_stored(&mut self, index: u32) -> Result<(Vec<u8>, u32, u32)> {
203        let rec = self
204            .chunks
205            .get(index as usize)
206            .ok_or(FormatError::ChunkIndexOutOfRange(index))?
207            .clone();
208        // The chunk's stored bytes must lie fully within the DATA section.
209        if rec.data_offset > self.data_len
210            || rec.len_stored as u64 > self.data_len - rec.data_offset
211            || rec.len_raw as u64 > crate::MAX_CHUNK_RAW
212        {
213            return Err(FormatError::Malformed("chunk bounds"));
214        }
215        self.file
216            .seek(SeekFrom::Start(self.data_offset + rec.data_offset))?;
217        let mut stored = vec![0u8; rec.len_stored as usize];
218        self.file.read_exact(&mut stored)?;
219        Ok((stored, rec.flags, rec.len_raw))
220    }
221
222    /// Read, decompress and verify one chunk payload.
223    pub fn read_chunk(&mut self, index: u32) -> Result<Vec<u8>> {
224        let (stored, flags, len_raw) = self.read_chunk_stored(index)?;
225        let raw = if flags & CHUNK_FLAG_ZSTD != 0 {
226            // len_raw was bounded by MAX_CHUNK_RAW in read_chunk_stored, so
227            // the decompression capacity hint is safe.
228            zstd::bulk::decompress(&stored, len_raw as usize).map_err(FormatError::Zstd)?
229        } else {
230            stored
231        };
232        let rec = &self.chunks[index as usize];
233        if raw.len() != len_raw as usize || hash_chunk(&raw) != rec.hash {
234            return Err(FormatError::ChunkHashMismatch { index });
235        }
236        Ok(raw)
237    }
238
239    /// Reconstruct a segment payload: ordered concatenation of its chunks.
240    pub fn segment_bytes(&mut self, segment: &SegmentRecord) -> Result<Vec<u8>> {
241        let mut out = Vec::new();
242        for &c in &segment.chunks {
243            out.extend_from_slice(&self.read_chunk(c)?);
244        }
245        Ok(out)
246    }
247
248    /// Reconstruct a track's init payload (e.g. CMAF init segment).
249    pub fn track_init_bytes(&mut self, track_id: u32) -> Result<Vec<u8>> {
250        let init_chunks = self.track(track_id)?.init_chunks.clone();
251        let mut out = Vec::new();
252        for c in init_chunks {
253            out.extend_from_slice(&self.read_chunk(c)?);
254        }
255        Ok(out)
256    }
257
258    /// Embedded content signature (sig, pubkey) if present, parsed from meta.
259    pub fn embedded_signature(&self) -> Option<([u8; 64], [u8; 32])> {
260        let hex_bytes = |key: &str, len: usize| -> Option<Vec<u8>> {
261            let value = self.meta.iter().find(|(k, _)| k == key).map(|(_, v)| v)?;
262            if value.len() != len * 2 {
263                return None;
264            }
265            (0..len)
266                .map(|i| u8::from_str_radix(&value[i * 2..i * 2 + 2], 16).ok())
267                .collect()
268        };
269        let sig: [u8; 64] = hex_bytes("sig.ed25519", 64)?.try_into().ok()?;
270        let pk: [u8; 32] = hex_bytes("sig.pubkey", 32)?.try_into().ok()?;
271        Some((sig, pk))
272    }
273
274    /// Check the embedded Ed25519 content signature, if any. Returns
275    /// `Unsigned` when absent, `Valid(pubkey)` when it verifies, and an error
276    /// when present but invalid. Callers decide whether the returned pubkey
277    /// is trusted.
278    pub fn verify_signature(&self) -> Result<SignatureStatus> {
279        let Some((sig, pk)) = self.embedded_signature() else {
280            return Ok(SignatureStatus::Unsigned);
281        };
282        let key = ed25519_dalek::VerifyingKey::from_bytes(&pk)
283            .map_err(|_| FormatError::SignatureInvalid)?;
284        let message =
285            content_signature_message(&self.integrity.merkle_root, self.integrity.chunk_count);
286        use ed25519_dalek::Verifier;
287        key.verify(&message, &ed25519_dalek::Signature::from_bytes(&sig))
288            .map_err(|_| FormatError::SignatureInvalid)?;
289        Ok(SignatureStatus::Valid(pk))
290    }
291
292    /// Full verification: every chunk hash, the Merkle root against the
293    /// integrity section, and the DATA section hash from the directory.
294    pub fn verify(&mut self) -> Result<VerifyReport> {
295        let mut bytes = 0u64;
296        for i in 0..self.chunks.len() as u32 {
297            bytes += self.read_chunk(i)?.len() as u64;
298        }
299
300        let hashes: Vec<_> = self.chunks.iter().map(|c| c.hash).collect();
301        if merkle_root(&hashes) != self.integrity.merkle_root
302            || self.integrity.chunk_count != self.chunks.len() as u64
303        {
304            return Err(FormatError::MerkleMismatch);
305        }
306
307        // Stream-hash the DATA section against its directory entry.
308        let data_entry_hash = self
309            .sections
310            .iter()
311            .find(|s| s.section_type == SectionType::Data)
312            .map(|s| s.hash)
313            .ok_or(FormatError::MissingSection(SectionType::Data))?;
314        self.file.seek(SeekFrom::Start(self.data_offset))?;
315        let mut hasher = Hasher::new();
316        let mut remaining = self.data_len;
317        let mut buf = vec![0u8; 1 << 20];
318        while remaining > 0 {
319            let n = remaining.min(buf.len() as u64) as usize;
320            self.file.read_exact(&mut buf[..n])?;
321            hasher.update(&buf[..n]);
322            remaining -= n as u64;
323        }
324        if hasher.finalize() != data_entry_hash {
325            return Err(FormatError::SectionHashMismatch(SectionType::Data));
326        }
327
328        Ok(VerifyReport {
329            chunks_verified: self.chunks.len() as u64,
330            bytes_verified: bytes,
331            merkle_ok: true,
332            data_section_ok: true,
333        })
334    }
335}
336
337fn decode_tracks(buf: &[u8]) -> Result<Vec<TrackRecord>> {
338    let mut cur = Cursor::new(buf, "tracks");
339    let count = cur.u32()?;
340    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
341    for _ in 0..count {
342        let track_id = cur.u32()?;
343        let kind_raw = cur.u8()?;
344        let kind = TrackKind::from_u8(kind_raw).ok_or(FormatError::UnknownValue {
345            what: "track kind",
346            value: kind_raw as u32,
347        })?;
348        let flags = cur.u8()?;
349        let codec = cur.str16()?;
350        let name = cur.str16()?;
351        let timescale = cur.u32()?;
352        let n = cur.u32()?;
353        let mut init_chunks = Vec::with_capacity((n as usize).min(buf.len()));
354        for _ in 0..n {
355            init_chunks.push(cur.u32()?);
356        }
357        out.push(TrackRecord {
358            track_id,
359            kind,
360            flags,
361            codec,
362            name,
363            timescale,
364            init_chunks,
365        });
366    }
367    Ok(out)
368}
369
370fn decode_dict(buf: &[u8]) -> Result<Vec<u32>> {
371    let mut cur = Cursor::new(buf, "dict");
372    let count = cur.u32()?;
373    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
374    for _ in 0..count {
375        out.push(cur.u32()?);
376    }
377    Ok(out)
378}
379
380fn decode_chunks(buf: &[u8]) -> Result<Vec<ChunkRecord>> {
381    let mut cur = Cursor::new(buf, "chunks");
382    let count = cur.u32()?;
383    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
384    for _ in 0..count {
385        out.push(ChunkRecord {
386            hash: cur.hash()?,
387            data_offset: cur.u64()?,
388            len_raw: cur.u32()?,
389            len_stored: cur.u32()?,
390            flags: cur.u32()?,
391        });
392    }
393    Ok(out)
394}
395
396fn decode_segments(buf: &[u8]) -> Result<Vec<SegmentRecord>> {
397    let mut cur = Cursor::new(buf, "segments");
398    let count = cur.u32()?;
399    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
400    for _ in 0..count {
401        let segment_id = cur.u64()?;
402        let track_id = cur.u32()?;
403        let pts_start = cur.u64()?;
404        let duration = cur.u32()?;
405        let flags = cur.u32()?;
406        let n = cur.u32()?;
407        let mut chunks = Vec::with_capacity((n as usize).min(buf.len()));
408        for _ in 0..n {
409            chunks.push(cur.u32()?);
410        }
411        out.push(SegmentRecord {
412            segment_id,
413            track_id,
414            pts_start,
415            duration,
416            flags,
417            chunks,
418        });
419    }
420    Ok(out)
421}
422
423fn decode_meta(buf: &[u8]) -> Result<Vec<(String, String)>> {
424    let mut cur = Cursor::new(buf, "meta");
425    let count = cur.u32()?;
426    let mut out = Vec::with_capacity((count as usize).min(buf.len()));
427    for _ in 0..count {
428        let key = cur.str16()?;
429        let value_bytes = cur.bytes32()?;
430        let value = String::from_utf8(value_bytes).map_err(|_| FormatError::Malformed("meta"))?;
431        out.push((key, value));
432    }
433    Ok(out)
434}
435
436fn decode_integrity(buf: &[u8]) -> Result<Integrity> {
437    let mut cur = Cursor::new(buf, "integrity");
438    Ok(Integrity {
439        merkle_root: cur.hash()?,
440        chunk_count: cur.u64()?,
441        total_raw: cur.u64()?,
442        total_stored: cur.u64()?,
443    })
444}