use std::fmt;
use std::io::{Read, Seek, Write};
use std::path::Path;
use zip::write::SimpleFileOptions;
use zip::AesMode;
use crate::archive::{
check_entry_path_safety, normalize_path, ArchiveReader, ArchiveWriter, Entry, ExtractReport,
};
use crate::detect::ArchiveFormat;
use crate::error::{GeeZipError, GeeZipResult};
pub struct ZipReader<R: Read + Seek + Send> {
archive: zip::ZipArchive<R>,
format: ArchiveFormat,
password: Option<String>,
}
impl<R: Read + Seek + Send> fmt::Debug for ZipReader<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ZipReader")
.field("format", &self.format)
.finish_non_exhaustive()
}
}
impl<R: Read + Seek + Send> ZipReader<R> {
pub fn new(reader: R) -> GeeZipResult<Self> {
let archive = zip::ZipArchive::new(reader).map_err(convert_zip_error)?;
Ok(ZipReader {
archive,
format: ArchiveFormat::Zip,
password: None,
})
}
}
impl<R: Read + Seek + Send> ZipReader<R> {
pub fn set_password(&mut self, password: &str) {
self.password = Some(password.to_owned());
}
}
impl ZipReader<std::io::Cursor<Vec<u8>>> {
pub fn from_buf(buf: Vec<u8>) -> GeeZipResult<Self> {
ZipReader::new(std::io::Cursor::new(buf))
}
}
impl<R: Read + Seek + Send> ArchiveReader for ZipReader<R> {
fn format(&self) -> ArchiveFormat {
self.format
}
fn set_password(&mut self, password: &str) -> GeeZipResult<()> {
self.password = Some(password.to_owned());
Ok(())
}
fn entries(&mut self) -> GeeZipResult<Vec<Entry>> {
let len = self.archive.len();
let mut entries = Vec::with_capacity(len);
for i in 0..len {
let file = match &self.password {
Some(pwd) => self
.archive
.by_index_decrypt(i, pwd.as_bytes())
.map_err(convert_zip_error)?,
None => self.archive.by_index(i).map_err(convert_zip_error)?,
};
let modified = file.last_modified().map(|dt| {
crate::archive::datetime_to_timestamp(
dt.year() as u64,
dt.month() as u64,
dt.day() as u64,
dt.hour() as u64,
dt.minute() as u64,
dt.second() as u64,
)
});
entries.push(Entry {
path: file.name().to_owned(),
size: file.size(),
compressed_size: file.compressed_size(),
crc32: Some(file.crc32()),
modified,
is_dir: file.is_dir(),
});
}
Ok(entries)
}
fn extract(&mut self, entry: &Entry, writer: &mut dyn Write) -> GeeZipResult<u64> {
let mut file = match &self.password {
Some(password) => self
.archive
.by_name_decrypt(&entry.path, password.as_bytes())
.map_err(|e| match e {
zip::result::ZipError::FileNotFound => GeeZipError::EntryNotFound {
name: entry.path.clone(),
},
zip::result::ZipError::InvalidPassword => GeeZipError::Crypto {
message: format!("invalid password for '{}'", entry.path),
},
other => convert_zip_error(other),
})?,
None => self.archive.by_name(&entry.path).map_err(|e| match e {
zip::result::ZipError::FileNotFound => GeeZipError::EntryNotFound {
name: entry.path.clone(),
},
other => convert_zip_error(other),
})?,
};
let bytes = std::io::copy(&mut file, writer)
.map_err(|e| GeeZipError::io(e, format!("extracting '{}'", entry.path)))?;
Ok(bytes)
}
fn extract_all(&mut self, dest: &Path, overwrite: bool) -> GeeZipResult<ExtractReport> {
let entries = self.entries()?;
let mut report = ExtractReport::default();
let dest = normalize_path(dest);
for entry in &entries {
let entry_path = Path::new(&entry.path);
let target = match check_entry_path_safety(entry_path, &entry.path, &dest) {
Ok(t) => t,
Err((name, err)) => {
report.errors.push((name, err));
continue;
}
};
if entry.is_dir {
if let Err(e) = std::fs::create_dir_all(&target) {
report
.errors
.push((entry.path.clone(), GeeZipError::io(e, "creating directory")));
continue;
}
report.files_extracted += 1;
continue;
}
if let Some(parent) = target.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
report.errors.push((
entry.path.clone(),
GeeZipError::io(e, "creating parent directory"),
));
continue;
}
}
}
let mut output = if overwrite {
match std::fs::File::create(&target) {
Ok(f) => f,
Err(e) => {
report.errors.push((
entry.path.clone(),
GeeZipError::io(e, format!("creating '{}'", target.display())),
));
continue;
}
}
} else {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&target)
{
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
report.files_skipped += 1;
report.errors.push((
entry.path.clone(),
GeeZipError::clobber_denied(target.display().to_string()),
));
continue;
}
Err(e) => {
report.errors.push((
entry.path.clone(),
GeeZipError::io(e, format!("creating '{}'", target.display())),
));
continue;
}
}
};
match self.extract(entry, &mut output) {
Ok(bytes) => {
report.files_extracted += 1;
report.bytes_extracted += bytes;
}
Err(e) => {
report.errors.push((entry.path.clone(), e));
}
}
}
Ok(report)
}
}
pub struct ZipWriter<W: Write + Seek> {
inner: zip::ZipWriter<W>,
start_pos: u64,
format: ArchiveFormat,
password: Option<String>,
}
impl<W: Write + Seek> ZipWriter<W> {
pub fn new(mut writer: W) -> Self {
let start_pos = writer.stream_position().unwrap_or(0);
ZipWriter {
inner: zip::ZipWriter::new(writer),
start_pos,
format: ArchiveFormat::Zip,
password: None,
}
}
pub fn set_password(&mut self, password: &str) {
self.password = Some(password.to_owned());
}
pub fn finalize(self) -> GeeZipResult<(u64, W)> {
let start_pos = self.start_pos;
let mut writer = self.inner.finish().map_err(convert_zip_error)?;
let end_pos = writer
.stream_position()
.map_err(|e| GeeZipError::io(e, "getting final archive size"))?;
Ok((end_pos - start_pos, writer))
}
}
impl<W: Write + Seek + Send> ArchiveWriter for ZipWriter<W> {
fn format(&self) -> ArchiveFormat {
self.format
}
fn add_entry_from_reader(&mut self, path: &Path, reader: &mut dyn Read) -> GeeZipResult<()> {
let name = path.to_str().ok_or_else(|| GeeZipError::Format {
message: format!("non-UTF-8 path: {}", path.display()),
format: ArchiveFormat::Zip,
})?;
let mut options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::DEFLATE);
if let Some(password) = &self.password {
options = options.with_aes_encryption(AesMode::Aes256, password);
}
self.inner
.start_file(name, options)
.map_err(convert_zip_error)?;
std::io::copy(reader, &mut self.inner)
.map_err(|e| GeeZipError::io(e, format!("writing entry '{}'", name)))?;
Ok(())
}
fn add_directory(&mut self, path: &Path) -> GeeZipResult<()> {
let dir_path = format!("{}/", path.display());
let _name = path.to_str().ok_or_else(|| GeeZipError::Format {
message: format!("non-UTF-8 path: {}", path.display()),
format: ArchiveFormat::Zip,
})?;
let mut options = zip::write::FileOptions::<()>::default()
.compression_method(zip::CompressionMethod::Stored);
if let Some(password) = &self.password {
options = options.with_aes_encryption(AesMode::Aes256, password);
}
self.inner
.start_file(&dir_path, options)
.map_err(|e| GeeZipError::Format {
message: format!("starting ZIP directory entry: {e}"),
format: ArchiveFormat::Zip,
})?;
Ok(())
}
fn finish(self: Box<Self>) -> GeeZipResult<u64> {
let (bytes, _writer) = (*self).finalize()?;
Ok(bytes)
}
}
fn convert_zip_error(e: zip::result::ZipError) -> GeeZipError {
match e {
zip::result::ZipError::Io(inner) => GeeZipError::Io {
source: inner,
context: "ZIP operation failed".into(),
},
zip::result::ZipError::InvalidArchive(msg) => GeeZipError::Format {
message: format!("invalid ZIP archive: {msg}"),
format: ArchiveFormat::Zip,
},
zip::result::ZipError::FileNotFound => GeeZipError::EntryNotFound {
name: "(unknown)".into(),
},
zip::result::ZipError::UnsupportedArchive(msg) => GeeZipError::Format {
message: format!("unsupported ZIP feature: {msg}"),
format: ArchiveFormat::Zip,
},
zip::result::ZipError::InvalidPassword => GeeZipError::Crypto {
message: "invalid ZIP password".into(),
},
_ => GeeZipError::Format {
message: "unknown ZIP error".into(),
format: ArchiveFormat::Zip,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::path::PathBuf;
fn create_test_zip(files: &[(&str, &[u8])]) -> Vec<u8> {
let mut buf = Cursor::new(Vec::new());
{
let mut zip = zip::ZipWriter::new(&mut buf);
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
for (name, data) in files {
zip.start_file(*name, options).unwrap();
zip.write_all(data).unwrap();
}
zip.finish().unwrap();
}
buf.into_inner()
}
#[test]
fn zip_roundtrip_single_file() {
let content = b"hello world";
let data = create_test_zip(&[("hello.txt", content)]);
let mut reader = ZipReader::from_buf(data).unwrap();
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].path, "hello.txt");
assert_eq!(entries[0].size, content.len() as u64);
let mut output = Vec::new();
let bytes = reader.extract(&entries[0], &mut output).unwrap();
assert_eq!(bytes, content.len() as u64);
assert_eq!(output, content);
}
#[test]
fn zip_roundtrip_multiple_files() {
let files = [
("a.txt", b"aaa" as &[u8]),
("b.txt", b"bbb" as &[u8]),
("c.txt", b"ccc" as &[u8]),
];
let data = create_test_zip(&files);
let mut reader = ZipReader::from_buf(data).unwrap();
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 3);
for (i, (name, content)) in files.iter().enumerate() {
assert_eq!(entries[i].path, *name);
let mut output = Vec::new();
reader.extract(&entries[i], &mut output).unwrap();
assert_eq!(output, *content);
}
}
#[test]
fn zip_roundtrip_nested_path() {
let content = b"nested content";
let data = create_test_zip(&[("dir/subdir/file.txt", content)]);
let mut reader = ZipReader::from_buf(data).unwrap();
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].path, "dir/subdir/file.txt");
let mut output = Vec::new();
let bytes = reader.extract(&entries[0], &mut output).unwrap();
assert_eq!(output, content);
assert_eq!(bytes, content.len() as u64);
}
#[test]
fn zip_unicode_filename() {
let content = b"unicode content";
let data = create_test_zip(&[("\u{4e2d}\u{6587}.txt", content)]);
let mut reader = ZipReader::from_buf(data).unwrap();
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].path.contains('\u{4e2d}'));
}
#[test]
fn zip_empty_archive_fails() {
let err = ZipReader::from_buf(vec![]).unwrap_err();
let msg = err.to_string();
assert!(
msg.to_lowercase().contains("zip") || msg.to_lowercase().contains("invalid"),
"expected ZIP-related error, got: {msg}"
);
}
#[test]
fn zip_corrupted_archive_fails() {
let bad_data = b"this is not a zip file at all";
let err = ZipReader::from_buf(bad_data.to_vec()).unwrap_err();
let msg = err.to_string();
assert!(
msg.to_lowercase().contains("zip") || msg.to_lowercase().contains("invalid"),
"expected ZIP-related error, got: {msg}"
);
}
#[test]
fn zip_slip_detection() {
use std::io::Write as IoWrite;
let mut buf = Cursor::new(Vec::new());
{
let inner = Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(inner);
let name = "../escape.txt";
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file(name, options).unwrap();
zip.write_all(b"malicious").unwrap();
let inner = zip.finish().unwrap();
buf.write_all(&inner.into_inner()).unwrap();
}
let mut reader = ZipReader::from_buf(buf.into_inner()).unwrap();
let entries = reader.entries().unwrap();
assert!(entries[0].path.contains(".."));
let dest = tempfile::tempdir().unwrap();
let report = reader.extract_all(dest.path(), true).unwrap();
assert!(
report
.errors
.iter()
.any(|(_, e)| matches!(e, GeeZipError::PathTraversal { .. })),
"expected PathTraversal error, got: {report:?}"
);
assert_eq!(report.files_extracted, 0);
}
#[test]
fn zip_slip_dotdot_in_middle() {
use std::io::Write as IoWrite;
let mut buf = Cursor::new(Vec::new());
{
let inner = Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(inner);
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("subdir/../../../escape.txt", options)
.unwrap();
zip.write_all(b"escape").unwrap();
let inner = zip.finish().unwrap();
buf.write_all(&inner.into_inner()).unwrap();
}
let mut reader = ZipReader::from_buf(buf.into_inner()).unwrap();
let dest = tempfile::tempdir().unwrap();
let report = reader.extract_all(dest.path(), true).unwrap();
assert!(
report
.errors
.iter()
.any(|(_, e)| matches!(e, GeeZipError::PathTraversal { .. })),
"expected PathTraversal for foo/../../bar, got: {report:?}"
);
assert_eq!(report.files_extracted, 0);
}
#[test]
fn zip_slip_absolute_path() {
use std::io::Write as IoWrite;
let mut buf = Cursor::new(Vec::new());
{
let inner = Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(inner);
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("/etc/passwd", options).unwrap();
zip.write_all(b"leak").unwrap();
let inner = zip.finish().unwrap();
buf.write_all(&inner.into_inner()).unwrap();
}
let mut reader = ZipReader::from_buf(buf.into_inner()).unwrap();
let dest = tempfile::tempdir().unwrap();
let report = reader.extract_all(dest.path(), true).unwrap();
assert!(
report
.errors
.iter()
.any(|(_, e)| matches!(e, GeeZipError::PathTraversal { .. })),
"expected PathTraversal for absolute path, got: {report:?}"
);
assert_eq!(report.files_extracted, 0);
}
#[test]
fn zip_extract_all_to_curdir() {
use std::io::Write as IoWrite;
let mut buf = Cursor::new(Vec::new());
{
let inner = Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(inner);
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("file_a.txt", options).unwrap();
zip.write_all(b"AAA").unwrap();
let inner = zip.finish().unwrap();
buf.write_all(&inner.into_inner()).unwrap();
}
let tmp = tempfile::tempdir().unwrap();
let orig_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let mut reader = ZipReader::from_buf(buf.into_inner()).unwrap();
let report = reader.extract_all(Path::new("."), true).unwrap();
std::env::set_current_dir(orig_cwd).unwrap();
assert_eq!(report.files_extracted, 1);
assert!(report.errors.is_empty(), "errors: {report:#?}");
assert!(
tmp.path().join("file_a.txt").exists(),
"file_a.txt should exist in {}",
tmp.path().display()
);
}
#[test]
fn zip_writer_roundtrip() {
let buf = Cursor::new(Vec::new());
let mut zip_writer = ZipWriter::new(buf);
zip_writer
.add_entry_from_reader(
&PathBuf::from("test.txt"),
&mut Cursor::new(b"hello from writer"),
)
.unwrap();
let (bytes_written, writer) = zip_writer.finalize().unwrap();
assert!(bytes_written > 0, "should have written something");
let data = writer.into_inner();
let mut reader = ZipReader::from_buf(data).unwrap();
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].path, "test.txt");
let mut output = Vec::new();
let extracted = reader.extract(&entries[0], &mut output).unwrap();
assert_eq!(extracted, b"hello from writer".len() as u64);
assert_eq!(output, b"hello from writer");
}
#[test]
fn zip_writer_multiple_files_roundtrip() {
let buf = Cursor::new(Vec::new());
let mut zip_writer = ZipWriter::new(buf);
let files = [
("f1.txt", b"content 1" as &[u8]),
("f2.txt", b"content 2" as &[u8]),
("sub/f3.txt", b"nested content" as &[u8]),
];
for (name, content) in &files {
zip_writer
.add_entry_from_reader(&PathBuf::from(name), &mut Cursor::new(content))
.unwrap();
}
let boxed: Box<dyn ArchiveWriter> = Box::new(zip_writer);
let _bytes_written = boxed.finish().unwrap();
}
#[test]
fn zip_writer_add_directory_roundtrip() {
let buf = Cursor::new(Vec::new());
let mut zip_writer = ZipWriter::new(buf);
zip_writer
.add_entry_from_reader(
&PathBuf::from("file.txt"),
&mut Cursor::new(b"hello from file"),
)
.unwrap();
zip_writer.add_directory(Path::new("emptydir")).unwrap();
let (bytes_written, writer) = zip_writer.finalize().unwrap();
assert!(bytes_written > 0, "should have written something");
let data = writer.into_inner();
let mut reader = ZipReader::from_buf(data).unwrap();
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 2, "should have file + directory entries");
let dir_entry = entries.iter().find(|e| e.is_dir).expect("directory entry");
assert!(dir_entry.path.contains("emptydir"));
assert!(dir_entry.is_dir);
let file_entry = entries.iter().find(|e| !e.is_dir).expect("file entry");
assert_eq!(file_entry.path, "file.txt");
assert!(!file_entry.is_dir);
let dest = tempfile::tempdir().unwrap();
let report = reader.extract_all(dest.path(), true).unwrap();
assert_eq!(report.files_extracted, 2);
assert!(report.errors.is_empty(), "extract_all errors: {report:?}");
assert!(dest.path().join("emptydir").is_dir());
let file_content = std::fs::read_to_string(dest.path().join("file.txt")).unwrap();
assert_eq!(file_content, "hello from file");
}
#[test]
fn zip_writer_finish_returns_bytes() {
let buf = Cursor::new(Vec::new());
let mut zip_writer = ZipWriter::new(buf);
zip_writer
.add_entry_from_reader(&PathBuf::from("data.bin"), &mut Cursor::new(b"data"))
.unwrap();
let boxed: Box<dyn ArchiveWriter> = Box::new(zip_writer);
let bytes = boxed.finish().unwrap();
assert!(bytes > 0, "should report bytes written");
}
#[test]
fn zip_entry_not_found() {
let data = create_test_zip(&[("exists.txt", b"data")]);
let mut reader = ZipReader::from_buf(data).unwrap();
let fake_entry = Entry {
path: "does_not_exist.txt".into(),
size: 0,
compressed_size: 0,
crc32: None,
modified: None,
is_dir: false,
};
let mut output = Vec::new();
let err = reader.extract(&fake_entry, &mut output).unwrap_err();
assert!(matches!(err, GeeZipError::EntryNotFound { .. }));
}
#[test]
fn zip_extract_all_basic() {
let data = create_test_zip(&[("file_a.txt", b"AAA"), ("file_b.txt", b"BBB")]);
let mut reader = ZipReader::from_buf(data).unwrap();
let dest = tempfile::tempdir().unwrap();
let report = reader.extract_all(dest.path(), true).unwrap();
assert_eq!(report.files_extracted, 2);
assert_eq!(report.bytes_extracted, 6);
assert!(report.errors.is_empty());
assert!(dest.path().join("file_a.txt").exists());
assert!(dest.path().join("file_b.txt").exists());
}
#[test]
fn zip_no_clobber_skips_existing_files() {
let data = create_test_zip(&[("file_a.txt", b"AAA"), ("file_b.txt", b"BBB")]);
let mut reader = ZipReader::from_buf(data).unwrap();
let dest = tempfile::tempdir().unwrap();
let report = reader.extract_all(dest.path(), true).unwrap();
assert_eq!(report.files_extracted, 2);
assert!(report.errors.is_empty());
let modified_path = dest.path().join("file_a.txt");
std::fs::write(&modified_path, b"MODIFIED").unwrap();
let mut reader2 = ZipReader::from_buf(create_test_zip(&[
("file_a.txt", b"AAA"),
("file_b.txt", b"BBB"),
]))
.unwrap();
let report2 = reader2.extract_all(dest.path(), false).unwrap();
assert_eq!(
report2.files_extracted, 0,
"existing files should be skipped"
);
assert_eq!(
report2.files_skipped, 2,
"both files should be counted as skipped"
);
assert_eq!(
std::fs::read_to_string(&modified_path).unwrap(),
"MODIFIED",
"existing file content should be preserved"
);
assert!(
report2
.errors
.iter()
.any(|(_, e)| matches!(e, GeeZipError::ClobberDenied { .. })),
"expected at least one ClobberDenied error"
);
}
#[test]
fn zip_extract_all_with_cancel_normal() {
let data = create_test_zip(&[("a.txt", b"aaa"), ("b.txt", b"bbb")]);
let mut reader = ZipReader::from_buf(data).unwrap();
let dest = tempfile::tempdir().unwrap();
let report = reader
.extract_all_with_cancel(dest.path(), true, &|| false)
.unwrap();
assert_eq!(report.files_extracted, 2);
assert_eq!(report.bytes_extracted, 6);
assert!(report.errors.is_empty());
assert_eq!(
std::fs::read_to_string(dest.path().join("a.txt")).unwrap(),
"aaa"
);
assert_eq!(
std::fs::read_to_string(dest.path().join("b.txt")).unwrap(),
"bbb"
);
}
#[test]
fn zip_extract_all_with_cancel_before_first_entry() {
let data = create_test_zip(&[("only.txt", b"data")]);
let mut reader = ZipReader::from_buf(data).unwrap();
let dest = tempfile::tempdir().unwrap();
let err = reader
.extract_all_with_cancel(dest.path(), true, &|| true)
.unwrap_err();
assert!(matches!(err, GeeZipError::Cancelled));
assert!(!dest.path().join("only.txt").exists());
}
#[test]
fn zip_extract_all_with_cancel_between_entries() {
use std::cell::Cell;
let data = create_test_zip(&[("first.txt", b"AAA"), ("second.txt", b"BBB")]);
let mut reader = ZipReader::from_buf(data).unwrap();
let dest = tempfile::tempdir().unwrap();
let call_count = Cell::new(0u32);
let is_cancelled = || {
call_count.set(call_count.get() + 1);
call_count.get() > 2
};
let result = reader.extract_all_with_cancel(dest.path(), true, &is_cancelled);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), GeeZipError::Cancelled));
assert_eq!(
std::fs::read_to_string(dest.path().join("first.txt")).unwrap(),
"AAA"
);
assert!(!dest.path().join("second.txt").exists());
}
#[test]
fn archive_reader_trait_object() {
fn use_reader(_r: &mut dyn ArchiveReader) {}
let data = create_test_zip(&[("dummy.txt", b"x")]);
let mut reader = ZipReader::from_buf(data).unwrap();
use_reader(&mut reader);
}
#[test]
fn archive_writer_trait_object() {
fn use_writer(_w: Box<dyn ArchiveWriter>) {}
let buf = Cursor::new(Vec::new());
let writer = ZipWriter::new(buf);
use_writer(Box::new(writer));
}
}
#[test]
fn zip_truncated_not_panic() {
let err = ZipReader::from_buf(vec![0x50, 0x4B, 0x03, 0x04]).unwrap_err();
let msg = err.to_string().to_lowercase();
assert!(
msg.contains("zip") || msg.contains("invalid"),
"expected ZIP error for truncated zip, got: {err}"
);
}
#[cfg(test)]
mod aes_tests {
use super::*;
use std::io::Cursor;
fn create_encrypted_zip(files: &[(&str, &[u8])], password: &str) -> Vec<u8> {
let mut buf = Cursor::new(Vec::new());
{
let mut zip = zip::ZipWriter::new(&mut buf);
for (name, data) in files {
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.with_aes_encryption(AesMode::Aes256, password);
zip.start_file(*name, options).unwrap();
zip.write_all(data).unwrap();
}
zip.finish().unwrap();
}
buf.into_inner()
}
#[test]
fn encrypted_roundtrip_correct_password() {
let content = b"secret data";
let data = create_encrypted_zip(&[("secret.txt", content)], "mypassword");
let mut reader = ZipReader::from_buf(data).unwrap();
reader.set_password("mypassword");
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 1);
let mut output = Vec::new();
let bytes = reader.extract(&entries[0], &mut output).unwrap();
assert_eq!(bytes, content.len() as u64);
assert_eq!(output, content);
}
#[test]
fn encrypted_wrong_password_fails() {
let content = b"secret data";
let data = create_encrypted_zip(&[("secret.txt", content)], "correctpw");
let mut reader = ZipReader::from_buf(data).unwrap();
reader.set_password("wrongpw");
let entries_result = reader.entries();
let err = match entries_result {
Ok(entries) => reader.extract(&entries[0], &mut Vec::new()).unwrap_err(),
Err(e) => e,
};
let msg = err.to_string().to_lowercase();
assert!(
msg.contains("password") || msg.contains("crypto"),
"expected password/crypto error, got: {err}"
);
}
#[test]
fn encrypted_no_password_fails() {
let content = b"secret data";
let data = create_encrypted_zip(&[("secret.txt", content)], "secret123");
let mut reader = ZipReader::from_buf(data).unwrap();
let err = reader.entries().unwrap_err();
let msg = err.to_string().to_lowercase();
assert!(
msg.contains("password") || msg.contains("crypto"),
"expected password/crypto error, got: {err}"
);
}
#[test]
fn encrypted_list_entries_with_password() {
let content = b"secret data";
let data = create_encrypted_zip(
&[("file1.txt", content), ("file2.txt", b"more data")],
"mypassword",
);
let mut reader = ZipReader::from_buf(data).unwrap();
reader.set_password("mypassword");
let entries = reader.entries().unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].path, "file1.txt");
assert_eq!(entries[1].path, "file2.txt");
}
#[test]
fn encrypted_empty_password_roundtrip() {
let mut buf = Cursor::new(Vec::new());
{
let mut zip = zip::ZipWriter::new(&mut buf);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.with_aes_encryption(AesMode::Aes256, "");
zip.start_file("test.txt", options).unwrap();
zip.write_all(b"data").unwrap();
zip.finish().unwrap();
}
let data = buf.into_inner();
let mut reader = ZipReader::from_buf(data).unwrap();
reader.set_password("");
let entries = reader.entries().unwrap();
let mut output = Vec::new();
let _ = reader.extract(&entries[0], &mut output);
assert_eq!(entries[0].path, "test.txt");
}
}