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_chunk(&mut self, raw: &[u8]) -> Result<u32> {
109 self.logical_raw += raw.len() as u64;
110 self.logical_chunks += 1;
111 let hash = hash_chunk(raw);
112 let interned = self.cas.intern(hash, self.chunks.len() as u32);
113 if !interned.is_new() {
114 return Ok(interned.index());
115 }
116
117 let mut flags = 0u32;
118 let stored: Vec<u8>;
119 let stored_slice: &[u8] =
120 if self.compression == COMPRESSION_ZSTD && raw.len() >= COMPRESS_MIN_LEN {
121 stored = zstd::bulk::compress(raw, self.zstd_level).map_err(FormatError::Zstd)?;
122 if stored.len() < raw.len() - raw.len() / 16 {
124 flags |= CHUNK_FLAG_ZSTD;
125 &stored
126 } else {
127 raw
128 }
129 } else {
130 raw
131 };
132
133 self.out.write_all(stored_slice)?;
134 self.data_hasher.update(stored_slice);
135 self.chunks.push(ChunkRecord {
136 hash,
137 data_offset: self.data_len,
138 len_raw: raw.len() as u32,
139 len_stored: stored_slice.len() as u32,
140 flags,
141 });
142 self.data_len += stored_slice.len() as u64;
143 Ok(interned.index())
144 }
145
146 pub fn add_track(&mut self, track: TrackRecord) -> Result<()> {
147 for &c in &track.init_chunks {
148 self.check_chunk_index(c)?;
149 }
150 self.tracks.push(track);
151 Ok(())
152 }
153
154 pub fn add_segment(&mut self, segment: SegmentRecord) -> Result<()> {
155 for &c in &segment.chunks {
156 self.check_chunk_index(c)?;
157 }
158 self.segments.push(segment);
159 Ok(())
160 }
161
162 pub fn pin_dict(&mut self, chunk_index: u32) -> Result<()> {
165 self.check_chunk_index(chunk_index)?;
166 if !self.dict.contains(&chunk_index) {
167 self.dict.push(chunk_index);
168 }
169 Ok(())
170 }
171
172 pub fn set_meta(&mut self, key: &str, value: &str) {
173 self.meta.push((key.to_string(), value.to_string()));
174 }
175
176 pub fn chunk_count(&self) -> u32 {
177 self.chunks.len() as u32
178 }
179
180 fn check_chunk_index(&self, index: u32) -> Result<()> {
181 if (index as usize) < self.chunks.len() {
182 Ok(())
183 } else {
184 Err(FormatError::ChunkIndexOutOfRange(index))
185 }
186 }
187
188 pub fn finish(mut self) -> Result<PackStats> {
190 let hashes: Vec<ChunkHash> = self.chunks.iter().map(|c| c.hash).collect();
191 let root = merkle_root(&hashes);
192 let unique_raw: u64 = self.chunks.iter().map(|c| c.len_raw as u64).sum();
193 let stored: u64 = self.chunks.iter().map(|c| c.len_stored as u64).sum();
194
195 let integrity = Integrity {
196 merkle_root: root,
197 chunk_count: self.chunks.len() as u64,
198 total_raw: unique_raw,
199 total_stored: stored,
200 };
201
202 if let Some(signer) = &self.signer {
203 let message = content_signature_message(&root, integrity.chunk_count);
204 let sig = signer.sign(&message);
205 let sig_hex: String = sig.to_bytes().iter().map(|b| format!("{b:02x}")).collect();
206 let pk_hex: String = signer
207 .verifying_key()
208 .to_bytes()
209 .iter()
210 .map(|b| format!("{b:02x}"))
211 .collect();
212 self.meta.push(("sig.ed25519".to_string(), sig_hex));
213 self.meta.push(("sig.pubkey".to_string(), pk_hex));
214 }
215
216 let mut dir: Vec<(SectionType, u64, u64, ChunkHash)> = vec![(
218 SectionType::Data,
219 SUPERBLOCK_LEN,
220 self.data_len,
221 self.data_hasher.finalize(),
222 )];
223
224 let sections: Vec<(SectionType, Vec<u8>)> = vec![
225 (SectionType::Tracks, encode_tracks(&self.tracks)),
226 (SectionType::Dict, encode_dict(&self.dict)),
227 (SectionType::Chunks, encode_chunks(&self.chunks)),
228 (SectionType::Segments, encode_segments(&self.segments)),
229 (SectionType::Meta, encode_meta(&self.meta)),
230 (SectionType::Integrity, encode_integrity(&integrity)),
231 ];
232
233 let mut offset = SUPERBLOCK_LEN + self.data_len;
234 for (ty, bytes) in §ions {
235 self.out.write_all(bytes)?;
236 dir.push((*ty, offset, bytes.len() as u64, hash_chunk(bytes)));
237 offset += bytes.len() as u64;
238 }
239
240 let section_dir_offset = offset;
241 let mut dir_bytes = Vec::with_capacity(dir.len() * crate::SECTION_DIR_ENTRY_LEN);
242 for (ty, off, len, hash) in &dir {
243 put_u32(&mut dir_bytes, *ty as u32);
244 put_u64(&mut dir_bytes, *off);
245 put_u64(&mut dir_bytes, *len);
246 dir_bytes.extend_from_slice(hash);
247 }
248 self.out.write_all(&dir_bytes)?;
249 let file_size = section_dir_offset + dir_bytes.len() as u64;
250
251 let mut sb = Vec::with_capacity(SUPERBLOCK_LEN as usize);
253 sb.extend_from_slice(&MAGIC);
254 put_u16(&mut sb, VERSION_MAJOR);
255 put_u16(&mut sb, VERSION_MINOR);
256 put_u32(&mut sb, 0); sb.push(cavs_hash::HashAlgo::Blake3 as u8);
258 sb.push(self.compression);
259 put_u16(&mut sb, 0); sb.extend_from_slice(&self.asset_uuid);
261 put_u32(&mut sb, self.timescale);
262 put_u32(&mut sb, dir.len() as u32);
263 put_u64(&mut sb, section_dir_offset);
264 put_u64(&mut sb, file_size);
265 sb.resize(SUPERBLOCK_LEN as usize, 0);
266
267 self.out.flush()?;
268 let file = self.out.get_mut();
269 file.seek(SeekFrom::Start(0))?;
270 file.write_all(&sb)?;
271 file.sync_all()?;
272
273 Ok(PackStats {
274 file_size,
275 unique_chunks: self.chunks.len() as u64,
276 logical_chunks: self.logical_chunks,
277 logical_raw: self.logical_raw,
278 unique_raw,
279 stored,
280 merkle_root: root,
281 })
282 }
283}
284
285fn encode_tracks(tracks: &[TrackRecord]) -> Vec<u8> {
286 let mut buf = Vec::new();
287 put_u32(&mut buf, tracks.len() as u32);
288 for t in tracks {
289 put_u32(&mut buf, t.track_id);
290 buf.push(t.kind as u8);
291 buf.push(t.flags);
292 put_str(&mut buf, &t.codec);
293 put_str(&mut buf, &t.name);
294 put_u32(&mut buf, t.timescale);
295 put_u32(&mut buf, t.init_chunks.len() as u32);
296 for &c in &t.init_chunks {
297 put_u32(&mut buf, c);
298 }
299 }
300 buf
301}
302
303fn encode_dict(dict: &[u32]) -> Vec<u8> {
304 let mut buf = Vec::new();
305 put_u32(&mut buf, dict.len() as u32);
306 for &c in dict {
307 put_u32(&mut buf, c);
308 }
309 buf
310}
311
312fn encode_chunks(chunks: &[ChunkRecord]) -> Vec<u8> {
313 let mut buf = Vec::with_capacity(4 + chunks.len() * 52);
314 put_u32(&mut buf, chunks.len() as u32);
315 for c in chunks {
316 buf.extend_from_slice(&c.hash);
317 put_u64(&mut buf, c.data_offset);
318 put_u32(&mut buf, c.len_raw);
319 put_u32(&mut buf, c.len_stored);
320 put_u32(&mut buf, c.flags);
321 }
322 buf
323}
324
325fn encode_segments(segments: &[SegmentRecord]) -> Vec<u8> {
326 let mut buf = Vec::new();
327 put_u32(&mut buf, segments.len() as u32);
328 for s in segments {
329 put_u64(&mut buf, s.segment_id);
330 put_u32(&mut buf, s.track_id);
331 put_u64(&mut buf, s.pts_start);
332 put_u32(&mut buf, s.duration);
333 put_u32(&mut buf, s.flags);
334 put_u32(&mut buf, s.chunks.len() as u32);
335 for &c in &s.chunks {
336 put_u32(&mut buf, c);
337 }
338 }
339 buf
340}
341
342fn encode_meta(meta: &[(String, String)]) -> Vec<u8> {
343 let mut buf = Vec::new();
344 put_u32(&mut buf, meta.len() as u32);
345 for (k, v) in meta {
346 put_str(&mut buf, k);
347 put_bytes32(&mut buf, v.as_bytes());
348 }
349 buf
350}
351
352fn encode_integrity(i: &Integrity) -> Vec<u8> {
353 let mut buf = Vec::with_capacity(56);
354 buf.extend_from_slice(&i.merkle_root);
355 put_u64(&mut buf, i.chunk_count);
356 put_u64(&mut buf, i.total_raw);
357 put_u64(&mut buf, i.total_stored);
358 buf
359}