1use crate::chunk;
5use crate::codec;
6use crate::container;
7use crate::crypto::{self, ArchiveKey, Credentials, RecipientsBlock};
8use crate::error::{Error, Result};
9use crate::format::{self, section, Header, SectionEntry, SignatureBlock};
10use crate::manifest::{ChunkIndex, Entry, Manifest};
11use crate::pack::{blob_aad, zstd_unwrap};
12use crate::paths;
13use serde::Serialize;
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18pub const READER_MAX_TOTAL: u64 = 64 << 30;
20pub const READER_MAX_SECTION: usize = 1 << 30;
21pub const READER_MAX_ENTRIES: usize = 10_000_000;
23pub const CACHE_BYTES: usize = 64 << 20;
25
26enum Backing {
27 Mem(Vec<u8>),
28 Map(memmap2::Mmap),
29}
30
31impl std::ops::Deref for Backing {
32 type Target = [u8];
33 fn deref(&self) -> &[u8] {
34 match self {
35 Backing::Mem(v) => v,
36 Backing::Map(m) => m,
37 }
38 }
39}
40
41pub struct Reader {
42 data: Backing,
43 pub header: Header,
44 pub sections: Vec<SectionEntry>,
45 pub manifest: Manifest,
46 pub index: ChunkIndex,
47 dict: Option<Vec<u8>>,
48 key: Option<ArchiveKey>,
49 blobs_off: u64,
50 blob_of_chunk: Vec<u32>,
51 cache: HashMap<u32, Arc<Vec<u8>>>,
52 signature: Option<SignatureBlock>,
53 refs: Vec<Reader>,
55 ref_lookup: HashMap<[u8; 32], (usize, u32)>,
56}
57
58#[derive(Clone, Debug, Serialize)]
59pub struct VerifyReport {
60 pub sections_ok: bool,
61 pub entries_total: usize,
62 pub entries_ok: usize,
63 pub entries_bad: Vec<String>,
64 pub blobs: usize,
65 pub signature: Option<String>,
66 pub signature_valid: Option<bool>,
67}
68
69pub const MAX_DELTA_DEPTH: u8 = 16;
71
72fn delta_depth(bi: usize, blobs: &[crate::manifest::BlobRecord], blob_of_chunk: &[u32], memo: &mut [u8], visiting: &mut [bool]) -> Result<u8> {
75 if blobs[bi].codec != codec::ZSTD_DELTA {
76 return Ok(0);
77 }
78 if memo[bi] != 0 {
79 return Ok(memo[bi]);
80 }
81 if visiting[bi] {
82 return Err(Error::Corrupt(format!("delta blob {bi}: dictionary cycle")));
83 }
84 visiting[bi] = true;
85 let mut depth = 1u8;
86 for &ci in &blobs[bi].d {
87 let owner = blob_of_chunk[ci as usize];
88 if owner == u32::MAX {
89 continue; }
91 if owner as usize == bi {
92 return Err(Error::Corrupt(format!("delta blob {bi}: dictionary made of its own chunks")));
93 }
94 let below = delta_depth(owner as usize, blobs, blob_of_chunk, memo, visiting)?;
95 depth = depth.max(below.saturating_add(1));
96 if depth > MAX_DELTA_DEPTH {
97 return Err(Error::Limit(format!("delta blob {bi}: dictionary chain deeper than {MAX_DELTA_DEPTH}")));
98 }
99 }
100 visiting[bi] = false;
101 memo[bi] = depth;
102 Ok(depth)
103}
104
105fn framing(data: &[u8]) -> Result<(Header, Vec<SectionEntry>)> {
108 if data.len() < format::HEADER_LEN + format::TRAILER_LEN {
109 return Err(Error::Corrupt("file too short".into()));
110 }
111 let header = format::decode_header(&data[..format::HEADER_LEN])?;
112 let (toff, tlen, crc) = format::decode_trailer(&data[data.len() - format::TRAILER_LEN..])?;
113 let tend = toff.checked_add(tlen).ok_or_else(|| Error::Corrupt("table range".into()))?;
114 if toff < format::HEADER_LEN as u64 || tend != (data.len() - format::TRAILER_LEN) as u64 {
115 return Err(Error::Corrupt("section table does not end at the trailer".into()));
116 }
117 let table = &data[toff as usize..tend as usize];
118 if format::crc32(table) != crc {
119 return Err(Error::Corrupt("section table crc".into()));
120 }
121 let sections: Vec<SectionEntry> = format::cbor_decode(table)?;
122
123 let mut cursor = format::HEADER_LEN as u64;
125 for s in §ions {
126 let end = s.off.checked_add(s.len).ok_or_else(|| Error::Corrupt("section range overflow".into()))?;
127 if s.off < cursor || end > toff {
128 return Err(Error::Corrupt(format!("section {} out of bounds or overlapping", s.t)));
129 }
130 if s.len as usize > READER_MAX_SECTION && s.t != section::BLOBS {
131 return Err(Error::Limit(format!("section {} too large", s.t)));
132 }
133 let bytes = &data[s.off as usize..(s.off + s.len) as usize];
134 if chunk::hash(bytes)[..] != s.h[..] {
135 return Err(Error::HashMismatch(format!("section {}", s.t)));
136 }
137 cursor = s.off + s.len;
138 }
139 Ok((header, sections))
140}
141
142#[derive(Clone, Debug, Serialize)]
145pub struct Inspect {
146 pub version: u16,
147 pub encrypted: bool,
148 pub signed: bool,
149 pub file_bytes: u64,
150 pub sections: Vec<SectionEntry>,
151 pub stanzas: Vec<StanzaInfo>,
152 pub signature: Option<SignatureBlock>,
153}
154
155#[derive(Clone, Debug, Serialize)]
156pub struct StanzaInfo {
157 pub t: String,
158 pub m_kib: u32,
159 pub t_cost: u32,
160 pub p: u32,
161}
162
163pub fn inspect(path: &Path) -> Result<Inspect> {
164 let file = std::fs::File::open(path)?;
165 let data = Backing::Map(unsafe { memmap2::Mmap::map(&file)? });
166 let (header, sections) = framing(&data)?;
167 let find = |t: u8| sections.iter().find(|s| s.t == t).map(|s| &data[s.off as usize..(s.off + s.len) as usize]);
168 let stanzas = match find(section::RECIPIENTS) {
169 Some(b) => {
170 let block: RecipientsBlock = format::cbor_decode(b)?;
171 block.stanzas.iter().map(|s| StanzaInfo { t: s.t.clone(), m_kib: s.m_kib, t_cost: s.t_cost, p: s.p }).collect()
172 }
173 None => Vec::new(),
174 };
175 let signature = match find(section::SIGNATURES) {
176 Some(b) => Some(format::cbor_decode::<SignatureBlock>(b)?),
177 None => None,
178 };
179 Ok(Inspect {
180 version: header.version,
181 encrypted: header.flags & format::FLAG_ENCRYPTED != 0,
182 signed: header.flags & format::FLAG_SIGNED != 0,
183 file_bytes: data.len() as u64,
184 sections,
185 stanzas,
186 signature,
187 })
188}
189
190impl Reader {
191 pub fn open(path: &Path, password: Option<&str>) -> Result<Reader> {
194 Self::open_with_refs(path, password, &[])
195 }
196
197 pub fn open_with_refs(path: &Path, password: Option<&str>, references: &[PathBuf]) -> Result<Reader> {
199 let creds = Credentials { password: password.map(|s| s.to_string()), identities: Vec::new() };
200 Self::open_with(path, &creds, references)
201 }
202
203 pub fn open_with(path: &Path, creds: &Credentials, references: &[PathBuf]) -> Result<Reader> {
205 let mut r = Self::open_one(path, creds)?;
206 for rp in references {
207 let rr = Self::open_one(rp, creds).map_err(|e| Error::Missing(format!("reference archive {}: {e}", rp.display())))?;
208 let ri = r.refs.len();
209 for (ci, c) in rr.index.chunks.iter().enumerate() {
210 if c.x == 0 && c.h.len() == 32 {
211 let mut h = [0u8; 32];
212 h.copy_from_slice(&c.h);
213 r.ref_lookup.entry(h).or_insert((ri, ci as u32));
214 }
215 }
216 r.refs.push(rr);
217 }
218 Ok(r)
219 }
220
221 pub fn unresolved_external(&self) -> usize {
223 self.index
224 .chunks
225 .iter()
226 .filter(|c| c.x != 0)
227 .filter(|c| {
228 let mut h = [0u8; 32];
229 if c.h.len() != 32 {
230 return true;
231 }
232 h.copy_from_slice(&c.h);
233 !self.ref_lookup.contains_key(&h)
234 })
235 .count()
236 }
237
238 fn open_one(path: &Path, creds: &Credentials) -> Result<Reader> {
239 let file = std::fs::File::open(path)?;
240 let map = unsafe { memmap2::Mmap::map(&file)? };
244 Self::from_backing(Backing::Map(map), creds)
245 }
246
247 pub fn from_bytes(data: Vec<u8>, password: Option<&str>) -> Result<Reader> {
248 let creds = Credentials { password: password.map(|s| s.to_string()), identities: Vec::new() };
249 Self::from_backing(Backing::Mem(data), &creds)
250 }
251
252 fn from_backing(data: Backing, creds: &Credentials) -> Result<Reader> {
253 if data.len() < format::HEADER_LEN + format::TRAILER_LEN {
254 return Err(Error::Corrupt("file too short".into()));
255 }
256 let (header, sections) = framing(&data)?;
257 let find = |t: u8| sections.iter().find(|s| s.t == t).map(|s| &data[s.off as usize..(s.off + s.len) as usize]);
258
259 let key = if header.flags & format::FLAG_ENCRYPTED != 0 {
260 if creds.password.is_none() && creds.identities.is_empty() {
261 return Err(Error::Crypto("archive is encrypted: password or identity required".into()));
262 }
263 let block: RecipientsBlock = format::cbor_decode(find(section::RECIPIENTS).ok_or_else(|| Error::Corrupt("missing recipients section".into()))?)?;
264 Some(crypto::unlock(&block, creds)?)
265 } else {
266 None
267 };
268
269 let open_section = |label: u8, aad: &[u8], bytes: &[u8]| -> Result<Vec<u8>> {
270 match &key {
271 Some(k) => crypto::open(k, label, 0, aad, bytes),
272 None => Ok(bytes.to_vec()),
273 }
274 };
275
276 let index_raw = open_section(crypto::LABEL_INDEX, b"index", find(section::CHUNK_INDEX).ok_or_else(|| Error::Corrupt("missing chunk index".into()))?)?;
277 let index: ChunkIndex = format::cbor_decode(&zstd_unwrap(&index_raw, READER_MAX_SECTION)?)?;
278 let manifest_raw = open_section(crypto::LABEL_MANIFEST, b"manifest", find(section::MANIFEST).ok_or_else(|| Error::Corrupt("missing manifest".into()))?)?;
279 let manifest: Manifest = format::cbor_decode(&zstd_unwrap(&manifest_raw, READER_MAX_SECTION)?)?;
280 if manifest.entries.len() > READER_MAX_ENTRIES {
281 return Err(Error::Limit(format!("{} entries exceed the reader limit of {READER_MAX_ENTRIES}", manifest.entries.len())));
282 }
283 if manifest.tsaur != 1 {
284 return Err(Error::Version(manifest.tsaur));
285 }
286 let dict = match find(section::DICTIONARY) {
287 Some(b) => Some(open_section(crypto::LABEL_DICT, b"dict", b)?),
288 None => None,
289 };
290 let blobs_off = sections.iter().find(|s| s.t == section::BLOBS).map(|s| s.off).ok_or_else(|| Error::Corrupt("missing blobs section".into()))?;
291 let signature = match find(section::SIGNATURES) {
292 Some(b) => Some(format::cbor_decode::<SignatureBlock>(b)?),
293 None => None,
294 };
295
296 let total: u64 = index.blobs.iter().map(|b| b.ulen as u64).sum();
298 let max_total = manifest.limits.max_total.min(READER_MAX_TOTAL);
299 if total > max_total {
300 return Err(Error::Limit(format!("declared {total} bytes exceed limit {max_total}")));
301 }
302 let ratio = total / (data.len() as u64).max(1);
303 if ratio > manifest.limits.max_ratio as u64 {
304 return Err(Error::Limit(format!("expansion ratio {ratio}x exceeds limit")));
305 }
306 let mut blob_of_chunk = vec![u32::MAX; index.chunks.len()];
307 let blobs_len = sections.iter().find(|s| s.t == section::BLOBS).map(|s| s.len).unwrap_or(0);
308 for (bi, b) in index.blobs.iter().enumerate() {
309 if b.off.checked_add(b.clen as u64).is_none_or(|e| e > blobs_len) {
310 return Err(Error::Corrupt(format!("blob {bi} out of bounds")));
311 }
312 if b.n.is_empty() || b.n.iter().any(|&ci| ci as usize >= index.chunks.len()) {
313 return Err(Error::Corrupt(format!("blob {bi} references chunks out of range")));
314 }
315 let sum: u64 = b.n.iter().map(|&ci| index.chunks[ci as usize].s as u64).sum();
316 if sum != b.ulen as u64 {
317 return Err(Error::Corrupt(format!("blob {bi} size does not match its chunks")));
318 }
319 for &ci in &b.n {
320 if blob_of_chunk[ci as usize] != u32::MAX {
321 return Err(Error::Corrupt(format!("chunk {ci} referenced by two blobs")));
322 }
323 blob_of_chunk[ci as usize] = bi as u32;
324 }
325 }
326 for (ci, c) in index.chunks.iter().enumerate() {
327 if c.h.len() != 32 {
328 return Err(Error::Corrupt(format!("chunk {ci}: bad hash length")));
329 }
330 if c.x == 0 && blob_of_chunk[ci] == u32::MAX {
331 return Err(Error::Corrupt(format!("chunk {ci} without blob")));
332 }
333 if c.x != 0 && blob_of_chunk[ci] != u32::MAX {
334 return Err(Error::Corrupt(format!("chunk {ci} marked external but stored")));
335 }
336 }
337 for (bi, b) in index.blobs.iter().enumerate() {
341 if b.codec != codec::ZSTD_DELTA {
342 continue;
343 }
344 if b.d.is_empty() || b.d.iter().any(|&ci| ci as usize >= index.chunks.len()) {
345 return Err(Error::Corrupt(format!("delta blob {bi}: dictionary chunks out of range")));
346 }
347 let dict_bytes: u64 = b.d.iter().map(|&ci| index.chunks[ci as usize].s as u64).sum();
348 if dict_bytes > 64 << 20 {
349 return Err(Error::Limit(format!("delta blob {bi}: dictionary larger than 64 MiB")));
350 }
351 }
352 let mut memo = vec![0u8; index.blobs.len()];
353 let mut visiting = vec![false; index.blobs.len()];
354 for bi in 0..index.blobs.len() {
355 if index.blobs[bi].codec == codec::ZSTD_DELTA {
356 delta_depth(bi, &index.blobs, &blob_of_chunk, &mut memo, &mut visiting)?;
357 }
358 }
359 for e in &manifest.entries {
360 paths::normalize(&e.path)?;
361 let refs: Box<dyn Iterator<Item = &u32>> = match (&e.zip, &e.streams) {
362 (Some(z), _) => Box::new(z.members.iter().flat_map(|m| m.chunks.iter())),
363 (None, Some(s)) => Box::new(s.segments.iter().flat_map(|m| m.chunks.iter())),
364 (None, None) => Box::new(e.chunks.iter()),
365 };
366 for &ci in refs {
367 if ci as usize >= index.chunks.len() {
368 return Err(Error::Corrupt(format!("{}: chunk reference out of range", e.path)));
369 }
370 }
371 }
372
373 Ok(Reader { data, header, sections, manifest, index, dict, key, blobs_off, blob_of_chunk, cache: HashMap::new(), signature, refs: Vec::new(), ref_lookup: HashMap::new() })
374 }
375
376 pub fn is_encrypted(&self) -> bool {
377 self.key.is_some()
378 }
379
380 pub fn signature(&self) -> Option<&SignatureBlock> {
381 self.signature.as_ref()
382 }
383
384 pub fn verify_signature(&self, pubkey: &[u8; 32]) -> Result<bool> {
386 let s = self.signature.as_ref().ok_or_else(|| Error::Missing("archive is not signed".into()))?;
387 if s.alg != "ed25519" {
388 return Err(Error::Crypto(format!("unsupported signature algorithm {}", s.alg)));
389 }
390 if s.key[..] != pubkey[..] {
391 return Ok(false);
392 }
393 let msg = format::signing_message(&self.data[..format::HEADER_LEN], &self.sections);
394 Ok(crypto::sig::verify(pubkey, &msg, &s.sig))
395 }
396
397 fn blob_plain(&mut self, bi: u32) -> Result<Arc<Vec<u8>>> {
398 if let Some(p) = self.cache.get(&bi) {
399 return Ok(p.clone());
400 }
401 let b = self.index.blobs[bi as usize].clone();
402 let start = (self.blobs_off + b.off) as usize;
403 let stored = &self.data[start..start + b.clen as usize];
404 let aad = blob_aad(b.codec, b.ulen, b.n[0], b.n.len() as u32);
405 let compressed = match &self.key {
406 Some(k) => crypto::open(k, crypto::LABEL_BLOB, bi as u64, &aad, stored)?,
407 None => stored.to_vec(),
408 };
409 let plain = if b.codec == codec::ZSTD_DELTA {
410 let mut prefix = Vec::with_capacity(b.d.iter().map(|&ci| self.index.chunks[ci as usize].s as usize).sum());
412 for &ci in &b.d {
413 prefix.extend_from_slice(&self.chunk(ci)?);
414 }
415 codec::zstd_decompress_with_prefix(&compressed, b.ulen as usize, &prefix)?
416 } else {
417 codec::decompress_with(b.codec, &compressed, b.ulen as usize, self.dict.as_deref(), &b.p, b.f)?
418 };
419 let mut off = 0usize;
421 for &ci in &b.n {
422 let c = &self.index.chunks[ci as usize];
423 let piece = &plain[off..off + c.s as usize];
424 if chunk::hash(piece)[..] != c.h[..] {
425 return Err(Error::HashMismatch(format!("chunk {ci}")));
426 }
427 off += c.s as usize;
428 }
429 let arc = Arc::new(plain);
430 let cached: usize = self.cache.values().map(|v| v.len()).sum();
432 if cached + arc.len() > CACHE_BYTES {
433 self.cache.clear();
434 }
435 self.cache.insert(bi, arc.clone());
436 Ok(arc)
437 }
438
439 pub fn stream_entry(&mut self, ei: usize, sink: &mut dyn std::io::Write) -> Result<u64> {
442 let e = self.manifest.entries.get(ei).cloned().ok_or_else(|| Error::Missing(format!("entry {ei}")))?;
443 if e.mode != "raw" {
444 let bytes = self.entry_bytes(ei)?;
445 sink.write_all(&bytes)?;
446 return Ok(bytes.len() as u64);
447 }
448 let mut hasher = blake3::Hasher::new();
449 let mut total = 0u64;
450 for &ci in &e.chunks {
451 let c = self.chunk(ci)?;
452 hasher.update(&c);
453 total += c.len() as u64;
454 sink.write_all(&c)?;
455 }
456 if total != e.size || hasher.finalize().as_bytes()[..] != e.h[..] {
457 return Err(Error::HashMismatch(e.path.clone()));
458 }
459 Ok(total)
460 }
461
462 pub fn chunk(&mut self, ci: u32) -> Result<Vec<u8>> {
464 let c = &self.index.chunks[ci as usize];
465 if c.x != 0 {
466 let mut h = [0u8; 32];
467 h.copy_from_slice(&c.h);
468 let (ri, cj) = *self.ref_lookup.get(&h).ok_or_else(|| Error::Missing(format!("chunk {ci} is external (b3:{}…) and no reference archive provides it", hex::encode(&h[..6]))))?;
469 let data = self.refs[ri].chunk(cj)?;
470 if chunk::hash(&data) != h {
471 return Err(Error::HashMismatch(format!("external chunk {ci}")));
472 }
473 return Ok(data);
474 }
475 let bi = self.blob_of_chunk[ci as usize];
476 let b = &self.index.blobs[bi as usize];
477 let mut off = 0usize;
478 for &k in &b.n {
479 if k == ci {
480 break;
481 }
482 off += self.index.chunks[k as usize].s as usize;
483 }
484 let len = self.index.chunks[ci as usize].s as usize;
485 let plain = self.blob_plain(bi)?;
486 Ok(plain[off..off + len].to_vec())
487 }
488
489 fn stream(&mut self, refs: &[u32]) -> Result<Vec<u8>> {
490 let mut out = Vec::new();
491 for &ci in refs {
492 out.extend_from_slice(&self.chunk(ci)?);
493 }
494 Ok(out)
495 }
496
497 pub fn entry_bytes(&mut self, ei: usize) -> Result<Vec<u8>> {
499 let e = self.manifest.entries.get(ei).cloned().ok_or_else(|| Error::Missing(format!("entry {ei}")))?;
500 let data = match e.mode.as_str() {
501 "raw" => self.stream(&e.chunks)?,
502 "zip" => {
503 let z = e.zip.as_ref().ok_or_else(|| Error::Corrupt("zip entry without recipe".into()))?;
504 let mut parts = Vec::with_capacity(z.members.len());
505 for m in &z.members {
506 parts.push(self.stream(&m.chunks)?);
507 }
508 container::rebuild(z, &parts)?
509 }
510 "streams" => {
511 let s = e.streams.as_ref().ok_or_else(|| Error::Corrupt("streams entry without recipe".into()))?;
512 let mut parts = Vec::with_capacity(s.segments.len());
513 for seg in &s.segments {
514 parts.push(self.stream(&seg.chunks)?);
515 }
516 container::rebuild_streams(s, &parts)?
517 }
518 other => return Err(Error::Corrupt(format!("unknown entry mode {other}"))),
519 };
520 if data.len() as u64 != e.size || chunk::hash(&data)[..] != e.h[..] {
521 return Err(Error::HashMismatch(e.path.clone()));
522 }
523 Ok(data)
524 }
525
526 pub fn read_range(&mut self, ei: usize, start: u64, len: usize) -> Result<Vec<u8>> {
528 let e = self.manifest.entries.get(ei).cloned().ok_or_else(|| Error::Missing(format!("entry {ei}")))?;
529 if e.mode != "raw" {
530 let all = self.entry_bytes(ei)?;
531 let s = (start as usize).min(all.len());
532 let t = (s + len).min(all.len());
533 return Ok(all[s..t].to_vec());
534 }
535 let end = start.saturating_add(len as u64).min(e.size);
536 let mut out = Vec::new();
537 let mut pos = 0u64;
538 for &ci in &e.chunks {
539 let sz = self.index.chunks[ci as usize].s as u64;
540 let (cs, ce) = (pos, pos + sz);
541 if ce > start && cs < end {
542 let c = self.chunk(ci)?;
543 let from = start.saturating_sub(cs) as usize;
544 let to = (end - cs).min(sz) as usize;
545 out.extend_from_slice(&c[from..to]);
546 }
547 pos = ce;
548 if pos >= end {
549 break;
550 }
551 }
552 Ok(out)
553 }
554
555 pub fn find_entry(&self, path: &str) -> Option<usize> {
556 let norm = paths::normalize(path).ok()?;
557 self.manifest.entries.iter().position(|e| e.path == norm)
558 }
559
560 pub fn entries(&self) -> &[Entry] {
561 &self.manifest.entries
562 }
563
564 pub fn extract(&mut self, dir: &Path, selected: Option<&[usize]>, overwrite: bool) -> Result<Vec<PathBuf>> {
570 let idxs: Vec<usize> = match selected {
571 Some(s) => s.to_vec(),
572 None => (0..self.manifest.entries.len()).filter(|&i| self.manifest.entries[i].derived.is_none()).collect(),
574 };
575 for &i in &idxs {
576 let e = self.manifest.entries.get(i).ok_or_else(|| Error::Missing(format!("entry {i}")))?;
577 let dest = paths::safe_join(dir, &e.path)?;
578 match std::fs::symlink_metadata(&dest) {
579 Ok(m) if m.is_dir() => return Err(Error::Policy(format!("{} exists and is a directory", dest.display()))),
580 Ok(_) if !overwrite => return Err(Error::Policy(format!("{} exists; pass --overwrite to replace existing files", dest.display()))),
581 _ => {}
582 }
583 }
584 if cfg!(any(windows, target_os = "macos")) {
587 let mut seen = std::collections::HashSet::new();
588 for &i in &idxs {
589 let p = &self.manifest.entries.get(i).ok_or_else(|| Error::Missing(format!("entry {i}")))?.path;
590 if !seen.insert(p.to_lowercase()) {
591 return Err(Error::Policy(format!("entries differ only by letter case ({p}): refusing to extract on a case-insensitive filesystem; select entries explicitly")));
592 }
593 }
594 }
595 let mut written = Vec::new();
596 for ei in idxs {
597 let e = self.manifest.entries.get(ei).cloned().ok_or_else(|| Error::Missing(format!("entry {ei}")))?;
598 let dest = paths::safe_join(dir, &e.path)?;
599 if let Some(parent) = dest.parent() {
600 std::fs::create_dir_all(parent)?;
601 }
602 let tmp = dest.with_extension(format!("{}.tsr-partial", dest.extension().map(|x| x.to_string_lossy().to_string()).unwrap_or_default()));
603 {
604 let mut f = std::io::BufWriter::with_capacity(1 << 20, std::fs::File::create(&tmp)?);
605 let res = self.stream_entry(ei, &mut f).and_then(|_| std::io::Write::flush(&mut f).map_err(Error::from));
606 drop(f);
607 if let Err(err) = res {
608 let _ = std::fs::remove_file(&tmp);
609 return Err(err);
610 }
611 }
612 if dest.exists() {
613 std::fs::remove_file(&dest)?;
614 }
615 std::fs::rename(&tmp, &dest)?;
616 written.push(dest);
617 }
618 Ok(written)
619 }
620
621 pub fn verify(&mut self, pubkey: Option<&[u8; 32]>) -> Result<VerifyReport> {
623 let mut bad = Vec::new();
624 let n = self.manifest.entries.len();
625 for ei in 0..n {
626 if let Err(e) = self.stream_entry(ei, &mut std::io::sink()) {
627 bad.push(format!("{}: {}", self.manifest.entries[ei].path, e));
628 }
629 }
630 let signature = self.signature.as_ref().map(|s| hex::encode(&s.key));
631 let signature_valid = match (pubkey, &self.signature) {
632 (Some(pk), Some(_)) => Some(self.verify_signature(pk)?),
633 _ => None,
634 };
635 Ok(VerifyReport { sections_ok: true, entries_total: n, entries_ok: n - bad.len(), entries_bad: bad, blobs: self.index.blobs.len(), signature, signature_valid })
636 }
637}