Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support to iterator over encrypted zip #116

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/uncompress_iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn main() -> compress_tools::Result<()> {

let source = std::fs::File::open(cmd.source_path)?;

for content in ArchiveIterator::from_read(source)? {
for content in ArchiveIterator::from_read(source, None)? {
if let ArchiveContents::StartOfEntry(name, stat) = content {
println!("{name}: size={}", stat.st_size);
}
Expand Down
1 change: 1 addition & 0 deletions scripts/generate-ffi
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ bindgen \
--whitelist-function "archive_read_data_block" \
--whitelist-function "archive_read_next_header" \
--whitelist-function "archive_read_open" \
--whitelist-function "archive_read_add_passphrase" \
--whitelist-function "archive_write_disk_new" \
--whitelist-function "archive_write_disk_set_options" \
--whitelist-function "archive_write_disk_set_standard_lookup" \
Expand Down
6 changes: 6 additions & 0 deletions src/ffi/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ extern "C" {
offset: *mut la_int64_t,
) -> ::std::os::raw::c_int;
}
extern "C" {
pub(crate) fn archive_read_add_passphrase(
arg1: *mut archive,
arg2: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
extern "C" {
pub(crate) fn archive_read_close(arg1: *mut archive) -> ::std::os::raw::c_int;
}
Expand Down
40 changes: 35 additions & 5 deletions src/iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ pub enum ArchiveContents {
/// The entry is processed on a return value of `true` and ignored on `false`.
pub type EntryFilterCallbackFn = dyn Fn(&str, &libc::stat) -> bool;

pub struct ArchivePassword(CString);

impl ArchivePassword {
pub fn extract(&self) -> *const i8 {
self.0.as_ptr() as *const i8
}
}

impl<T> From<T> for ArchivePassword
where
T: AsRef<str>,
{
fn from(s: T) -> Self {
Self(CString::new(s.as_ref()).unwrap())
}
}

/// An iterator over the contents of an archive.
#[allow(clippy::module_name_repetitions)]
pub struct ArchiveIterator<R: Read + Seek> {
Expand Down Expand Up @@ -119,6 +136,7 @@ impl<R: Read + Seek> ArchiveIterator<R> {
source: R,
decode: DecodeCallback,
filter: Option<Box<EntryFilterCallbackFn>>,
password: Option<ArchivePassword>,
) -> Result<ArchiveIterator<R>>
where
R: Read + Seek,
Expand All @@ -132,6 +150,10 @@ impl<R: Read + Seek> ArchiveIterator<R> {
let archive_entry: *mut ffi::archive_entry = std::ptr::null_mut();
let archive_reader = ffi::archive_read_new();

if let Some(password) = password {
ffi::archive_read_add_passphrase(archive_reader, password.extract());
}

let res = (|| {
archive_result(
ffi::archive_read_support_filter_all(archive_reader),
Expand Down Expand Up @@ -230,7 +252,7 @@ impl<R: Read + Seek> ArchiveIterator<R> {
where
R: Read + Seek,
{
Self::new(source, decode, None)
Self::new(source, decode, None, None)
}

/// Iterate over the contents of an archive, streaming the contents of each
Expand All @@ -245,7 +267,7 @@ impl<R: Read + Seek> ArchiveIterator<R> {
///
/// let mut name = String::default();
/// let mut size = 0;
/// let mut iter = ArchiveIterator::from_read(file)?;
/// let mut iter = ArchiveIterator::from_read(file, None)?;
///
/// for content in &mut iter {
/// match content {
Expand All @@ -265,11 +287,11 @@ impl<R: Read + Seek> ArchiveIterator<R> {
/// # Ok(())
/// # }
/// ```
pub fn from_read(source: R) -> Result<ArchiveIterator<R>>
pub fn from_read(source: R, password: Option<ArchivePassword>) -> Result<ArchiveIterator<R>>
where
R: Read + Seek,
{
Self::new(source, crate::decode_utf8, None)
Self::new(source, crate::decode_utf8, None, password)
}

/// Close the iterator, freeing up the associated resources.
Expand Down Expand Up @@ -392,6 +414,7 @@ where
source: R,
decoder: DecodeCallback,
filter: Option<Box<EntryFilterCallbackFn>>,
password: Option<ArchivePassword>,
}

/// A builder to generate an archive iterator over the contents of an
Expand Down Expand Up @@ -430,6 +453,7 @@ where
source,
decoder: crate::decode_utf8,
filter: None,
password: None,
}
}

Expand All @@ -450,8 +474,14 @@ where
self
}

/// Set a custom password to decode content of archive entries.
pub fn with_password(mut self, password: ArchivePassword) -> ArchiveIteratorBuilder<R> {
self.password = Some(password);
self
}

/// Finish the builder and generate the configured `ArchiveIterator`.
pub fn build(self) -> Result<ArchiveIterator<R>> {
ArchiveIterator::new(self.source, self.decoder, self.filter)
ArchiveIterator::new(self.source, self.decoder, self.filter, self.password)
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub mod tokio_support;
use error::archive_result;
pub use error::{Error, Result};
use io::{Seek, SeekFrom};
pub use iterator::{ArchiveContents, ArchiveIterator, ArchiveIteratorBuilder};
pub use iterator::{ArchiveContents, ArchiveIterator, ArchiveIteratorBuilder, ArchivePassword};
use std::{
ffi::{CStr, CString},
io::{self, Read, Write},
Expand Down
Binary file added tests/fixtures/with-password.zip
Binary file not shown.
51 changes: 48 additions & 3 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,8 +641,7 @@ fn iterate_zip_with_cjk_pathname() {
#[test]
fn iterate_truncated_archive() {
let source = std::fs::File::open("tests/fixtures/truncated.log.gz").unwrap();

for content in ArchiveIterator::from_read(source).unwrap() {
for content in ArchiveIterator::from_read(source, None).unwrap() {
if let ArchiveContents::Err(Error::Unknown) = content {
return;
}
Expand All @@ -654,7 +653,7 @@ fn iterate_truncated_archive() {
fn uncompress_bytes_helper(bytes: &[u8]) {
let wrapper = Cursor::new(bytes);

for content in ArchiveIterator::from_read(wrapper).unwrap() {
for content in ArchiveIterator::from_read(wrapper, None).unwrap() {
if let ArchiveContents::Err(Error::Unknown) = content {
return;
}
Expand Down Expand Up @@ -805,3 +804,49 @@ fn iterate_archive_with_filter_path() {
"filtered file list inside the archive did not match"
);
}

#[test]
fn iterate_archive_with_password() {
let source = std::fs::File::open("tests/fixtures/with-password.zip").unwrap();
let source_password: ArchivePassword = "123".into();

let mut files_result: Vec<String> = Vec::new();
let mut current_file_content: Vec<u8> = vec![];
let mut current_file_name = String::new();

let mut iter = ArchiveIteratorBuilder::new(source)
.with_password(source_password)
.filter(|name, _| name.ends_with(".txt"))
.build()
.unwrap();

for content in &mut iter {
match content {
ArchiveContents::StartOfEntry(name, _stat) => {
current_file_name = name;
}
ArchiveContents::DataChunk(dt) => {
current_file_content.extend(dt);
}
ArchiveContents::EndOfEntry => {
let content_raw = String::from_utf8(current_file_content.clone()).unwrap();
current_file_content.clear();

let content = format!("{}={}", current_file_name, content_raw);
files_result.push(content);
}
_ => {}
}
}

iter.close().unwrap();

assert_eq!(files_result.len(), 2);
assert_eq!(
files_result,
vec![
"with-password/file1.txt=its encrypted file".to_string(),
"with-password/file2.txt=file 2 in archive encrypted!".to_string()
]
);
}