1use crate::wire::*;
8use crate::{
9 ChunkRecord, FormatError, Integrity, Result, SectionType, SegmentRecord, TrackRecord,
10 CHUNK_FLAG_ZSTD, COMPRESSION_NONE, COMPRESSION_ZSTD, MAGIC, SUPERBLOCK_LEN, VERSION_MAJOR,
11 VERSION_MINOR,
12};
13use cavs_hash::{content_signature_message, hash_chunk, merkle_root, ChunkHash, Hasher};
14use cavs_store::CasIndex;
15use ed25519_dalek::{Signer, SigningKey};
16use std::fs::File;
17use std::io::{BufWriter, Seek, SeekFrom, Write as _};
18use std::path::Path;
19
20const COMPRESS_MIN_LEN: usize = 512;
22
23#[derive(Debug, Clone)]
25pub struct PackStats {
26 pub file_size: u64,
27 pub unique_chunks: u64,
28 pub logical_chunks: u64,
29 pub logical_raw: u64,
31 pub unique_raw: u64,
33 pub stored: u64,
35 pub merkle_root: ChunkHash,
36}
37
38pub struct Writer {
39 out: BufWriter<File>,
40 cas: CasIndex,
41 chunks: Vec<ChunkRecord>,
42 tracks: Vec<TrackRecord>,
43 segments: Vec<SegmentRecord>,
44 dict: Vec<u32>,
45 meta: Vec<(String, String)>,
46 data_len: u64,
47 data_hasher: Hasher,
48 logical_raw: u64,
49 logical_chunks: u64,
50 compression: u8,
51 zstd_level: i32,
52 timescale: u32,
53 asset_uuid: [u8; 16],
54 signer: Option<SigningKey>,
55}
56
57impl Writer {
58 pub fn create(
61 path: &Path,
62 asset_uuid: [u8; 16],
63 timescale: u32,
64 compress: bool,
65 ) -> Result<Self> {
66 let mut out = BufWriter::new(File::create(path)?);
67 out.write_all(&[0u8; SUPERBLOCK_LEN as usize])?;
69 Ok(Self {
70 out,
71 cas: CasIndex::new(),
72 chunks: Vec::new(),
73 tracks: Vec::new(),
74 segments: Vec::new(),
75 dict: Vec::new(),
76 meta: Vec::new(),
77 data_len: 0,
78 data_hasher: Hasher::new(),
79 logical_raw: 0,
80 logical_chunks: 0,
81 compression: if compress {
82 COMPRESSION_ZSTD
83 } else {
84 COMPRESSION_NONE
85 },
86 zstd_level: 3,
87 timescale,
88 asset_uuid,
89 signer: None,
90 })
91 }
92
93 pub fn set_zstd_level(&mut self, level: i32) {
95 self.zstd_level = level;
96 }
97
98 pub fn sign_with(&mut self, secret: &[u8; 32]) {
103 self.signer = Some(SigningKey::from_bytes(secret));
104 }
105
106 pub fn add_chunks_parallel(
114 &mut self,
115 data: &[u8],
116 ranges: &[std::ops::Range<usize>],
117 ) -> Result<Vec<u32>> {
118 use rayon::prelude::*;
119
120 let hashes: Vec<ChunkHash> = ranges
122 .par_iter()
123 .map(|r| hash_chunk(&data[r.clone()]))
124 .collect();
125
126 let mut first_occurrence = vec![false; ranges.len()];
129 let mut batch_seen = std::collections::HashSet::new();
130 for (i, h) in hashes.iter().enumerate() {
131 if self.cas.get(h).is_none() && batch_seen.insert(*h) {
132 first_occurrence[i] = true;
133 }
134 }
135
136 let compress = self.compression == COMPRESSION_ZSTD;
137 let level = self.zstd_level;
138 let prepared: std::result::Result<Vec<Option<Vec<u8>>>, std::io::Error> = ranges
139 .par_iter()
140 .enumerate()
141 .map(|(i, r)| {
142 let raw = &data[r.clone()];
143 if !first_occurrence[i] || !compress || raw.len() < COMPRESS_MIN_LEN {
144 return Ok(None);
145 }
146 let c = zstd::bulk::compress(raw, level)?;
147 Ok((c.len() < raw.len() - raw.len() / 16).then_some(c))
149 })
150 .collect();
151 let prepared = prepared.map_err(FormatError::Zstd)?;
152
153 let mut out = Vec::with_capacity(ranges.len());
154 for ((r, hash), comp) in ranges.iter().zip(hashes).zip(prepared) {
155 let raw = &data[r.clone()];
156 self.logical_raw += raw.len() as u64;
157 self.logical_chunks += 1;
158 let interned = self.cas.intern(hash, self.chunks.len() as u32);
159 if !interned.is_new() {
160 out.push(interned.index());
161 continue;
162 }
163 let (stored_slice, flags): (&[u8], u32) = match &comp {
164 Some(c) => (c, CHUNK_FLAG_ZSTD),
165 None => (raw, 0),
166 };
167 self.out.write_all(stored_slice)?;
168 self.data_hasher.update(stored_slice);
169 self.chunks.push(ChunkRecord {
170 hash,
171 data_offset: self.data_len,
172 len_raw: raw.len() as u32,
173 len_stored: stored_slice.len() as u32,
174 flags,
175 });
176 self.data_len += stored_slice.len() as u64;
177 out.push(interned.index());
178 }
179 Ok(out)
180 }
181
182 pub fn add_chunk(&mut self, raw: &[u8]) -> Result<u32> {
185 self.logical_raw += raw.len() as u64;
186 self.logical_chunks += 1;
187 let hash = hash_chunk(raw);
188 let interned = self.cas.intern(hash, self.chunks.len() as u32);
189 if !interned.is_new() {
190 return Ok(interned.index());
191 }
192
193 let mut flags = 0u32;
194 let stored: Vec<u8>;
195 let stored_slice: &[u8] =
196 if self.compression == COMPRESSION_ZSTD && raw.len() >= COMPRESS_MIN_LEN {
197 stored = zstd::bulk::compress(raw, self.zstd_level).map_err(FormatError::Zstd)?;
198 if stored.len() < raw.len() - raw.len() / 16 {
200 flags |= CHUNK_FLAG_ZSTD;
201 &stored
202 } else {
203 raw
204 }
205 } else {
206 raw
207 };
208
209 self.out.write_all(stored_slice)?;
210 self.data_hasher.update(stored_slice);
211 self.chunks.push(ChunkRecord {
212 hash,
213 data_offset: self.data_len,
214 len_raw: raw.len() as u32,
215 len_stored: stored_slice.len() as u32,
216 flags,
217 });
218 self.data_len += stored_slice.len() as u64;
219 Ok(interned.index())
220 }
221
222 pub fn add_track(&mut self, track: TrackRecord) -> Result<()> {
223 for &c in &track.init_chunks {
224 self.check_chunk_index(c)?;
225 }
226 self.tracks.push(track);
227 Ok(())
228 }
229
230 pub fn add_segment(&mut self, segment: SegmentRecord) -> Result<()> {
231 for &c in &segment.chunks {
232 self.check_chunk_index(c)?;
233 }
234 self.segments.push(segment);
235 Ok(())
236 }
237
238 pub fn pin_dict(&mut self, chunk_index: u32) -> Result<()> {
241 self.check_chunk_index(chunk_index)?;
242 if !self.dict.contains(&chunk_index) {
243 self.dict.push(chunk_index);
244 }
245 Ok(())
246 }
247
248 pub fn set_meta(&mut self, key: &str, value: &str) {
249 self.meta.push((key.to_string(), value.to_string()));
250 }
251
252 pub fn chunk_count(&self) -> u32 {
253 self.chunks.len() as u32
254 }
255
256 fn check_chunk_index(&self, index: u32) -> Result<()> {
257 if (index as usize) < self.chunks.len() {
258 Ok(())
259 } else {
260 Err(FormatError::ChunkIndexOutOfRange(index))
261 }
262 }
263
264 pub fn finish(mut self) -> Result<PackStats> {
266 let hashes: Vec<ChunkHash> = self.chunks.iter().map(|c| c.hash).collect();
267 let root = merkle_root(&hashes);
268 let unique_raw: u64 = self.chunks.iter().map(|c| c.len_raw as u64).sum();
269 let stored: u64 = self.chunks.iter().map(|c| c.len_stored as u64).sum();
270
271 let integrity = Integrity {
272 merkle_root: root,
273 chunk_count: self.chunks.len() as u64,
274 total_raw: unique_raw,
275 total_stored: stored,
276 };
277
278 if let Some(signer) = &self.signer {
279 let message = content_signature_message(&root, integrity.chunk_count);
280 let sig = signer.sign(&message);
281 let sig_hex: String = sig.to_bytes().iter().map(|b| format!("{b:02x}")).collect();
282 let pk_hex: String = signer
283 .verifying_key()
284 .to_bytes()
285 .iter()
286 .map(|b| format!("{b:02x}"))
287 .collect();
288 self.meta.push(("sig.ed25519".to_string(), sig_hex));
289 self.meta.push(("sig.pubkey".to_string(), pk_hex));
290 }
291
292 let mut dir: Vec<(SectionType, u64, u64, ChunkHash)> = vec![(
294 SectionType::Data,
295 SUPERBLOCK_LEN,
296 self.data_len,
297 self.data_hasher.finalize(),
298 )];
299
300 let sections: Vec<(SectionType, Vec<u8>)> = vec![
301 (SectionType::Tracks, encode_tracks(&self.tracks)),
302 (SectionType::Dict, encode_dict(&self.dict)),
303 (SectionType::Chunks, encode_chunks(&self.chunks)),
304 (SectionType::Segments, encode_segments(&self.segments)),
305 (SectionType::Meta, encode_meta(&self.meta)),
306 (SectionType::Integrity, encode_integrity(&integrity)),
307 ];
308
309 let mut offset = SUPERBLOCK_LEN + self.data_len;
310 for (ty, bytes) in §ions {
311 self.out.write_all(bytes)?;
312 dir.push((*ty, offset, bytes.len() as u64, hash_chunk(bytes)));
313 offset += bytes.len() as u64;
314 }
315
316 let section_dir_offset = offset;
317 let mut dir_bytes = Vec::with_capacity(dir.len() * crate::SECTION_DIR_ENTRY_LEN);
318 for (ty, off, len, hash) in &dir {
319 put_u32(&mut dir_bytes, *ty as u32);
320 put_u64(&mut dir_bytes, *off);
321 put_u64(&mut dir_bytes, *len);
322 dir_bytes.extend_from_slice(hash);
323 }
324 self.out.write_all(&dir_bytes)?;
325 let file_size = section_dir_offset + dir_bytes.len() as u64;
326
327 let mut sb = Vec::with_capacity(SUPERBLOCK_LEN as usize);
329 sb.extend_from_slice(&MAGIC);
330 put_u16(&mut sb, VERSION_MAJOR);
331 put_u16(&mut sb, VERSION_MINOR);
332 put_u32(&mut sb, 0); sb.push(cavs_hash::HashAlgo::Blake3 as u8);
334 sb.push(self.compression);
335 put_u16(&mut sb, 0); sb.extend_from_slice(&self.asset_uuid);
337 put_u32(&mut sb, self.timescale);
338 put_u32(&mut sb, dir.len() as u32);
339 put_u64(&mut sb, section_dir_offset);
340 put_u64(&mut sb, file_size);
341 sb.resize(SUPERBLOCK_LEN as usize, 0);
342
343 self.out.flush()?;
344 let file = self.out.get_mut();
345 file.seek(SeekFrom::Start(0))?;
346 file.write_all(&sb)?;
347 file.sync_all()?;
348
349 Ok(PackStats {
350 file_size,
351 unique_chunks: self.chunks.len() as u64,
352 logical_chunks: self.logical_chunks,
353 logical_raw: self.logical_raw,
354 unique_raw,
355 stored,
356 merkle_root: root,
357 })
358 }
359}
360
361fn encode_tracks(tracks: &[TrackRecord]) -> Vec<u8> {
362 let mut buf = Vec::new();
363 put_u32(&mut buf, tracks.len() as u32);
364 for t in tracks {
365 put_u32(&mut buf, t.track_id);
366 buf.push(t.kind as u8);
367 buf.push(t.flags);
368 put_str(&mut buf, &t.codec);
369 put_str(&mut buf, &t.name);
370 put_u32(&mut buf, t.timescale);
371 put_u32(&mut buf, t.init_chunks.len() as u32);
372 for &c in &t.init_chunks {
373 put_u32(&mut buf, c);
374 }
375 }
376 buf
377}
378
379fn encode_dict(dict: &[u32]) -> Vec<u8> {
380 let mut buf = Vec::new();
381 put_u32(&mut buf, dict.len() as u32);
382 for &c in dict {
383 put_u32(&mut buf, c);
384 }
385 buf
386}
387
388fn encode_chunks(chunks: &[ChunkRecord]) -> Vec<u8> {
389 let mut buf = Vec::with_capacity(4 + chunks.len() * 52);
390 put_u32(&mut buf, chunks.len() as u32);
391 for c in chunks {
392 buf.extend_from_slice(&c.hash);
393 put_u64(&mut buf, c.data_offset);
394 put_u32(&mut buf, c.len_raw);
395 put_u32(&mut buf, c.len_stored);
396 put_u32(&mut buf, c.flags);
397 }
398 buf
399}
400
401fn encode_segments(segments: &[SegmentRecord]) -> Vec<u8> {
402 let mut buf = Vec::new();
403 put_u32(&mut buf, segments.len() as u32);
404 for s in segments {
405 put_u64(&mut buf, s.segment_id);
406 put_u32(&mut buf, s.track_id);
407 put_u64(&mut buf, s.pts_start);
408 put_u32(&mut buf, s.duration);
409 put_u32(&mut buf, s.flags);
410 put_u32(&mut buf, s.chunks.len() as u32);
411 for &c in &s.chunks {
412 put_u32(&mut buf, c);
413 }
414 }
415 buf
416}
417
418fn encode_meta(meta: &[(String, String)]) -> Vec<u8> {
419 let mut buf = Vec::new();
420 put_u32(&mut buf, meta.len() as u32);
421 for (k, v) in meta {
422 put_str(&mut buf, k);
423 put_bytes32(&mut buf, v.as_bytes());
424 }
425 buf
426}
427
428fn encode_integrity(i: &Integrity) -> Vec<u8> {
429 let mut buf = Vec::with_capacity(56);
430 buf.extend_from_slice(&i.merkle_root);
431 put_u64(&mut buf, i.chunk_count);
432 put_u64(&mut buf, i.total_raw);
433 put_u64(&mut buf, i.total_stored);
434 buf
435}