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

Fix error when uncompressing specific files from 7z archives #48

Merged
merged 2 commits into from
Feb 11, 2021
Merged
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
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@

## [Unreleased] - ReleaseDate

### Changed

* Update MSRV to 1.44.0

### Fixed

* Fix error when uncompressing specific files from 7z archives. [#48]

[#48]: https://github.com/OSSystems/compress-tools-rs/pull/48

## [0.9.0] - 2020-12-25

* Upgrade `tokio` to 1.0.0 release
Expand Down
4 changes: 2 additions & 2 deletions Cross.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[target.armv7-unknown-linux-gnueabihf]
image = "ossystems/rust-cross:armv7-unknown-linux-gnueabihf-libarchive-0.2.1"
image = "ossystems/rust-cross:armv7-unknown-linux-gnueabihf-libarchive-0.2.1a"

[target.aarch64-unknown-linux-gnu]
image = "ossystems/rust-cross:aarch64-unknown-linux-gnu-libarchive-0.2.1"
image = "ossystems/rust-cross:aarch64-unknown-linux-gnu-libarchive-0.2.1a"
1 change: 1 addition & 0 deletions binding_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fn main() {
.whitelist_var("ARCHIVE_EXTRACT_FFLAGS")
.whitelist_var("ARCHIVE_EXTRACT_XATTR")
.whitelist_function("archive_read_new")
.whitelist_function("archive_read_set_seek_callback")
.whitelist_function("archive_read_support_filter_all")
.whitelist_function("archive_read_support_format_all")
.whitelist_function("archive_read_support_format_raw")
Expand Down
10 changes: 9 additions & 1 deletion src/async_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use futures_util::{
};
use std::{
future::Future,
io::{ErrorKind, Read, Write},
io::{ErrorKind, Read, Seek, SeekFrom, Write},
path::Path,
};

Expand Down Expand Up @@ -53,6 +53,14 @@ impl Read for AsyncReadWrapper {
}
}

// Hints Rust compiler that the seek is indeed supported, but
// underlying, it is done by the libarchive_seek_callback() callback.
impl Seek for AsyncReadWrapper {
fn seek(&mut self, _: SeekFrom) -> std::io::Result<u64> {
unreachable!("We need to use libarchive_seek_callback() underlying.")
}
}
otavio marked this conversation as resolved.
Show resolved Hide resolved

fn make_async_read_wrapper_and_worker<R>(
mut read: R,
) -> (AsyncReadWrapper, impl Future<Output = Result<()>>)
Expand Down
14 changes: 14 additions & 0 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ pub type archive_read_callback = ::std::option::Option<
_buffer: *mut *const ::std::os::raw::c_void,
) -> la_ssize_t,
>;
pub type archive_seek_callback = ::std::option::Option<
unsafe extern "C" fn(
arg1: *mut archive,
_client_data: *mut ::std::os::raw::c_void,
offset: la_int64_t,
whence: ::std::os::raw::c_int,
) -> la_int64_t,
>;
pub type archive_open_callback = ::std::option::Option<
unsafe extern "C" fn(
arg1: *mut archive,
Expand All @@ -56,6 +64,12 @@ extern "C" {
extern "C" {
pub fn archive_read_support_format_raw(arg1: *mut archive) -> ::std::os::raw::c_int;
}
extern "C" {
pub fn archive_read_set_seek_callback(
arg1: *mut archive,
arg2: archive_seek_callback,
) -> ::std::os::raw::c_int;
}
extern "C" {
pub fn archive_read_open(
arg1: *mut archive,
Expand Down
202 changes: 147 additions & 55 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ pub mod tokio_support;

use error::archive_result;
pub use error::{Error, Result};
use io::{Seek, SeekFrom};
use std::{
ffi::{CStr, CString},
io::{self, Read, Write},
os::raw::c_void,
os::raw::{c_int, c_void},
path::Path,
slice,
};
Expand All @@ -78,13 +79,20 @@ pub enum Ownership {
Ignore,
}

struct Pipe<'a> {
struct ReaderPipe<'a> {
reader: &'a mut dyn Read,
buffer: &'a mut [u8],
}

trait ReadAndSeek: Read + Seek {}
impl<T> ReadAndSeek for T where T: Read + Seek {}

struct SeekableReaderPipe<'a> {
reader: &'a mut dyn ReadAndSeek,
buffer: &'a mut [u8],
}

enum Mode {
AllFormat,
RawFormat,
WriteDisk { ownership: Ownership },
}
Expand All @@ -105,29 +113,25 @@ enum Mode {
/// ```
pub fn list_archive_files<R>(source: R) -> Result<Vec<String>>
where
R: Read,
R: Read + Seek,
{
run_with_archive(
Mode::AllFormat,
source,
|archive_reader, _, mut entry| unsafe {
#[allow(clippy::vec_init_then_push)]
let mut file_list = Vec::new();
loop {
match ffi::archive_read_next_header(archive_reader, &mut entry) {
ffi::ARCHIVE_OK => {
file_list.push(
CStr::from_ptr(ffi::archive_entry_pathname(entry))
.to_str()?
.to_string(),
);
}
ffi::ARCHIVE_EOF => return Ok(file_list),
_ => return Err(Error::from(archive_reader)),
run_with_seekable_archive(source, |archive_reader, _, mut entry| unsafe {
let mut file_list = Vec::new();
#[allow(clippy::vec_init_then_push)]
loop {
match ffi::archive_read_next_header(archive_reader, &mut entry) {
ffi::ARCHIVE_OK => {
file_list.push(
CStr::from_ptr(ffi::archive_entry_pathname(entry))
.to_str()?
.to_string(),
);
}
ffi::ARCHIVE_EOF => return Ok(file_list),
_ => return Err(Error::from(archive_reader)),
}
},
)
}
})
}

/// Uncompress a file using the `source` need as reader and the `target` as a
Expand Down Expand Up @@ -199,7 +203,7 @@ where
/// ```
pub fn uncompress_archive<R>(source: R, dest: &Path, ownership: Ownership) -> Result<()>
where
R: Read,
R: Read + Seek,
{
run_with_archive(
Mode::WriteDisk { ownership },
Expand Down Expand Up @@ -265,35 +269,30 @@ where
/// ```
pub fn uncompress_archive_file<R, W>(source: R, target: W, path: &str) -> Result<usize>
where
R: Read,
R: Read + Seek,
W: Write,
{
run_with_archive(
Mode::AllFormat,
source,
|archive_reader, _, mut entry| unsafe {
loop {
match ffi::archive_read_next_header(archive_reader, &mut entry) {
ffi::ARCHIVE_OK => {
let file_name =
CStr::from_ptr(ffi::archive_entry_pathname(entry)).to_str()?;
if file_name == path {
break;
}
}
ffi::ARCHIVE_EOF => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path {} doesn't exist inside archive", path),
)
.into())
run_with_seekable_archive(source, |archive_reader, _, mut entry| unsafe {
loop {
match ffi::archive_read_next_header(archive_reader, &mut entry) {
ffi::ARCHIVE_OK => {
let file_name = CStr::from_ptr(ffi::archive_entry_pathname(entry)).to_str()?;
if file_name == path {
break;
}
_ => return Err(Error::from(archive_reader)),
}
ffi::ARCHIVE_EOF => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path {} doesn't exist inside archive", path),
)
.into())
}
_ => return Err(Error::from(archive_reader)),
}
libarchive_write_data_block(archive_reader, target)
},
)
}
libarchive_write_data_block(archive_reader, target)
})
}

fn run_with_archive<F, R, T>(mode: Mode, mut reader: R, f: F) -> Result<T>
Expand All @@ -317,10 +316,6 @@ where
ffi::archive_read_support_format_raw(archive_reader),
archive_reader,
)?,
Mode::AllFormat => archive_result(
ffi::archive_read_support_format_all(archive_reader),
archive_reader,
)?,
Mode::WriteDisk { ownership } => {
let mut writer_flags = ffi::ARCHIVE_EXTRACT_TIME
| ffi::ARCHIVE_EXTRACT_PERM
Expand Down Expand Up @@ -355,15 +350,15 @@ where
return Err(Error::NullArchive);
}

let mut pipe = Pipe {
let mut pipe = ReaderPipe {
reader: &mut reader,
buffer: &mut [0; READER_BUFFER_SIZE],
};

archive_result(
ffi::archive_read_open(
archive_reader,
(&mut pipe as *mut Pipe) as *mut c_void,
(&mut pipe as *mut ReaderPipe) as *mut c_void,
None,
Some(libarchive_read_callback),
None,
Expand All @@ -386,6 +381,62 @@ where
}
}

fn run_with_seekable_archive<F, R, T>(mut reader: R, f: F) -> Result<T>
where
F: FnOnce(*mut ffi::archive, *mut ffi::archive, *mut ffi::archive_entry) -> Result<T>,
R: Read + Seek,
{
unsafe {
let archive_entry: *mut ffi::archive_entry = std::ptr::null_mut();
let archive_reader = ffi::archive_read_new();
let archive_writer = ffi::archive_write_disk_new();

let res = (|| {
archive_result(
ffi::archive_read_support_format_all(archive_reader),
archive_reader,
)?;

archive_result(
ffi::archive_read_set_seek_callback(archive_reader, Some(libarchive_seek_callback)),
archive_reader,
)?;

if archive_reader.is_null() || archive_writer.is_null() {
return Err(Error::NullArchive);
}

let mut pipe = SeekableReaderPipe {
reader: &mut reader,
buffer: &mut [0; READER_BUFFER_SIZE],
};

archive_result(
ffi::archive_read_open(
archive_reader,
(&mut pipe as *mut SeekableReaderPipe) as *mut c_void,
None,
Some(libarchive_seekable_read_callback),
None,
),
archive_reader,
)?;

f(archive_reader, archive_writer, archive_entry)
})();

archive_result(ffi::archive_read_close(archive_reader), archive_reader)?;
archive_result(ffi::archive_read_free(archive_reader), archive_reader)?;

archive_result(ffi::archive_write_close(archive_writer), archive_writer)?;
archive_result(ffi::archive_write_free(archive_writer), archive_writer)?;

ffi::archive_entry_free(archive_entry);

res
}
}

fn libarchive_copy_data(
archive_reader: *mut ffi::archive,
archive_writer: *mut ffi::archive,
Expand Down Expand Up @@ -436,12 +487,53 @@ where
}
}

unsafe extern "C" fn libarchive_seek_callback(
_: *mut ffi::archive,
client_data: *mut c_void,
offset: ffi::la_int64_t,
whence: c_int,
) -> i64 {
let pipe = (client_data as *mut SeekableReaderPipe).as_mut().unwrap();
let whence = match whence {
0 => SeekFrom::Start(offset as u64),
1 => SeekFrom::Current(offset),
2 => SeekFrom::End(offset),
_ => return -1,
};

match pipe.reader.seek(whence) {
Ok(offset) => offset as i64,
Err(_) => -1,
}
}

unsafe extern "C" fn libarchive_seekable_read_callback(
archive: *mut ffi::archive,
client_data: *mut c_void,
buffer: *mut *const c_void,
) -> ffi::la_ssize_t {
let pipe = (client_data as *mut SeekableReaderPipe).as_mut().unwrap();

*buffer = pipe.buffer.as_ptr() as *const c_void;

match pipe.reader.read(&mut pipe.buffer) {
Ok(size) => size as ffi::la_ssize_t,
Err(e) => {
let description = CString::new(e.to_string()).unwrap();

ffi::archive_set_error(archive, e.raw_os_error().unwrap_or(0), description.as_ptr());

-1
}
}
}

unsafe extern "C" fn libarchive_read_callback(
archive: *mut ffi::archive,
client_data: *mut c_void,
buffer: *mut *const c_void,
) -> ffi::la_ssize_t {
let pipe = (client_data as *mut Pipe).as_mut().unwrap();
let pipe = (client_data as *mut ReaderPipe).as_mut().unwrap();

*buffer = pipe.buffer.as_ptr() as *const c_void;

Expand Down
Binary file added tests/fixtures/tree.7z
Binary file not shown.
15 changes: 15 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ fn get_a_file_from_tar() {
assert_eq!(written, 14, "Uncompressed bytes count did not match");
}

#[test]
fn get_a_file_from_7z() {
let mut source = std::fs::File::open("tests/fixtures/tree.7z").unwrap();
let mut target = Vec::default();

let written = uncompress_archive_file(&mut source, &mut target, &"tree/branch2/leaf")
.expect("Failed to get the file");
assert_eq!(
String::from_utf8_lossy(&target),
"Goodbye World\n",
"Uncompressed file did not match",
);
assert_eq!(written, 14, "Uncompressed bytes count did not match");
}

#[async_std::test]
#[cfg(feature = "futures_support")]
async fn get_a_file_from_tar_futures() {
Expand Down