use std::cell::Cell;
use std::fs;
use std::path::{Path, PathBuf};
use std::vec::Vec;
use tinyzip::{Archive, Compression, Error, Reader, SliceReaderError};
const VALID_ROOT: &str = "tests/data/valid";
const INVALID_ROOT: &str = "tests/data/invalid";
struct CountingReader<'a> {
bytes: &'a [u8],
bytes_read: &'a Cell<u64>,
}
impl<'a> CountingReader<'a> {
fn new(bytes: &'a [u8], bytes_read: &'a Cell<u64>) -> Self {
Self { bytes, bytes_read }
}
}
impl Reader for CountingReader<'_> {
type Error = ();
fn size(&self) -> Result<u64, Self::Error> {
Ok(self.bytes.len() as u64)
}
fn read_exact_at(&self, pos: u64, buf: &mut [u8]) -> Result<(), Self::Error> {
let pos = usize::try_from(pos).map_err(|_| ())?;
let end = pos.checked_add(buf.len()).ok_or(())?;
let src = self.bytes.get(pos..end).ok_or(())?;
buf.copy_from_slice(src);
self.bytes_read
.set(self.bytes_read.get() + u64::try_from(buf.len()).map_err(|_| ())?);
Ok(())
}
}
struct Fixture {
name: &'static str,
path: &'static str,
expected: Expected,
}
enum Expected {
Reject(Error<()>),
Accept {
entry_count: u64,
entries: &'static [ExpectedEntry],
},
}
struct ExpectedEntry {
name: &'static [u8],
compression: Compression,
compressed_size: u64,
uncompressed_size: u64,
}
const TEST_ENTRIES: &[ExpectedEntry] = &[
ExpectedEntry {
name: b"test.txt",
compression: Compression::Deflated,
compressed_size: 25,
uncompressed_size: 26,
},
ExpectedEntry {
name: b"gophercolor16x16.png",
compression: Compression::Stored,
compressed_size: 785,
uncompressed_size: 785,
},
];
const DD_ENTRIES: &[ExpectedEntry] = &[ExpectedEntry {
name: b"filename",
compression: Compression::Deflated,
compressed_size: 24,
uncompressed_size: 25,
}];
const GO_STORED_ENTRIES: &[ExpectedEntry] = &[
ExpectedEntry {
name: b"foo.txt",
compression: Compression::Stored,
compressed_size: 4,
uncompressed_size: 4,
},
ExpectedEntry {
name: b"bar.txt",
compression: Compression::Stored,
compressed_size: 4,
uncompressed_size: 4,
},
];
const ZIP64_ENTRIES: &[ExpectedEntry] = &[ExpectedEntry {
name: b"README",
compression: Compression::Deflated,
compressed_size: 36,
uncompressed_size: 36,
}];
const MANUAL_FIXTURES: &[Fixture] = &[
Fixture {
name: "test.zip",
path: "tests/data/manual/go-archive-zip/test.zip",
expected: Expected::Accept {
entry_count: 2,
entries: TEST_ENTRIES,
},
},
Fixture {
name: "test-trailing-junk.zip",
path: "tests/data/manual/go-archive-zip/test-trailing-junk.zip",
expected: Expected::Accept {
entry_count: 2,
entries: TEST_ENTRIES,
},
},
Fixture {
name: "test-prefix.zip",
path: "tests/data/manual/go-archive-zip/test-prefix.zip",
expected: Expected::Accept {
entry_count: 2,
entries: TEST_ENTRIES,
},
},
Fixture {
name: "dd.zip",
path: "tests/data/manual/go-archive-zip/dd.zip",
expected: Expected::Accept {
entry_count: 1,
entries: DD_ENTRIES,
},
},
Fixture {
name: "go-with-datadesc-sig.zip",
path: "tests/data/manual/go-archive-zip/go-with-datadesc-sig.zip",
expected: Expected::Accept {
entry_count: 2,
entries: GO_STORED_ENTRIES,
},
},
Fixture {
name: "zip64.zip",
path: "tests/data/manual/go-archive-zip/zip64.zip",
expected: Expected::Accept {
entry_count: 1,
entries: ZIP64_ENTRIES,
},
},
Fixture {
name: "zip64-2.zip",
path: "tests/data/manual/go-archive-zip/zip64-2.zip",
expected: Expected::Accept {
entry_count: 1,
entries: ZIP64_ENTRIES,
},
},
Fixture {
name: "readme.notzip",
path: "tests/data/manual/go-archive-zip/readme.notzip",
expected: Expected::Reject(Error::NotZip),
},
Fixture {
name: "test-baddirsz.zip",
path: "tests/data/manual/go-archive-zip/test-baddirsz.zip",
expected: Expected::Accept {
entry_count: 2,
entries: TEST_ENTRIES,
},
},
Fixture {
name: "test-badbase.zip",
path: "tests/data/manual/go-archive-zip/test-badbase.zip",
expected: Expected::Accept {
entry_count: 2,
entries: TEST_ENTRIES,
},
},
];
#[test]
fn manual_inspected_corpus() {
for fixture in MANUAL_FIXTURES {
match &fixture.expected {
Expected::Reject(kind) => {
let err = open_fixture_expect_err(fixture.path);
assert_eq!(
structural_err(&err),
*kind,
"fixture {}: expected open error {:?}, got {:?}",
fixture.name,
kind,
err
);
}
Expected::Accept {
entry_count,
entries,
} => {
let bytes = read_fixture(fixture.path);
let archive = open_archive(&bytes, fixture.path);
assert_archive_meta(fixture.name, &archive, *entry_count);
let mut iter = archive.entries();
for (entry_index, expected) in entries.iter().enumerate() {
let entry_index = entry_index as u64;
let entry = next_entry(fixture.name, &mut iter, entry_index);
assert_entry_matches(fixture.name, &archive, entry_index, &entry, expected);
let data = entry_data_range(fixture.name, entry_index, &entry);
assert_coherent_data_range(fixture.name, &archive, entry_index, &entry, &data);
}
assert!(
iter.next().is_none(),
"fixture {}: expected exactly {} entries",
fixture.name,
entries.len()
);
}
}
}
}
#[test]
fn valid_smoke_corpus() {
let paths = collect_corpus_files(VALID_ROOT);
assert!(
!paths.is_empty(),
"expected at least one file in {VALID_ROOT}"
);
for path in paths {
let bytes = read_fixture(&path);
let display = path.display().to_string();
let archive = open_archive(&bytes, &display);
let mut iter = archive.entries();
let mut seen = 0u64;
while seen < archive.entry_count() {
let entry = next_entry(&display, &mut iter, seen);
let name = entry_name(&display, seen, &entry);
let data = entry_data_range(&display, seen, &entry);
assert_coherent_data_range(&display, &archive, seen, &entry, &data);
assert_eq!(
range_len(&data.data_range),
entry.compressed_size(),
"fixture {} entry {} (name {}): compressed size mismatch between entry metadata and data range",
display,
seen,
format_name(&name)
);
seen += 1;
}
if let Some(extra) = iter.next() {
match extra {
Ok(entry) => {
let name = entry_name(&display, seen, &entry);
panic!(
"fixture {}: iterator yielded more entries than archive.entry_count()={}, extra entry name={}",
display,
archive.entry_count(),
format_name(&name)
);
}
Err(err) => {
panic!(
"fixture {display}: iterator returned error after {seen} successful entries: {err:?}"
);
}
}
}
}
}
#[test]
fn invalid_smoke_corpus() {
let paths = collect_corpus_files(INVALID_ROOT);
assert!(
!paths.is_empty(),
"expected at least one file in {INVALID_ROOT}"
);
for path in paths {
let bytes = read_fixture(&path);
let display = path.display().to_string();
let Some(reason) = fail_reason(&bytes, &display) else {
panic!(
"fixture {display}: expected an error at open, iteration, name read, or data-range resolution, but parsing succeeded cleanly"
);
};
assert!(
!reason.is_empty(),
"fixture {display}: expected a descriptive failure reason"
);
}
}
#[test]
fn synthetic_empty_zip() {
let bytes = empty_zip();
let archive = Archive::open(bytes.as_slice()).unwrap();
assert_eq!(archive.entry_count(), 0);
assert_eq!(archive.entries().count(), 0);
}
#[test]
fn synthetic_multidisk_is_rejected() {
let mut bytes = empty_zip();
bytes[4] = 1;
let err = Archive::open(bytes.as_slice()).err().unwrap();
assert_eq!(structural_err(&err), Error::MultiDisk);
}
#[test]
fn synthetic_truncated_eocd_is_rejected() {
let mut bytes = empty_zip();
bytes.pop();
let err = Archive::open(bytes.as_slice()).err().unwrap();
assert_eq!(structural_err(&err), Error::NotZip);
}
#[test]
fn synthetic_encrypted_entry_is_rejected_on_data_access() {
let mut bytes = simple_stored_zip(b"secret.txt", b"hidden");
bytes[6] |= 0x01;
let archive = Archive::open(bytes.as_slice()).unwrap();
let entry = archive.entries().next().unwrap().unwrap();
let err = entry.data_range().unwrap_err();
assert_eq!(structural_err(&err), Error::StrongEncryption);
}
#[test]
fn synthetic_truncated_local_header_is_rejected_on_data_access() {
let mut bytes = simple_stored_zip(b"name.txt", b"payload");
bytes[28] = 0xFF;
bytes[29] = 0xFF;
let archive = Archive::open(bytes.as_slice()).unwrap();
let entry = archive.entries().next().unwrap().unwrap();
let err = entry.data_range().unwrap_err();
assert_eq!(structural_err(&err), Error::Truncated);
}
#[test]
fn synthetic_filename_is_reads_only_needed_suffix_bytes() {
let mut path = vec![b'a'; 512];
path.extend_from_slice(b"/target.bin");
let bytes = simple_stored_zip(&path, b"payload");
let bytes_read = Cell::new(0);
let reader = CountingReader::new(&bytes, &bytes_read);
let archive = Archive::open(reader).unwrap();
let entry = archive.entries().next().unwrap().unwrap();
let before = bytes_read.get();
assert!(entry.filename_is(b"target.bin").unwrap());
let after = bytes_read.get();
assert!(
after > before,
"expected filename_is to perform additional reads beyond archive open"
);
assert!(
after - before < 128,
"expected filename_is to avoid reading the full path, but it read {} extra bytes for a {}-byte path",
after - before,
path.len()
);
}
#[test]
fn synthetic_path_is_utf8_trusts_zip_flag() {
let plain = simple_stored_zip(b"dir/hello.txt", b"payload");
let plain_archive = Archive::open(plain.as_slice()).unwrap();
let plain_entry = plain_archive.entries().next().unwrap().unwrap();
assert!(!plain_entry.path_is_utf8());
let mut flagged = simple_stored_zip(b"dir/\xFF.txt", b"payload");
set_utf8_flags(&mut flagged, b"dir/\xFF.txt", b"payload");
let flagged_archive = Archive::open(flagged.as_slice()).unwrap();
let flagged_entry = flagged_archive.entries().next().unwrap().unwrap();
assert!(flagged_entry.path_is_utf8());
}
fn fail_reason(bytes: &[u8], path: &str) -> Option<String> {
let archive = match Archive::open(bytes) {
Ok(archive) => archive,
Err(err) => return Some(format!("open failed: {err:?}")),
};
let mut iter = archive.entries();
let mut entry_index = 0u64;
while entry_index < archive.entry_count() {
let entry = match iter.next() {
Some(Ok(entry)) => entry,
Some(Err(err)) => return Some(format!("entry {entry_index} parse failed: {err:?}")),
None => return Some(format!("iterator ended early after {entry_index} entries")),
};
if let Err(err) = entry.read_path(&mut vec![0u8; PATH_BUF_LEN]) {
return Some(format!("entry {entry_index} name read failed: {err:?}"));
}
if let Err(err) = entry.data_range() {
return Some(format!("entry {entry_index} data range failed: {err:?}"));
}
entry_index += 1;
}
if let Some(extra) = iter.next() {
return Some(match extra {
Ok(_) => "iterator produced extra entry beyond entry_count".to_string(),
Err(err) => format!("iterator error after {entry_index} successful entries: {err:?}"),
});
}
let _ = path;
None
}
fn collect_corpus_files(root: &str) -> Vec<PathBuf> {
let mut paths = fs::read_dir(root)
.unwrap_or_else(|err| panic!("failed to read corpus dir {root}: {err}"))
.map(|entry| entry.unwrap().path())
.filter(|path| path.is_file())
.collect::<Vec<_>>();
paths.sort();
paths
}
fn read_fixture(path: impl AsRef<Path>) -> Vec<u8> {
let path = path.as_ref();
fs::read(path).unwrap_or_else(|err| panic!("failed to read {}: {}", path.display(), err))
}
fn open_fixture_expect_err(path: &str) -> Error<SliceReaderError> {
let bytes = read_fixture(path);
match Archive::open(bytes.as_slice()) {
Ok(archive) => panic!(
"fixture {}: expected open error, got archive with entry_count={}",
path,
archive.entry_count()
),
Err(err) => err,
}
}
fn open_archive<'a>(bytes: &'a [u8], label: &str) -> Archive<&'a [u8]> {
Archive::open(bytes)
.unwrap_or_else(|err| panic!("fixture {label}: expected archive to open, got {err:?}"))
}
fn assert_archive_meta(label: &str, archive: &Archive<&[u8]>, expected_entry_count: u64) {
assert_eq!(
archive.entry_count(),
expected_entry_count,
"fixture {}: entry_count mismatch, expected {}, got {}",
label,
expected_entry_count,
archive.entry_count()
);
}
fn next_entry<'a>(
label: &str,
iter: &mut impl Iterator<Item = Result<tinyzip::Entry<'a, &'a [u8]>, Error<SliceReaderError>>>,
entry_index: u64,
) -> tinyzip::Entry<'a, &'a [u8]> {
match iter.next() {
Some(Ok(entry)) => entry,
Some(Err(err)) => panic!(
"fixture {label} entry {entry_index}: expected entry, got iterator error {err:?}"
),
None => panic!("fixture {label} entry {entry_index}: expected entry, iterator ended early"),
}
}
fn assert_entry_matches(
label: &str,
archive: &Archive<&[u8]>,
entry_index: u64,
entry: &tinyzip::Entry<'_, &[u8]>,
expected: &ExpectedEntry,
) {
let name = entry_name(label, entry_index, entry);
assert_eq!(
name.as_slice(),
expected.name,
"fixture {} entry {}: name mismatch, expected {}, got {}",
label,
entry_index,
format_name(expected.name),
format_name(&name)
);
assert_eq!(
entry.compression().unwrap(),
expected.compression,
"fixture {} entry {} (name {}): compression mismatch, expected {:?}, got {:?}",
label,
entry_index,
format_name(&name),
expected.compression,
entry.compression().unwrap()
);
assert_eq!(
entry.compressed_size(),
expected.compressed_size,
"fixture {} entry {} (name {}): compressed_size mismatch, expected {}, got {}",
label,
entry_index,
format_name(&name),
expected.compressed_size,
entry.compressed_size()
);
assert_eq!(
entry.uncompressed_size(),
expected.uncompressed_size,
"fixture {} entry {} (name {}): uncompressed_size mismatch, expected {}, got {}",
label,
entry_index,
format_name(&name),
expected.uncompressed_size,
entry.uncompressed_size()
);
let _ = archive;
}
fn assert_coherent_data_range(
label: &str,
archive: &Archive<&[u8]>,
entry_index: u64,
entry: &tinyzip::Entry<'_, &[u8]>,
data: &tinyzip::DataRange,
) {
let name = entry_name(label, entry_index, entry);
assert!(
data.local_header_range.start < data.local_header_range.end,
"fixture {} entry {} (name {}): invalid local header range {:?}",
label,
entry_index,
format_name(&name),
data.local_header_range
);
assert!(
data.local_name_range.start >= data.local_header_range.start
&& data.local_name_range.end <= data.local_header_range.end,
"fixture {} entry {} (name {}): local name range {:?} escapes local header range {:?}",
label,
entry_index,
format_name(&name),
data.local_name_range,
data.local_header_range
);
assert!(
data.local_extra_range.start >= data.local_name_range.end
&& data.local_extra_range.end <= data.local_header_range.end,
"fixture {} entry {} (name {}): local extra range {:?} escapes local header range {:?}",
label,
entry_index,
format_name(&name),
data.local_extra_range,
data.local_header_range
);
assert!(
data.data_range.start >= data.local_header_range.end,
"fixture {} entry {} (name {}): data range {:?} starts before local header ends {:?}",
label,
entry_index,
format_name(&name),
data.data_range,
data.local_header_range
);
assert!(
data.data_range.end <= archive.size(),
"fixture {} entry {} (name {}): data range {:?} exceeds archive size {}",
label,
entry_index,
format_name(&name),
data.data_range,
archive.size()
);
}
fn entry_name(label: &str, entry_index: u64, entry: &tinyzip::Entry<'_, &[u8]>) -> Vec<u8> {
let mut name_buf = vec![0u8; PATH_BUF_LEN];
match entry.read_path(&mut name_buf) {
Ok(name) => name.to_vec(),
Err(err) => {
panic!("fixture {label} entry {entry_index}: failed to read path bytes: {err:?}")
}
}
}
fn entry_data_range(
label: &str,
entry_index: u64,
entry: &tinyzip::Entry<'_, &[u8]>,
) -> tinyzip::DataRange {
match entry.data_range() {
Ok(data) => data,
Err(err) => panic!(
"fixture {} entry {}: failed to resolve data range for name {} compression={:?} compressed_size={} uncompressed_size={}: {:?}",
label,
entry_index,
format_name(&entry_name(label, entry_index, entry)),
entry.compression(),
entry.compressed_size(),
entry.uncompressed_size(),
err
),
}
}
fn structural_err<E>(err: &Error<E>) -> Error<()> {
match err {
Error::Io(_) => panic!("unexpected I/O error"),
Error::NotZip => Error::NotZip,
Error::Truncated => Error::Truncated,
Error::InvalidSignature => Error::InvalidSignature,
Error::InvalidOffset => Error::InvalidOffset,
Error::InvalidRecord => Error::InvalidRecord,
Error::MultiDisk => Error::MultiDisk,
Error::StrongEncryption => Error::StrongEncryption,
Error::MaskedLocalHeaders => Error::MaskedLocalHeaders,
Error::UnsupportedCompression(method) => Error::UnsupportedCompression(*method),
Error::NotFound => Error::NotFound,
}
}
fn format_name(bytes: &[u8]) -> String {
let rendered = String::from_utf8_lossy(bytes);
if rendered.as_bytes() == bytes {
format!("{rendered:?}")
} else {
format!("{rendered:?} (raw bytes: {bytes:?})")
}
}
fn empty_zip() -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0x0605_4B50u32.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out
}
fn simple_stored_zip(name: &[u8], data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let local_offset = 0u32;
out.extend_from_slice(&0x0403_4B50u32.to_le_bytes());
out.extend_from_slice(&20u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&to_u32(data.len()).to_le_bytes());
out.extend_from_slice(&to_u32(data.len()).to_le_bytes());
out.extend_from_slice(&to_u16(name.len()).to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(name);
out.extend_from_slice(data);
let cd_offset = out.len();
out.extend_from_slice(&0x0201_4B50u32.to_le_bytes());
out.extend_from_slice(&20u16.to_le_bytes());
out.extend_from_slice(&20u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&to_u32(data.len()).to_le_bytes());
out.extend_from_slice(&to_u32(data.len()).to_le_bytes());
out.extend_from_slice(&to_u16(name.len()).to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&local_offset.to_le_bytes());
out.extend_from_slice(name);
let cd_size = out.len() - cd_offset;
out.extend_from_slice(&0x0605_4B50u32.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&to_u32(cd_size).to_le_bytes());
out.extend_from_slice(&to_u32(cd_offset).to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out
}
fn set_utf8_flags(zip: &mut [u8], name: &[u8], data: &[u8]) {
zip[7] |= 0x08;
let central_offset = 30 + name.len() + data.len();
zip[central_offset + 9] |= 0x08;
}
fn range_len(range: &std::ops::Range<u64>) -> u64 {
range.end - range.start
}
fn to_u16(value: usize) -> u16 {
u16::try_from(value).expect("value does not fit u16")
}
fn to_u32(value: usize) -> u32 {
u32::try_from(value).expect("value does not fit u32")
}
const PATH_BUF_LEN: usize = u16::MAX as usize;
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xEDB8_8320;
} else {
crc >>= 1;
}
}
}
!crc
}
#[test]
fn synthetic_path_is_full_match() {
let bytes = simple_stored_zip(b"dir/file.txt", b"payload");
let archive = Archive::open(bytes.as_slice()).unwrap();
let entry = archive.entries().next().unwrap().unwrap();
assert!(entry.path_is(b"dir/file.txt").unwrap());
assert!(entry.path_is("dir/file.txt").unwrap());
assert!(!entry.path_is(b"file.txt").unwrap());
assert!(!entry.path_is(b"dir/file.tx").unwrap());
assert!(!entry.path_is(b"dir/file.txtz").unwrap());
assert!(!entry.path_is(b"other/file.txt").unwrap());
}
#[test]
fn find_file_in_corpus() {
let bytes = read_fixture("tests/data/manual/go-archive-zip/test.zip");
let archive = Archive::open(bytes.as_slice()).unwrap();
let entry = archive.find_file(b"test.txt").unwrap();
assert_eq!(entry.compressed_size(), 25);
assert_eq!(entry.uncompressed_size(), 26);
}
#[test]
fn find_file_not_found() {
let bytes = read_fixture("tests/data/manual/go-archive-zip/test.zip");
let archive = Archive::open(bytes.as_slice()).unwrap();
let err = archive
.find_file(b"nonexistent.txt")
.err()
.expect("expected NotFound error");
assert_eq!(structural_err(&err), Error::NotFound);
}
#[test]
fn read_stored_entry_to_slice() {
let bytes = read_fixture("tests/data/manual/go-archive-zip/go-with-datadesc-sig.zip");
let archive = Archive::open(bytes.as_slice()).unwrap();
let entry = archive.find_file(b"foo.txt").unwrap();
assert_eq!(entry.compression().unwrap(), Compression::Stored);
let mut buf = [0u8; 64];
let data = entry.read_to_slice(&mut buf).unwrap();
assert_eq!(data, b"foo\n");
assert_eq!(crc32(data), entry.crc32());
}
#[test]
fn decompress_deflated_with_miniz_oxide() {
use miniz_oxide::inflate::decompress_slice_iter_to_slice;
let bytes = read_fixture("tests/data/manual/go-archive-zip/test.zip");
let archive = Archive::open(bytes.as_slice()).unwrap();
let entry = archive.find_file(b"test.txt").unwrap();
assert_eq!(entry.compression().unwrap(), Compression::Deflated);
let mut compressed = [0u8; 256];
let data = entry.read_to_slice(&mut compressed).unwrap();
let mut decompressed = [0u8; 256];
let n =
decompress_slice_iter_to_slice(&mut decompressed, data.chunks(512), false, false).unwrap();
assert_eq!(n, 26);
assert_eq!(&decompressed[..n], b"This is a test text file.\n");
assert_eq!(crc32(&decompressed[..n]), entry.crc32());
}
#[test]
fn read_chunks_lending_iterator() {
let bytes = read_fixture("tests/data/manual/go-archive-zip/go-with-datadesc-sig.zip");
let archive = Archive::open(bytes.as_slice()).unwrap();
let entry = archive.find_file(b"bar.txt").unwrap();
assert_eq!(entry.compression().unwrap(), Compression::Stored);
let mut chunks = entry.read_chunks::<2>().unwrap();
let mut collected = Vec::new();
while let Some(chunk) = chunks.next() {
collected.extend_from_slice(chunk.unwrap());
}
assert_eq!(collected, b"bar\n");
assert_eq!(crc32(&collected), entry.crc32());
}
#[cfg(feature = "std")]
mod std_tests {
use super::*;
use std::fs;
use std::fs::File;
use std::io::Read;
use tinyzip::std_io::ReadSeekReader;
#[test]
fn decompress_deflated_with_flate2() {
use flate2::read::DeflateDecoder;
let file_bytes = read_fixture("tests/data/manual/go-archive-zip/test.zip");
let reader = ReadSeekReader::new(std::io::Cursor::new(file_bytes));
let archive = Archive::open(reader).unwrap();
let entry = archive.find_file(b"test.txt").unwrap();
assert_eq!(entry.compression().unwrap(), Compression::Deflated);
let entry_reader = entry.reader().unwrap();
let mut decoder = DeflateDecoder::new(entry_reader);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed).unwrap();
assert_eq!(decompressed.len(), 26);
assert_eq!(decompressed, b"This is a test text file.\n");
assert_eq!(crc32(&decompressed), entry.crc32());
}
#[test]
fn reader_stored_io_copy() {
let file_bytes = read_fixture("tests/data/manual/go-archive-zip/go-with-datadesc-sig.zip");
let reader = ReadSeekReader::new(std::io::Cursor::new(file_bytes));
let archive = Archive::open(reader).unwrap();
let entry = archive.find_file(b"foo.txt").unwrap();
assert_eq!(entry.compression().unwrap(), Compression::Stored);
let mut entry_reader = entry.reader().unwrap();
let mut output = Vec::new();
std::io::copy(&mut entry_reader, &mut output).unwrap();
assert_eq!(output, b"foo\n");
assert_eq!(crc32(&output), entry.crc32());
}
#[test]
fn open_archive_from_file_without_manual_wrapper() {
let file_bytes = read_fixture("tests/data/manual/go-archive-zip/test.zip");
let path = std::env::temp_dir().join(format!(
"tinyzip-open-{}-{}.zip",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::write(&path, file_bytes).unwrap();
let result = (|| {
let zip_file = File::open(&path).unwrap();
let archive = Archive::try_from(zip_file).unwrap();
let entry = archive.find_file(b"test.txt").unwrap();
let mut decoder = flate2::read::DeflateDecoder::new(entry.reader().unwrap());
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed).unwrap();
assert_eq!(decompressed, b"This is a test text file.\n");
})();
fs::remove_file(&path).unwrap();
result
}
}