1use 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum SignatureStatus {
40 Unsigned,
42 Valid([u8; 32]),
44}
45
46impl Reader {
47 pub fn open(path: &Path) -> Result<Self> {
48 let mut file = File::open(path)?;
49 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()?; 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 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 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 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 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 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 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 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 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 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 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 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 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 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}