1use crate::detect::{sniff, Format};
13use crate::error::{ArchiveError, Result};
14use crate::peel::MAX_INFLATED;
15use crate::plan::Access;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ArchiveEntry {
20 pub name: String,
22 pub size: u64,
24 pub is_dir: bool,
26}
27
28pub struct Archive {
30 format: Format,
31 entries: Vec<ArchiveEntry>,
32 backend: Backend,
33}
34
35#[allow(clippy::large_enum_variant)]
40enum Backend {
41 Tar { compressed: Vec<u8>, outer: Format },
45 Zip {
47 archive: zip_core::ZipArchive<std::io::Cursor<Vec<u8>>>,
48 },
49 SevenZip {
51 reader: sevenz_rust2::ArchiveReader<std::io::Cursor<Vec<u8>>>,
52 },
53}
54
55impl Archive {
56 pub fn open(data: &[u8], name: Option<&str>) -> Result<Option<Archive>> {
68 Self::open_with_format(sniff(name, data), data)
69 }
70
71 pub(crate) fn open_with_format(format: Format, data: &[u8]) -> Result<Option<Archive>> {
81 match format {
82 Format::Tar | Format::TarGz | Format::TarBz2 => {
83 Self::open_tar(format, data.to_vec()).map(Some)
84 }
85 Format::Zip => Self::open_zip(data).map(Some),
86 Format::SevenZip => Self::open_7z(data).map(Some),
87 _ => Ok(None),
88 }
89 }
90
91 pub fn member_access(&mut self, index: usize) -> Result<Access> {
101 let count = self.entries.len();
102 if index >= count {
103 return Err(ArchiveError::IndexOutOfRange { index, count });
104 }
105 match &mut self.backend {
106 Backend::Zip { archive } => {
107 let f = archive.by_index(index).map_err(|e| ArchiveError::Read {
108 format: "zip",
109 detail: e.to_string(),
112 })?;
113 Ok(match f.compression() {
114 zip_core::CompressionMethod::Stored => Access::InPlace {
115 offset: f.data_start(),
116 len: f.compressed_size(),
117 },
118 zip_core::CompressionMethod::Deflated
119 | zip_core::CompressionMethod::Deflate64 => Access::Zran,
120 _ => Access::SpillToTemp,
121 })
122 }
123 Backend::Tar { .. } | Backend::SevenZip { .. } => Ok(Access::SpillToTemp),
126 }
127 }
128
129 #[must_use]
131 pub fn format(&self) -> Format {
132 self.format
133 }
134
135 #[must_use]
137 pub fn entries(&self) -> &[ArchiveEntry] {
138 &self.entries
139 }
140
141 pub fn read(&mut self, index: usize) -> Result<Vec<u8>> {
148 let count = self.entries.len();
149 if index >= count {
150 return Err(ArchiveError::IndexOutOfRange { index, count });
151 }
152 let (name, declared_size) = {
155 let e = &self.entries[index];
156 (e.name.clone(), e.size)
157 };
158 match &mut self.backend {
159 Backend::Tar { compressed, outer } => {
160 extract_tar_member_streaming(compressed, *outer, index, MAX_INFLATED)
161 }
162 Backend::Zip { archive } => read_zip_member(archive, index),
163 Backend::SevenZip { reader } => read_7z_member(reader, &name, declared_size),
164 }
165 }
166
167 pub fn stream_member(
180 &mut self,
181 index: usize,
182 out: &mut dyn std::io::Write,
183 cap: u64,
184 ) -> Result<u64> {
185 let count = self.entries.len();
186 if index >= count {
187 return Err(ArchiveError::IndexOutOfRange { index, count });
188 }
189 let (name, declared_size) = {
190 let e = &self.entries[index];
191 (e.name.clone(), e.size)
192 };
193 match &mut self.backend {
194 Backend::Tar { compressed, outer } => {
195 stream_tar_member(compressed, *outer, index, out, cap)
196 }
197 Backend::Zip { archive } => stream_zip_member(archive, index, out, cap),
198 Backend::SevenZip { reader } => {
199 let bytes = read_7z_member(reader, &name, declared_size)?;
200 if bytes.len() as u64 > cap {
201 return Err(ArchiveError::TooLarge { cap });
202 }
203 out.write_all(&bytes).map_err(|e| ArchiveError::Read {
204 format: "7z",
205 detail: e.to_string(),
206 })?;
207 Ok(bytes.len() as u64)
208 }
209 }
210 }
211
212 fn open_tar(outer: Format, compressed: Vec<u8>) -> Result<Archive> {
217 let entries = {
218 let mut ar = tar::Archive::new(tar_stream(&compressed, outer));
219 let iter = ar.entries().map_err(|e| ArchiveError::Open {
220 format: "tar",
221 detail: e.to_string(),
222 })?;
223 let mut entries = Vec::new();
224 for entry in iter {
225 let entry = entry.map_err(|e| ArchiveError::Open {
226 format: "tar",
227 detail: e.to_string(),
228 })?;
229 let name = entry.path().map_or_else(
230 |_| String::from_utf8_lossy(&entry.path_bytes()).into_owned(),
231 |p| p.to_string_lossy().into_owned(),
232 );
233 let header = entry.header();
234 let size = header.size().map_err(|e| ArchiveError::Open {
235 format: "tar",
236 detail: e.to_string(),
237 })?;
238 entries.push(ArchiveEntry {
239 name,
240 size,
241 is_dir: header.entry_type().is_dir(),
242 });
243 }
244 entries
245 };
246 Ok(Archive {
247 format: outer,
248 entries,
249 backend: Backend::Tar { compressed, outer },
250 })
251 }
252
253 fn open_zip(data: &[u8]) -> Result<Archive> {
255 let mut archive =
256 zip_core::ZipArchive::new(std::io::Cursor::new(data.to_vec())).map_err(|e| {
257 ArchiveError::Open {
258 format: "zip",
259 detail: e.to_string(),
260 }
261 })?;
262 let count = archive.len();
263 let mut entries = Vec::with_capacity(count);
264 for i in 0..count {
265 let f = archive.by_index(i).map_err(|e| ArchiveError::Open {
266 format: "zip",
267 detail: e.to_string(),
268 })?;
269 entries.push(ArchiveEntry {
270 name: f.name().to_string(),
271 size: f.size(),
272 is_dir: f.is_dir(),
273 });
274 }
275 Ok(Archive {
276 format: Format::Zip,
277 entries,
278 backend: Backend::Zip { archive },
279 })
280 }
281
282 fn open_7z(data: &[u8]) -> Result<Archive> {
284 let reader = sevenz_rust2::ArchiveReader::new(
285 std::io::Cursor::new(data.to_vec()),
286 sevenz_rust2::Password::empty(),
287 )
288 .map_err(|e| ArchiveError::Open {
289 format: "7z",
290 detail: e.to_string(),
291 })?;
292 let entries = reader
293 .archive()
294 .files
295 .iter()
296 .map(|f| ArchiveEntry {
297 name: f.name.clone(),
298 size: f.size,
299 is_dir: f.is_directory,
300 })
301 .collect();
302 Ok(Archive {
303 format: Format::SevenZip,
304 entries,
305 backend: Backend::SevenZip { reader },
306 })
307 }
308}
309
310fn tar_stream(compressed: &[u8], outer: Format) -> Box<dyn std::io::Read + '_> {
314 let cursor = std::io::Cursor::new(compressed);
315 match outer {
316 Format::TarGz => Box::new(flate2::read::GzDecoder::new(cursor)),
317 Format::TarBz2 => Box::new(bzip2_rs::DecoderReader::new(cursor)),
318 _ => Box::new(cursor),
321 }
322}
323
324fn extract_tar_member_streaming(
330 compressed: &[u8],
331 outer: Format,
332 index: usize,
333 cap: u64,
334) -> Result<Vec<u8>> {
335 use std::io::Read;
336 let mut ar = tar::Archive::new(tar_stream(compressed, outer));
337 let mut iter = ar.entries().map_err(|e| ArchiveError::Read {
338 format: "tar",
339 detail: e.to_string(),
340 })?;
341 let entry = iter
342 .nth(index)
343 .ok_or(ArchiveError::IndexOutOfRange {
344 index,
345 count: index,
346 })?
347 .map_err(|e| ArchiveError::Read {
348 format: "tar",
349 detail: e.to_string(),
350 })?;
351 let mut out = Vec::new();
352 let mut limited = entry.take(cap + 1);
353 limited
354 .read_to_end(&mut out)
355 .map_err(|e| ArchiveError::Read {
356 format: "tar",
357 detail: e.to_string(),
358 })?;
359 if out.len() as u64 > cap {
360 return Err(ArchiveError::TooLarge { cap });
361 }
362 Ok(out)
363}
364
365fn read_zip_member(
368 archive: &mut zip_core::ZipArchive<std::io::Cursor<Vec<u8>>>,
369 index: usize,
370) -> Result<Vec<u8>> {
371 use std::io::Read;
372 let zf = archive.by_index(index).map_err(|e| ArchiveError::Read {
373 format: "zip",
374 detail: e.to_string(),
375 })?;
376 let mut out = Vec::new();
377 let mut limited = zf.take(MAX_INFLATED + 1);
378 limited
379 .read_to_end(&mut out)
380 .map_err(|e| ArchiveError::Read {
381 format: "zip",
382 detail: e.to_string(),
383 })?;
384 if out.len() as u64 > MAX_INFLATED {
385 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
386 }
387 Ok(out)
388}
389
390fn read_7z_member(
395 reader: &mut sevenz_rust2::ArchiveReader<std::io::Cursor<Vec<u8>>>,
396 name: &str,
397 declared_size: u64,
398) -> Result<Vec<u8>> {
399 if declared_size > MAX_INFLATED {
400 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
401 }
402 let out = reader.read_file(name).map_err(|e| ArchiveError::Read {
403 format: "7z",
404 detail: e.to_string(),
405 })?;
406 if out.len() as u64 > MAX_INFLATED {
407 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
408 }
409 Ok(out)
410}
411
412fn copy_capped(
416 reader: impl std::io::Read,
417 out: &mut dyn std::io::Write,
418 cap: u64,
419 format: &'static str,
420) -> Result<u64> {
421 let mut limited = reader.take(cap + 1);
422 let n = std::io::copy(&mut limited, out).map_err(|e| ArchiveError::Read {
423 format,
424 detail: e.to_string(),
425 })?;
426 if n > cap {
427 return Err(ArchiveError::TooLarge { cap });
428 }
429 Ok(n)
430}
431
432fn stream_tar_member(
436 compressed: &[u8],
437 outer: Format,
438 index: usize,
439 out: &mut dyn std::io::Write,
440 cap: u64,
441) -> Result<u64> {
442 let mut ar = tar::Archive::new(tar_stream(compressed, outer));
443 let mut iter = ar.entries().map_err(|e| ArchiveError::Read {
444 format: "tar",
445 detail: e.to_string(),
446 })?;
447 let entry = iter
448 .nth(index)
449 .ok_or(ArchiveError::IndexOutOfRange {
450 index,
451 count: index,
452 })?
453 .map_err(|e| ArchiveError::Read {
454 format: "tar",
455 detail: e.to_string(),
456 })?;
457 copy_capped(entry, out, cap, "tar")
458}
459
460fn stream_zip_member(
463 archive: &mut zip_core::ZipArchive<std::io::Cursor<Vec<u8>>>,
464 index: usize,
465 out: &mut dyn std::io::Write,
466 cap: u64,
467) -> Result<u64> {
468 let zf = archive.by_index(index).map_err(|e| ArchiveError::Read {
469 format: "zip",
470 detail: e.to_string(),
471 })?;
472 copy_capped(zf, out, cap, "zip")
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use std::io::Write;
479
480 fn build_tar(members: &[(&str, Vec<u8>)]) -> Vec<u8> {
482 let mut b = tar::Builder::new(Vec::new());
483 for (name, data) in members {
484 let mut h = tar::Header::new_gnu();
485 h.set_size(data.len() as u64);
486 h.set_mode(0o644);
487 h.set_cksum();
488 b.append_data(&mut h, name, data.as_slice()).unwrap();
489 }
490 b.into_inner().unwrap()
491 }
492
493 fn gzip(data: &[u8]) -> Vec<u8> {
494 let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
495 e.write_all(data).unwrap();
496 e.finish().unwrap()
497 }
498
499 #[test]
506 fn streaming_targz_extracts_each_member_under_whole_tar_cap() {
507 let a = vec![0xAA_u8; 1000];
508 let b = vec![0xBB_u8; 1000];
509 let tar = build_tar(&[("a.bin", a.clone()), ("b.bin", b.clone())]);
510 assert!(
511 tar.len() as u64 > 1200,
512 "combined tar ({}) must exceed the per-member cap",
513 tar.len()
514 );
515 let targz = gzip(&tar);
516 assert_eq!(
517 extract_tar_member_streaming(&targz, Format::TarGz, 0, 1200).unwrap(),
518 a
519 );
520 assert_eq!(
521 extract_tar_member_streaming(&targz, Format::TarGz, 1, 1200).unwrap(),
522 b
523 );
524 }
525
526 #[test]
527 fn streaming_plain_tar_extracts_member() {
528 let a = vec![0x11_u8; 800];
529 let b = vec![0x22_u8; 800];
530 let tar = build_tar(&[("a.bin", a.clone()), ("b.bin", b.clone())]);
531 assert_eq!(
532 extract_tar_member_streaming(&tar, Format::Tar, 0, MAX_INFLATED).unwrap(),
533 a
534 );
535 assert_eq!(
536 extract_tar_member_streaming(&tar, Format::Tar, 1, MAX_INFLATED).unwrap(),
537 b
538 );
539 }
540
541 #[test]
542 fn streaming_member_over_cap_fails_loud() {
543 let targz = gzip(&build_tar(&[("a.bin", vec![0xAA_u8; 1000])]));
544 match extract_tar_member_streaming(&targz, Format::TarGz, 0, 500) {
545 Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 500),
546 other => panic!("expected TooLarge, got {other:?}"),
547 }
548 }
549
550 #[test]
551 fn streaming_bad_index_fails_loud() {
552 let tar = build_tar(&[("only.bin", vec![0x33_u8; 10])]);
553 assert!(matches!(
554 extract_tar_member_streaming(&tar, Format::Tar, 99, MAX_INFLATED),
555 Err(ArchiveError::IndexOutOfRange { .. })
556 ));
557 }
558
559 const PAYLOAD_TBZ2: &[u8] = include_bytes!("../../tests/data/fixtures/payload.tbz2");
563
564 #[test]
565 fn member_access_out_of_range_fails_loud() {
566 let mut a = Archive::open(PAYLOAD_TBZ2, Some("payload.tbz2"))
567 .unwrap()
568 .unwrap();
569 assert!(matches!(
570 a.member_access(9999),
571 Err(ArchiveError::IndexOutOfRange { .. })
572 ));
573 }
574
575 #[test]
576 fn streaming_tbz2_reads_member_via_same_path() {
577 let mut a = Archive::open(PAYLOAD_TBZ2, Some("payload.tbz2"))
578 .unwrap()
579 .unwrap();
580 assert_eq!(a.format(), Format::TarBz2);
581 let ia = a
582 .entries()
583 .iter()
584 .position(|e| e.name == "a.txt" && !e.is_dir)
585 .unwrap();
586 assert_eq!(a.read(ia).unwrap(), b"alpha member contents\n");
587 }
588}