1use crate::canonical;
10use crate::chunk::{self, ChunkParams, Hash};
11use crate::codec::{self, CodecChoice, CodecOptions};
12use crate::container;
13use crate::crypto::{self, ArchiveKey, KdfParams};
14use crate::error::{Error, Result};
15use crate::format::{self, section, Header, SectionEntry, SignatureBlock};
16use crate::manifest::{BlobRecord, ChunkIndex, ChunkRef, Derived, Entry, Limits, Manifest, RefArchive};
17use crate::paths;
18use crate::read::Reader;
19use fastcdc::v2020::{Normalization, StreamCDC};
20use rayon::prelude::*;
21use serde::Serialize;
22use serde_bytes::ByteBuf;
23use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
24use std::fs::File;
25use std::io::{BufReader, BufWriter, Write};
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28
29#[derive(Clone, Debug)]
30pub struct PackOptions {
31 pub chunk: ChunkParams,
32 pub block_size: u32,
35 pub level: i32,
36 pub codec: CodecChoice,
38 pub effort: u8,
43 pub train_dict: bool,
45 pub dict_size: usize,
46 pub dict_warmup: usize,
47 pub container_aware: bool,
48 pub password: Option<String>,
49 pub kdf: KdfParams,
50 pub sign_seed: Option<[u8; 32]>,
51 pub keep_mtime: bool,
52 pub timestamp: bool,
53 pub profile: String,
54 pub limits: Limits,
55 pub batch: usize,
57 pub references: Vec<PathBuf>,
59 pub recipients: Vec<crypto::hybrid::Recipient>,
61 pub canonical: bool,
64 pub delta: bool,
68 pub jpeg_recompression: bool,
72}
73
74impl Default for PackOptions {
75 fn default() -> Self {
76 Self {
77 chunk: ChunkParams::P2P,
78 block_size: 1 << 20,
79 level: 19,
80 codec: CodecChoice::Best,
81 effort: codec::EFFORT_DEFAULT,
82 train_dict: true,
83 dict_size: 110 * 1024,
84 dict_warmup: 8 << 20,
85 container_aware: true,
86 password: None,
87 kdf: KdfParams::default(),
88 sign_seed: None,
89 keep_mtime: false,
90 timestamp: false,
91 profile: "block".into(),
92 limits: Limits::default(),
93 batch: 0,
94 references: Vec::new(),
95 recipients: Vec::new(),
96 canonical: false,
97 delta: true,
98 jpeg_recompression: true,
99 }
100 }
101}
102
103#[derive(Clone, Debug, Serialize)]
104pub struct PackReport {
105 pub archive_bytes: u64,
106 pub input_bytes: u64,
107 pub logical_bytes: u64,
108 pub unique_bytes: u64,
109 pub chunks_total: usize,
110 pub chunks_unique: usize,
111 pub blobs: usize,
112 pub dict_bytes: usize,
113 pub codec_hist: BTreeMap<String, u32>,
114 pub blocks_sampled: usize,
117 pub entries: usize,
118 pub containers_exploded: usize,
119 pub containers_fallback: usize,
120 pub root: String,
121 pub encrypted: bool,
122 pub signed: bool,
123 pub referenced_chunks: usize,
124 pub referenced_bytes: u64,
125 pub views: usize,
127 pub skipped_links: usize,
129}
130
131struct Block {
133 chunks: Vec<u32>,
134 plain: Vec<u8>,
135 delta: Option<Vec<u32>>,
137}
138
139const DELTA_WINDOW: usize = 32 << 20;
141const DELTA_DICT_MAX: usize = 4 << 20;
143const DELTA_WHOLE_FILE_MAX: u64 = 64 << 20;
145const MAX_WRITER_DELTA_DEPTH: u8 = 6;
148
149enum DictSource {
150 Ref { ri: usize, chunks: Vec<(Hash, u32)> },
152 Local { chunks: Vec<u32> },
154}
155
156struct DeltaCandidate {
157 source: DictSource,
158}
159
160fn shares_content(old: &[u8], parts: &[&[u8]]) -> bool {
164 if old.is_empty() {
165 return false;
166 }
167 let fine = ChunkParams::FINE;
168 let prints: HashSet<Hash> = chunk::boundaries(old, &fine).into_iter().map(|(off, len)| chunk::hash(&old[off..off + len])).collect();
169 let (mut shared, mut total) = (0usize, 0usize);
170 for part in parts {
171 for (off, len) in chunk::boundaries(part, &fine) {
172 total += len;
173 if prints.contains(&chunk::hash(&part[off..off + len])) {
174 shared += len;
175 }
176 }
177 }
178 shared > 0 && shared * 4 >= total
179}
180
181fn entry_chunk_indices(e: &Entry) -> Vec<u32> {
183 match (&e.zip, &e.streams) {
184 (Some(z), _) => z.members.iter().flat_map(|m| m.chunks.iter().copied()).collect(),
185 (None, Some(s)) => s.segments.iter().flat_map(|x| x.chunks.iter().copied()).collect(),
186 _ => e.chunks.clone(),
187 }
188}
189
190fn name_key(path: &str) -> Option<(String, [u8; 3])> {
193 let name = file_name(path).as_bytes();
194 if name.len() < 3 {
195 return None;
196 }
197 Some((extension(path).to_string(), [name[0], name[1], name[2]]))
198}
199
200type NameIndex = HashMap<(String, [u8; 3]), Vec<usize>>;
201
202fn file_name(path: &str) -> &str {
203 path.rsplit('/').next().unwrap_or(path)
204}
205
206fn extension(path: &str) -> &str {
207 let name = file_name(path);
208 match name.rfind('.') {
209 Some(i) if i > 0 => &name[i + 1..],
210 _ => "",
211 }
212}
213
214fn common_prefix(a: &str, b: &str) -> usize {
216 let n = a.bytes().zip(b.bytes()).take_while(|(x, y)| x == y).count();
217 let mut n = n;
218 while !a.is_char_boundary(n) {
219 n -= 1;
220 }
221 n
222}
223
224struct Packer {
225 opts: PackOptions,
226 out: BufWriter<File>,
227 pos: u64,
228 header: [u8; format::HEADER_LEN],
229 sections: Vec<SectionEntry>,
230 key: Option<ArchiveKey>,
231 hashes: Vec<Hash>,
233 sizes: Vec<u32>,
234 external: Vec<u8>,
235 index: HashMap<Hash, u32>,
236 ref_lookup: HashMap<Hash, (usize, u32)>,
238 ref_meta: Vec<RefArchive>,
239 refs: Vec<Reader>,
240 ref_path_index: Vec<HashMap<String, usize>>,
242 ref_name_index: Vec<NameIndex>,
243 name_index: NameIndex,
245 cur_delta: Option<Vec<u32>>,
247 delta_depth: Vec<u8>,
249 recent: VecDeque<(u32, Arc<Vec<u8>>)>,
250 recent_map: HashMap<u32, Arc<Vec<u8>>>,
251 recent_bytes: usize,
252 referenced_chunks: usize,
253 referenced_bytes: u64,
254 current: Option<Block>,
256 ready: Vec<Block>,
257 blob_records: Vec<BlobRecord>,
258 blobs_off: u64,
259 blobs_written: u64,
260 codec_hist: BTreeMap<String, u32>,
261 blocks_sampled: usize,
262 dict: Option<Vec<u8>>,
264 dict_done: bool,
265 warmup: Vec<(u32, Vec<u8>)>,
266 warmup_len: usize,
267 entries: Vec<Entry>,
269 input_bytes: u64,
270 logical_bytes: u64,
271 unique_bytes: u64,
272 chunk_refs: usize,
273 exploded: usize,
274 fallback: usize,
275 views: usize,
276}
277
278impl Packer {
279 fn new(opts: PackOptions, out_path: &Path) -> Result<Self> {
280 opts.chunk.validate()?;
281 if !(1..=5).contains(&opts.effort) {
282 return Err(Error::Invalid(format!("effort {} is outside 1..=5", opts.effort)));
283 }
284 let key = if opts.password.is_some() || !opts.recipients.is_empty() { Some(ArchiveKey::random()?) } else { None };
285 let mut flags = 0u16;
286 if key.is_some() {
287 flags |= format::FLAG_ENCRYPTED;
288 }
289 if opts.sign_seed.is_some() {
290 flags |= format::FLAG_SIGNED;
291 }
292 let header = format::encode_header(&Header { version: format::VERSION, flags });
293 let file = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(true).open(out_path)?;
295 let mut out = BufWriter::with_capacity(1 << 20, file);
296 out.write_all(&header)?;
297 let mut ref_lookup: HashMap<Hash, (usize, u32)> = HashMap::new();
299 let mut ref_meta = Vec::new();
300 let mut refs = Vec::new();
301 let mut ref_path_index = Vec::new();
302 let mut ref_name_index = Vec::new();
303 for (i, rp) in opts.references.iter().enumerate() {
304 let r = Reader::open(rp, opts.password.as_deref()).map_err(|e| Error::Missing(format!("reference archive {}: {e}", rp.display())))?;
305 for (cj, c) in r.index.chunks.iter().enumerate() {
306 if c.x == 0 && c.h.len() == 32 {
307 let mut h = [0u8; 32];
308 h.copy_from_slice(&c.h);
309 ref_lookup.entry(h).or_insert((i, cj as u32));
310 }
311 }
312 ref_meta.push(RefArchive { root: r.manifest.root.clone(), hint: rp.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(), chunks: 0, bytes: 0 });
313 let mut paths = HashMap::new();
314 let mut names: NameIndex = HashMap::new();
315 for (ei, e) in r.manifest.entries.iter().enumerate() {
316 if e.derived.is_some() {
317 continue;
318 }
319 paths.entry(e.path.clone()).or_insert(ei);
320 if let Some(k) = name_key(&e.path) {
321 names.entry(k).or_default().push(ei);
322 }
323 }
324 ref_path_index.push(paths);
325 ref_name_index.push(names);
326 refs.push(r);
327 }
328 let mut p = Self {
329 opts,
330 out,
331 pos: header.len() as u64,
332 header,
333 sections: Vec::new(),
334 key,
335 hashes: Vec::new(),
336 sizes: Vec::new(),
337 external: Vec::new(),
338 index: HashMap::new(),
339 ref_lookup,
340 ref_meta,
341 refs,
342 ref_path_index,
343 ref_name_index,
344 name_index: HashMap::new(),
345 cur_delta: None,
346 delta_depth: Vec::new(),
347 recent: VecDeque::new(),
348 recent_map: HashMap::new(),
349 recent_bytes: 0,
350 referenced_chunks: 0,
351 referenced_bytes: 0,
352 current: None,
353 ready: Vec::new(),
354 blob_records: Vec::new(),
355 blobs_off: 0,
356 blobs_written: 0,
357 codec_hist: BTreeMap::new(),
358 blocks_sampled: 0,
359 dict: None,
360 dict_done: false,
361 warmup: Vec::new(),
362 warmup_len: 0,
363 entries: Vec::new(),
364 input_bytes: 0,
365 logical_bytes: 0,
366 unique_bytes: 0,
367 chunk_refs: 0,
368 exploded: 0,
369 fallback: 0,
370 views: 0,
371 };
372 if let Some(k) = &p.key {
373 let mut block = crypto::RecipientsBlock::default();
374 if let Some(pw) = &p.opts.password {
375 block.stanzas.push(crypto::wrap_password(k, pw.as_bytes(), &p.opts.kdf)?);
376 }
377 for r in &p.opts.recipients {
378 block.stanzas.push(crypto::hybrid::wrap(k, r)?);
379 }
380 let bytes = format::cbor_encode(&block)?;
381 p.write_section(section::RECIPIENTS, &bytes)?;
382 }
383 if p.opts.block_size > 0 || !p.opts.train_dict {
385 p.dict_done = true;
386 }
387 p.blobs_off = p.pos;
388 Ok(p)
389 }
390
391 fn write_section(&mut self, t: u8, bytes: &[u8]) -> Result<()> {
392 let off = self.pos;
393 self.out.write_all(bytes)?;
394 self.pos += bytes.len() as u64;
395 self.sections.push(SectionEntry { t, off, len: bytes.len() as u64, h: ByteBuf::from(chunk::hash(bytes).to_vec()) });
396 Ok(())
397 }
398
399 fn batch_size(&self) -> usize {
400 if self.opts.batch > 0 {
401 self.opts.batch
402 } else {
403 rayon::current_num_threads().max(1)
404 }
405 }
406
407 const MAX_INFLIGHT_BYTES: usize = 128 << 20;
410
411 fn batch_ready(&self) -> bool {
412 self.ready.len() >= self.batch_size() || self.ready.iter().map(|b| b.plain.len()).sum::<usize>() >= Self::MAX_INFLIGHT_BYTES
413 }
414
415 fn ingest_chunk(&mut self, data: &[u8]) -> Result<u32> {
417 let h = chunk::hash(data);
418 self.chunk_refs += 1;
419 self.logical_bytes += data.len() as u64;
420 if let Some(&i) = self.index.get(&h) {
421 return Ok(i);
422 }
423 let idx = self.hashes.len() as u32;
424 self.hashes.push(h);
425 self.sizes.push(data.len() as u32);
426 self.delta_depth.push(0);
427 self.index.insert(h, idx);
428 if let Some(&(ri, _)) = self.ref_lookup.get(&h) {
430 self.external.push(1);
431 self.referenced_chunks += 1;
432 self.referenced_bytes += data.len() as u64;
433 self.ref_meta[ri].chunks += 1;
434 self.ref_meta[ri].bytes += data.len() as u64;
435 return Ok(idx);
436 }
437 self.external.push(0);
438 self.unique_bytes += data.len() as u64;
439 self.remember(idx, data);
440
441 if self.opts.block_size == 0 {
442 if !self.dict_done {
443 self.warmup.push((idx, data.to_vec()));
444 self.warmup_len += data.len();
445 if self.warmup_len >= self.opts.dict_warmup {
446 self.finish_warmup()?;
447 }
448 } else {
449 self.ready.push(Block { chunks: vec![idx], plain: data.to_vec(), delta: None });
450 if self.batch_ready() {
451 self.flush_ready()?;
452 }
453 }
454 } else {
455 let full = match &self.current {
457 Some(b) => {
460 (b.plain.len() + data.len() > self.opts.block_size as usize && !b.plain.is_empty())
461 || self.cur_delta.as_ref().is_some_and(|d| (b.delta.is_some() && b.delta.as_ref() != Some(d)) || d.iter().any(|ci| b.chunks.contains(ci)))
462 }
463 None => false,
464 };
465 if full {
466 let b = self.current.take().unwrap();
467 self.ready.push(b);
468 if self.batch_ready() {
469 self.flush_ready()?;
470 }
471 }
472 match &mut self.current {
473 Some(b) => {
474 b.chunks.push(idx);
475 b.plain.extend_from_slice(data);
476 }
477 None => self.current = Some(Block { chunks: vec![idx], plain: data.to_vec(), delta: self.cur_delta.clone() }),
478 }
479 if let Some(b) = &mut self.current {
482 let adopted = b.delta.is_none() && self.cur_delta.is_some();
483 if adopted {
484 b.delta = self.cur_delta.clone();
485 }
486 if let Some(dict) = &b.delta {
487 let depth = dict.iter().map(|&ci| self.delta_depth[ci as usize]).max().unwrap_or(0).saturating_add(1);
488 if adopted {
489 for &ci in &b.chunks {
490 self.delta_depth[ci as usize] = self.delta_depth[ci as usize].max(depth);
491 }
492 } else {
493 self.delta_depth[idx as usize] = self.delta_depth[idx as usize].max(depth);
494 }
495 }
496 }
497 }
498 Ok(idx)
499 }
500
501 fn register_entry(&mut self, entry: Entry) {
503 if entry.derived.is_none() {
504 if let Some(k) = name_key(&entry.path) {
505 self.name_index.entry(k).or_default().push(self.entries.len());
506 }
507 }
508 self.entries.push(entry);
509 }
510
511 fn remember(&mut self, idx: u32, data: &[u8]) {
513 if !self.opts.delta || self.opts.block_size == 0 {
514 return;
515 }
516 let arc = Arc::new(data.to_vec());
517 self.recent_bytes += data.len();
518 self.recent.push_back((idx, arc.clone()));
519 self.recent_map.insert(idx, arc);
520 while self.recent_bytes > DELTA_WINDOW {
521 if let Some((old, bytes)) = self.recent.pop_front() {
522 self.recent_bytes -= bytes.len();
523 self.recent_map.remove(&old);
524 } else {
525 break;
526 }
527 }
528 }
529
530 fn intern_external(&mut self, h: Hash, size: u32, ri: usize) -> u32 {
532 if let Some(&i) = self.index.get(&h) {
533 return i;
534 }
535 let idx = self.hashes.len() as u32;
536 self.hashes.push(h);
537 self.sizes.push(size);
538 self.delta_depth.push(0);
539 self.external.push(1);
540 self.index.insert(h, idx);
541 self.referenced_chunks += 1;
542 self.referenced_bytes += size as u64;
543 self.ref_meta[ri].chunks += 1;
544 self.ref_meta[ri].bytes += size as u64;
545 idx
546 }
547
548 fn delta_candidates(&self, path: &str) -> Vec<DeltaCandidate> {
552 let name = file_name(path);
553 let ext = extension(path);
554 let similar = |other: &str| -> Option<usize> {
555 if other == path || extension(other) != ext {
556 return None;
557 }
558 let other_name = file_name(other);
559 let lcp = common_prefix(name, other_name);
560 (lcp >= 3 && lcp * 2 >= name.len().min(other_name.len())).then_some(lcp)
561 };
562 let mut ranked: Vec<(usize, usize, DeltaCandidate)> = Vec::new(); let key = name_key(path);
564 for (ri, r) in self.refs.iter().enumerate() {
565 let exact = self.ref_path_index[ri].get(path).copied();
566 let bucket = key.as_ref().and_then(|k| self.ref_name_index[ri].get(k)).map(|v| v.as_slice()).unwrap_or(&[]);
567 for &ei in exact.iter().chain(bucket.iter()) {
568 let e = &r.manifest.entries[ei];
569 let score = if e.path == path {
570 usize::MAX
571 } else {
572 match similar(&e.path) {
573 Some(l) => l,
574 None => continue,
575 }
576 };
577 let mut chunks = Vec::new();
578 for cj in entry_chunk_indices(e) {
579 let c = &r.index.chunks[cj as usize];
580 if c.x != 0 || c.h.len() != 32 {
581 continue;
582 }
583 let mut h = [0u8; 32];
584 h.copy_from_slice(&c.h);
585 chunks.push((h, c.s));
586 }
587 if !chunks.is_empty() {
588 ranked.push((score, ranked.len(), DeltaCandidate { source: DictSource::Ref { ri, chunks } }));
589 }
590 }
591 }
592 let bucket = key.as_ref().and_then(|k| self.name_index.get(k)).map(|v| v.as_slice()).unwrap_or(&[]);
593 for &ei in bucket {
594 let e = &self.entries[ei];
595 let Some(score) = similar(&e.path) else { continue };
596 let mut chunks = Vec::new();
597 for ci in entry_chunk_indices(e) {
598 if self.delta_depth[ci as usize] < MAX_WRITER_DELTA_DEPTH && (self.recent_map.contains_key(&ci) || self.external[ci as usize] != 0) {
600 chunks.push(ci);
601 }
602 }
603 if !chunks.is_empty() {
604 ranked.push((score, ranked.len(), DeltaCandidate { source: DictSource::Local { chunks } }));
605 }
606 }
607 ranked.sort_by_key(|x| std::cmp::Reverse((x.0, x.1)));
609 ranked.truncate(3);
610 ranked.into_iter().map(|(_, _, c)| c).collect()
611 }
612
613 fn dict_chunk_bytes(&mut self, ci: u32) -> Option<Vec<u8>> {
615 if self.external[ci as usize] != 0 {
616 let (ri, cj) = *self.ref_lookup.get(&self.hashes[ci as usize])?;
617 self.refs[ri].chunk(cj).ok()
618 } else {
619 self.recent_map.get(&ci).map(|b| b.to_vec())
620 }
621 }
622
623 fn confirm_delta(&mut self, cand: DeltaCandidate, parts: &[&[u8]]) -> Option<Vec<u32>> {
626 let mut dict = Vec::new();
628 let mut out = Vec::new();
629 match cand.source {
630 DictSource::Ref { ri, chunks } => {
631 for (h, size) in chunks {
632 if dict.len() + size as usize > DELTA_DICT_MAX {
633 break;
634 }
635 let (_, cj) = *self.ref_lookup.get(&h)?;
636 let bytes = self.refs[ri].chunk(cj).ok()?;
637 dict.extend_from_slice(&bytes);
638 out.push((h, size));
639 }
640 if !shares_content(&dict, parts) {
642 return None;
643 }
644 let idx: Vec<u32> = out.into_iter().map(|(h, size)| self.intern_external(h, size, ri)).collect();
645 (!idx.is_empty()).then_some(idx)
646 }
647 DictSource::Local { chunks } => {
648 let mut idx = Vec::new();
649 for ci in chunks {
650 let bytes = self.dict_chunk_bytes(ci)?;
651 if dict.len() + bytes.len() > DELTA_DICT_MAX {
652 break;
653 }
654 dict.extend_from_slice(&bytes);
655 idx.push(ci);
656 }
657 if !shares_content(&dict, parts) {
658 return None;
659 }
660 (!idx.is_empty()).then_some(idx)
661 }
662 }
663 }
664
665 fn drop_delta_if_base_in_block(&mut self, incoming: usize) {
668 if let (Some(d), Some(b)) = (&self.cur_delta, &self.current) {
669 if b.delta.is_some() || b.plain.len() + incoming / 2 > self.opts.block_size as usize {
671 return;
672 }
673 let (mut inside, mut outside) = (0usize, 0usize);
674 for &ci in d {
675 let size = self.sizes[ci as usize] as usize;
676 if b.chunks.contains(&ci) {
677 inside += size;
678 } else {
679 outside += size;
680 }
681 }
682 if outside * 4 < inside + outside {
683 self.cur_delta = None;
684 }
685 }
686 }
687
688 fn delta_dictionary(&mut self, chunks: &[u32]) -> Option<Vec<u8>> {
690 let mut dict = Vec::new();
691 for &ci in chunks {
692 dict.extend_from_slice(&self.dict_chunk_bytes(ci)?);
693 }
694 Some(dict)
695 }
696
697 fn finish_warmup(&mut self) -> Result<()> {
699 if self.dict_done {
700 return Ok(());
701 }
702 self.dict_done = true;
703 let samples: Vec<&[u8]> = self.warmup.iter().map(|(_, d)| d.as_slice()).collect();
704 self.dict = codec::train_dict(&samples, self.opts.dict_size);
705 let warm = std::mem::take(&mut self.warmup);
706 for (idx, data) in warm {
707 self.ready.push(Block { chunks: vec![idx], plain: data, delta: None });
708 if self.batch_ready() {
709 self.flush_ready()?;
710 }
711 }
712 Ok(())
713 }
714
715 fn flush_ready(&mut self) -> Result<()> {
717 if self.ready.is_empty() {
718 return Ok(());
719 }
720 let mut blocks = std::mem::take(&mut self.ready);
721 let copts = CodecOptions { level: self.opts.level, long_window_log: 27, dict: self.dict.clone(), choice: self.opts.codec, effort: self.opts.effort };
722 let mut dicts: Vec<Option<Vec<u8>>> = Vec::with_capacity(blocks.len());
724 for b in blocks.iter_mut() {
725 let d = match &b.delta {
726 Some(chunks) => self.delta_dictionary(chunks),
727 None => None,
728 };
729 if d.is_none() {
730 b.delta = None;
731 }
732 dicts.push(d);
733 }
734 let level = self.opts.level;
735 let compressed: Vec<codec::Encoded> = blocks
736 .par_iter()
737 .zip(dicts.par_iter())
738 .map(|(b, d)| {
739 let mut e = codec::compress_best(&b.plain, &copts)?;
740 if let Some(dict) = d {
741 let z = codec::zstd_compress_with_prefix(&b.plain, level, dict)?;
742 if z.len() < e.bytes.len() {
743 e = codec::Encoded { codec: codec::ZSTD_DELTA, bytes: z, params: Vec::new(), filter: codec::FILTER_NONE };
744 }
745 }
746 Ok(e)
747 })
748 .collect::<Result<Vec<_>>>()?;
749 for (b, e) in blocks.into_iter().zip(compressed) {
750 let c = e.codec;
751 *self.codec_hist.entry(codec::name(c).to_string()).or_insert(0) += 1;
752 if codec::samples_block(b.plain.len(), &copts) {
753 self.blocks_sampled += 1;
754 }
755 let blob_index = self.blob_records.len() as u64;
756 let aad = blob_aad(c, b.plain.len() as u32, b.chunks[0], b.chunks.len() as u32);
757 let stored = match &self.key {
758 Some(k) => crypto::seal(k, crypto::LABEL_BLOB, blob_index, &aad, &e.bytes)?,
759 None => e.bytes,
760 };
761 match codec::filter_id(e.filter) {
762 codec::FILTER_NONE => {}
763 codec::FILTER_X86 => *self.codec_hist.entry("filter:x86".to_string()).or_insert(0) += 1,
764 codec::FILTER_ARM64 => *self.codec_hist.entry("filter:arm64".to_string()).or_insert(0) += 1,
765 _ => *self.codec_hist.entry("filter:other".to_string()).or_insert(0) += 1,
766 }
767 let d = if c == codec::ZSTD_DELTA { b.delta.unwrap_or_default() } else { Vec::new() };
768 self.blob_records.push(BlobRecord { off: self.blobs_written, clen: stored.len() as u32, ulen: b.plain.len() as u32, codec: c, n: b.chunks, p: e.params, f: e.filter, d });
769 self.out.write_all(&stored)?;
770 self.blobs_written += stored.len() as u64;
771 self.pos += stored.len() as u64;
772 }
773 Ok(())
774 }
775
776 fn entry_skeleton(&self, path: String, size: u64, hash: Hash, fs_path: &Path) -> Entry {
777 let mtime = if self.opts.keep_mtime {
778 std::fs::metadata(fs_path).ok().and_then(|m| m.modified().ok()).and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map(|d| d.as_secs() as i64)
779 } else {
780 None
781 };
782 Entry { path, size, h: ByteBuf::from(hash.to_vec()), mode: "raw".into(), chunks: Vec::new(), mtime, zip: None, streams: None, note: None, derived: None }
783 }
784
785 fn add_file(&mut self, fs_path: &Path, arcname: &str) -> Result<()> {
786 let path = paths::normalize(arcname)?;
787 let size = std::fs::metadata(fs_path)?.len();
788 if size > self.opts.limits.max_entry {
789 return Err(Error::Limit(format!("{path}: entry larger than max_entry")));
790 }
791 self.input_bytes += size;
792 if self.input_bytes > self.opts.limits.max_total {
793 return Err(Error::Limit("total input larger than max_total".into()));
794 }
795
796 let candidates = if self.opts.delta && self.opts.block_size > 0 { self.delta_candidates(&path) } else { Vec::new() };
798 self.cur_delta = None;
799
800 let is_container = self.opts.container_aware && (container::is_zip_like(&path) || container::is_stream_container(&path)) && size > 64;
802 let wants_view = self.opts.canonical && canonical::derivable(&path).is_some();
803 if is_container || wants_view || (!candidates.is_empty() && size <= DELTA_WHOLE_FILE_MAX) {
804 let data = std::fs::read(fs_path)?;
805 if is_container {
806 self.add_container(&path, &data, size, fs_path, candidates)?;
807 } else {
808 for c in candidates {
809 if let Some(d) = self.confirm_delta(c, &[&data]) {
810 self.cur_delta = Some(d);
811 break;
812 }
813 }
814 self.drop_delta_if_base_in_block(data.len());
815 let mut entry = self.entry_skeleton(path.clone(), size, chunk::hash(&data), fs_path);
816 entry.chunks = self.chunk_buffer(&data)?;
817 self.register_entry(entry);
818 self.cur_delta = None;
819 }
820 if wants_view {
821 self.cur_delta = None;
822 self.add_view(&path, &data)?;
823 }
824 return Ok(());
825 }
826
827 let file = File::open(fs_path)?;
829 let reader = BufReader::with_capacity(1 << 20, file);
830 let p = self.opts.chunk;
831 let mut hasher = blake3::Hasher::new();
832 let mut refs = Vec::new();
833 let mut total = 0u64;
834 if size > 0 {
835 let cdc = StreamCDC::with_level(reader, p.min, p.avg, p.max, Normalization::Level2);
836 for item in cdc {
837 let c = item.map_err(|e| Error::Io(std::io::Error::other(format!("chunking {}: {e}", fs_path.display()))))?;
838 hasher.update(&c.data);
839 total += c.data.len() as u64;
840 refs.push(self.ingest_chunk(&c.data)?);
841 }
842 }
843 if total != size {
844 return Err(Error::Io(std::io::Error::other(format!("{}: size changed while reading", fs_path.display()))));
845 }
846 let mut entry = self.entry_skeleton(path, size, *hasher.finalize().as_bytes(), fs_path);
847 entry.chunks = refs;
848 self.register_entry(entry);
849 Ok(())
850 }
851
852 fn add_container(&mut self, path: &str, data: &[u8], size: u64, fs_path: &Path, mut candidates: Vec<DeltaCandidate>) -> Result<()> {
855 let mut entry = self.entry_skeleton(path.to_string(), size, chunk::hash(data), fs_path);
856 if container::is_zip_like(path) {
857 if let Some((mut recipe, parts)) = container::try_bitexact(data) {
858 let views: Vec<&[u8]> = parts.iter().map(|p| p.as_slice()).collect();
859 for c in std::mem::take(&mut candidates) {
860 if let Some(d) = self.confirm_delta(c, &views) {
861 self.cur_delta = Some(d);
862 break;
863 }
864 }
865 self.drop_delta_if_base_in_block(views.iter().map(|v| v.len()).sum());
866 for (m, part) in recipe.members.iter_mut().zip(parts.iter()) {
867 m.chunks = self.chunk_buffer(part)?;
868 }
869 self.cur_delta = None;
870 entry.mode = "zip".into();
871 entry.zip = Some(recipe);
872 self.exploded += 1;
873 self.register_entry(entry);
874 return Ok(());
875 }
876 entry.note = Some("container could not be rebuilt bit-exact; stored raw".into());
877 } else if let Some((mut recipe, parts)) = container::try_bitexact_streams(data, self.opts.jpeg_recompression) {
878 let views: Vec<&[u8]> = parts.iter().map(|p| p.as_slice()).collect();
879 for c in std::mem::take(&mut candidates) {
880 if let Some(d) = self.confirm_delta(c, &views) {
881 self.cur_delta = Some(d);
882 break;
883 }
884 }
885 self.drop_delta_if_base_in_block(views.iter().map(|v| v.len()).sum());
886 for (s, part) in recipe.segments.iter_mut().zip(parts.iter()) {
887 s.chunks = self.chunk_buffer(part)?;
888 }
889 self.cur_delta = None;
890 entry.mode = "streams".into();
891 entry.streams = Some(recipe);
892 self.exploded += 1;
893 self.register_entry(entry);
894 return Ok(());
895 } else {
896 entry.note = Some("no invertible zlib streams found; stored raw".into());
897 }
898 self.fallback += 1;
899 for c in std::mem::take(&mut candidates) {
900 if let Some(d) = self.confirm_delta(c, &[data]) {
901 self.cur_delta = Some(d);
902 break;
903 }
904 }
905 self.drop_delta_if_base_in_block(data.len());
906 entry.chunks = self.chunk_buffer(data)?;
907 self.cur_delta = None;
908 self.register_entry(entry);
909 Ok(())
910 }
911
912 fn add_view(&mut self, source: &str, data: &[u8]) -> Result<()> {
916 let Some(kind) = canonical::derivable(source) else { return Ok(()) };
917 let text = match canonical::canonical_view(source, data) {
918 Ok(Some(t)) => t,
919 Ok(None) => return Ok(()),
920 Err(e) => {
921 if let Some(entry) = self.entries.iter_mut().rev().find(|e| e.path == source) {
922 entry.note = Some(format!("canonical view not generated: {e}"));
923 }
924 return Ok(());
925 }
926 };
927 let vpath = paths::normalize(&canonical::view_path(source, kind))?;
928 let bytes = text.into_bytes();
929 let derived = Derived { from: source.to_string(), view: kind.to_string(), generator: crate::WRITER.to_string(), tokens_est: (bytes.len() as f64 / 3.5).ceil() as u64 };
930 let mut entry = Entry {
931 path: vpath,
932 size: bytes.len() as u64,
933 h: ByteBuf::from(chunk::hash(&bytes).to_vec()),
934 mode: "raw".into(),
935 chunks: Vec::new(),
936 mtime: None,
937 zip: None,
938 streams: None,
939 note: None,
940 derived: Some(derived),
941 };
942 entry.chunks = self.chunk_buffer(&bytes)?;
943 self.register_entry(entry);
944 self.views += 1;
945 Ok(())
946 }
947
948 fn chunk_buffer(&mut self, data: &[u8]) -> Result<Vec<u32>> {
949 let mut refs = Vec::new();
950 for (off, len) in chunk::boundaries(data, &self.opts.chunk) {
951 refs.push(self.ingest_chunk(&data[off..off + len])?);
952 }
953 Ok(refs)
954 }
955
956 fn finish(mut self) -> Result<PackReport> {
957 self.finish_warmup()?;
959 if let Some(b) = self.current.take() {
960 self.ready.push(b);
961 }
962 self.flush_ready()?;
963 let opts = self.opts.clone();
964
965 let blobs_len = self.blobs_written;
967 let blobs_off = self.blobs_off;
968 let blobs_hash = self.hash_written_range(blobs_off, blobs_len)?;
969 self.sections.push(SectionEntry { t: section::BLOBS, off: blobs_off, len: blobs_len, h: ByteBuf::from(blobs_hash.to_vec()) });
970
971 let mut dict_bytes = 0usize;
972 if let Some(d) = self.dict.clone() {
973 dict_bytes = d.len();
974 let bytes = match &self.key {
975 Some(k) => crypto::seal(k, crypto::LABEL_DICT, 0, b"dict", &d)?,
976 None => d,
977 };
978 self.write_section(section::DICTIONARY, &bytes)?;
979 }
980
981 let index = ChunkIndex {
982 chunks: self.hashes.iter().zip(self.sizes.iter()).zip(self.external.iter()).map(|((h, s), x)| ChunkRef { h: ByteBuf::from(h.to_vec()), s: *s, x: *x }).collect(),
983 blobs: std::mem::take(&mut self.blob_records),
984 };
985 let index_bytes = seal_section(&self.key, crypto::LABEL_INDEX, b"index", &zstd_wrap(&format::cbor_encode(&index)?)?)?;
986 self.write_section(section::CHUNK_INDEX, &index_bytes)?;
987
988 let root = chunk::merkle_root(&self.hashes);
989 let entries = std::mem::take(&mut self.entries);
990 let n_entries = entries.len();
991 let manifest = Manifest {
992 tsaur: 1,
993 profile: opts.profile.clone(),
994 fidelity: if opts.canonical { "hybrid".into() } else { "bit-exact".into() },
995 chunking: opts.chunk,
996 solid_block: opts.block_size,
997 limits: opts.limits,
998 root: ByteBuf::from(root.to_vec()),
999 entries,
1000 created: if opts.timestamp { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok().map(|d| d.as_secs()) } else { None },
1001 generator: crate::WRITER.to_string(),
1002 refs: std::mem::take(&mut self.ref_meta),
1003 };
1004 let manifest_bytes = seal_section(&self.key, crypto::LABEL_MANIFEST, b"manifest", &zstd_wrap(&format::cbor_encode(&manifest)?)?)?;
1005 self.write_section(section::MANIFEST, &manifest_bytes)?;
1006
1007 if let Some(seed) = &opts.sign_seed {
1008 let msg = format::signing_message(&self.header, &self.sections);
1009 let block = SignatureBlock { alg: "ed25519".into(), key: ByteBuf::from(crypto::sig::public_key(seed).to_vec()), sig: ByteBuf::from(crypto::sig::sign(seed, &msg).to_vec()) };
1010 let bytes = format::cbor_encode(&block)?;
1011 self.write_section(section::SIGNATURES, &bytes)?;
1012 }
1013
1014 let table = format::cbor_encode(&self.sections)?;
1015 let table_off = self.pos;
1016 self.out.write_all(&table)?;
1017 self.out.write_all(&format::encode_trailer(table_off, table.len() as u64, format::crc32(&table)))?;
1018 self.out.flush()?;
1019 let archive_bytes = table_off + table.len() as u64 + format::TRAILER_LEN as u64;
1020 let blobs = index.blobs.len();
1021
1022 Ok(PackReport {
1023 skipped_links: 0,
1024 archive_bytes,
1025 input_bytes: self.input_bytes,
1026 logical_bytes: self.logical_bytes,
1027 unique_bytes: self.unique_bytes,
1028 chunks_total: self.chunk_refs,
1029 chunks_unique: self.hashes.len(),
1030 blobs,
1031 dict_bytes,
1032 codec_hist: self.codec_hist.clone(),
1033 blocks_sampled: self.blocks_sampled,
1034 entries: n_entries,
1035 containers_exploded: self.exploded,
1036 containers_fallback: self.fallback,
1037 root: hex::encode(root),
1038 encrypted: self.key.is_some(),
1039 signed: opts.sign_seed.is_some(),
1040 referenced_chunks: self.referenced_chunks,
1041 referenced_bytes: self.referenced_bytes,
1042 views: self.views,
1043 })
1044 }
1045
1046 fn hash_written_range(&mut self, off: u64, len: u64) -> Result<Hash> {
1048 use std::io::{Read, Seek, SeekFrom};
1049 self.out.flush()?;
1050 let mut f = self.out.get_ref().try_clone()?;
1051 f.seek(SeekFrom::Start(off))?;
1052 let mut hasher = blake3::Hasher::new();
1053 let mut remaining = len;
1054 let mut buf = vec![0u8; 1 << 20];
1055 while remaining > 0 {
1056 let want = remaining.min(buf.len() as u64) as usize;
1057 let n = f.read(&mut buf[..want])?;
1058 if n == 0 {
1059 return Err(Error::Io(std::io::Error::other("short read while hashing output")));
1060 }
1061 hasher.update(&buf[..n]);
1062 remaining -= n as u64;
1063 }
1064 Ok(*hasher.finalize().as_bytes())
1065 }
1066}
1067
1068pub(crate) fn blob_aad(codec: u8, ulen: u32, first: u32, count: u32) -> [u8; 13] {
1069 let mut a = [0u8; 13];
1070 a[0] = codec;
1071 a[1..5].copy_from_slice(&ulen.to_le_bytes());
1072 a[5..9].copy_from_slice(&first.to_le_bytes());
1073 a[9..13].copy_from_slice(&count.to_le_bytes());
1074 a
1075}
1076
1077pub(crate) fn zstd_wrap(bytes: &[u8]) -> Result<Vec<u8>> {
1079 let mut c = zstd::bulk::Compressor::new(19)?;
1080 let mut out = (bytes.len() as u32).to_le_bytes().to_vec();
1081 out.extend_from_slice(&c.compress(bytes)?);
1082 Ok(out)
1083}
1084
1085pub(crate) fn zstd_unwrap(bytes: &[u8], max: usize) -> Result<Vec<u8>> {
1086 if bytes.len() < 4 {
1087 return Err(Error::Corrupt("section too short".into()));
1088 }
1089 let ulen = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1090 if ulen > max {
1091 return Err(Error::Limit(format!("section declares {ulen} bytes, limit {max}")));
1092 }
1093 let mut d = zstd::bulk::Decompressor::new()?;
1094 let out = d.decompress(&bytes[4..], ulen)?;
1095 if out.len() != ulen {
1096 return Err(Error::Corrupt("section length mismatch".into()));
1097 }
1098 Ok(out)
1099}
1100
1101fn seal_section(key: &Option<ArchiveKey>, label: u8, aad: &[u8], bytes: &[u8]) -> Result<Vec<u8>> {
1102 match key {
1103 Some(k) => crypto::seal(k, label, 0, aad, bytes),
1104 None => Ok(bytes.to_vec()),
1105 }
1106}
1107
1108pub type Inputs = Vec<(PathBuf, String)>;
1110
1111pub fn collect_inputs(inputs: &[PathBuf]) -> Result<Inputs> {
1113 Ok(collect_inputs_noting(inputs)?.0)
1114}
1115
1116pub fn collect_inputs_noting(inputs: &[PathBuf]) -> Result<(Inputs, Vec<PathBuf>)> {
1121 let mut out = Vec::new();
1122 let mut skipped = Vec::new();
1123 for input in inputs {
1124 if input.is_dir() {
1125 let mut stack = vec![input.clone()];
1126 while let Some(dir) = stack.pop() {
1127 let mut children: Vec<_> = std::fs::read_dir(&dir)?.collect::<std::io::Result<Vec<_>>>()?;
1128 children.sort_by_key(|e| e.file_name());
1129 for e in children {
1130 let p = e.path();
1131 let kind = e.file_type()?;
1132 if kind.is_symlink() {
1133 skipped.push(p);
1134 } else if kind.is_dir() {
1135 stack.push(p);
1136 } else if kind.is_file() {
1137 let rel = p.strip_prefix(input).map_err(|_| Error::Invalid("path prefix".into()))?;
1138 out.push((p.clone(), rel.to_string_lossy().replace('\\', "/")));
1139 }
1140 }
1141 }
1142 } else if input.is_file() {
1143 let name = input.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
1144 out.push((input.clone(), name));
1145 } else {
1146 return Err(Error::Missing(format!("input not found: {}", input.display())));
1147 }
1148 }
1149 out.sort_by(|a, b| a.1.cmp(&b.1));
1150 out.dedup_by(|a, b| a.1 == b.1);
1151 Ok((out, skipped))
1152}
1153
1154pub fn pack(inputs: &[PathBuf], out_path: &Path, opts: PackOptions) -> Result<PackReport> {
1156 let (files, skipped) = collect_inputs_noting(inputs)?;
1157 if files.is_empty() {
1158 return Err(Error::Missing("nothing to pack".into()));
1159 }
1160 let result = (|| {
1161 let mut p = Packer::new(opts, out_path)?;
1162 for (fs_path, arcname) in &files {
1163 p.add_file(fs_path, arcname)?;
1164 }
1165 p.finish()
1166 })();
1167 if result.is_err() {
1168 let _ = std::fs::remove_file(out_path);
1169 }
1170 result.map(|mut r| {
1171 r.skipped_links = skipped.len();
1172 r
1173 })
1174}