Skip to content

Commit b868768

Browse files
committed
fix: skip data reads on directory entries for RAR archives
libarchive's RAR reader returns ARCHIVE_FAILED on `archive_read_data_block` for directory entries, causing every RAR containing directories to error with "Can't decompress an entry marked as a directory". Skip the data-read step for directory entries in `uncompress_archive`, `uncompress_archive_file`, and `ArchiveIterator`. Closes #131
1 parent a34e9bf commit b868768

5 files changed

Lines changed: 104 additions & 3 deletions

File tree

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
## [Unreleased] - ReleaseDate
66

7+
* Fix RAR extraction failing with "Can't decompress an entry marked as a
8+
directory" whenever the archive contains directory entries. Data reads are
9+
now skipped for directory entries in `uncompress_archive`,
10+
`uncompress_archive_file`, and `ArchiveIterator` [#131]
711
* Add `list_archive_entries` (and `_with_encoding` variant) returning path and
812
uncompressed size per entry, so callers can obtain per-file sizes without
913
extracting the archive. Mirrored in `async_support`, `futures_support`, and

src/iterator.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::stat;
1212
use libc::stat;
1313

1414
use crate::{
15-
error::archive_result, ffi, ffi::UTF8LocaleGuard, DecodeCallback, Error, Result,
16-
READER_BUFFER_SIZE,
15+
error::archive_result, ffi, ffi::UTF8LocaleGuard, libarchive_entry_is_dir, DecodeCallback,
16+
Error, Result, READER_BUFFER_SIZE,
1717
};
1818

1919
struct HeapReadSeekerPipe<R: Read + Seek> {
@@ -76,6 +76,7 @@ pub struct ArchiveIterator<R: Read + Seek> {
7676

7777
decode: DecodeCallback,
7878
in_file: bool,
79+
current_is_dir: bool,
7980
closed: bool,
8081
error: bool,
8182
filter: Option<Box<EntryFilterCallbackFn>>,
@@ -243,6 +244,7 @@ impl<R: Read + Seek> ArchiveIterator<R> {
243244

244245
decode,
245246
in_file: false,
247+
current_is_dir: false,
246248
closed: false,
247249
error: false,
248250
filter,
@@ -380,13 +382,18 @@ impl<R: Read + Seek> ArchiveIterator<R> {
380382
Err(e) => return ArchiveContents::Err(e),
381383
};
382384
let stat = *ffi::archive_entry_stat(self.archive_entry);
385+
self.current_is_dir = libarchive_entry_is_dir(self.archive_entry);
383386
ArchiveContents::StartOfEntry(file_name, stat)
384387
}
385388
_ => ArchiveContents::Err(Error::from(self.archive_reader)),
386389
}
387390
}
388391

389392
unsafe fn next_data_chunk(&mut self) -> ArchiveContents {
393+
if self.current_is_dir {
394+
return ArchiveContents::EndOfEntry;
395+
}
396+
390397
let mut buffer = std::ptr::null();
391398
let mut offset = 0;
392399
let mut size = 0;

src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,9 @@ where
380380
ffi::archive_write_header(archive_writer, entry),
381381
archive_writer,
382382
)?;
383-
libarchive_copy_data(archive_reader, archive_writer)?;
383+
if !libarchive_entry_is_dir(entry) {
384+
libarchive_copy_data(archive_reader, archive_writer)?;
385+
}
384386

385387
archive_result_strict(
386388
ffi::archive_write_finish_entry(archive_writer),
@@ -470,6 +472,9 @@ where
470472
}
471473
}
472474

475+
if libarchive_entry_is_dir(entry) {
476+
return Ok(0);
477+
}
473478
libarchive_write_data_block(archive_reader, target)
474479
},
475480
)
@@ -701,6 +706,15 @@ fn libarchive_entry_size(entry: *mut ffi::archive_entry) -> u64 {
701706
size.max(0) as u64
702707
}
703708

709+
// Raw POSIX mode bits: `libc::S_IFDIR` is not exposed on Windows, where our
710+
// `stat` mirrors libarchive's own layout.
711+
pub(crate) fn libarchive_entry_is_dir(entry: *mut ffi::archive_entry) -> bool {
712+
const S_IFMT: u32 = 0o170000;
713+
const S_IFDIR: u32 = 0o040000;
714+
let mode = unsafe { (*ffi::archive_entry_stat(entry)).st_mode } as u32;
715+
(mode & S_IFMT) == S_IFDIR
716+
}
717+
704718
fn libarchive_entry_pathname<'a>(entry: *mut ffi::archive_entry) -> Result<&'a CStr> {
705719
let pathname = unsafe { ffi::archive_entry_pathname(entry) };
706720
if pathname.is_null() {

tests/fixtures/tree.rar

211 Bytes
Binary file not shown.

tests/integration_test.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,82 @@ fn uncompress_7z_to_dir_not_preserve_owner() {
556556
);
557557
}
558558

559+
#[test]
560+
fn get_a_file_from_rar() {
561+
let mut source = std::fs::File::open("tests/fixtures/tree.rar").unwrap();
562+
let mut target = Vec::default();
563+
564+
let written = uncompress_archive_file(&mut source, &mut target, "tree/branch2/leaf")
565+
.expect("Failed to get the file");
566+
assert_eq!(
567+
String::from_utf8_lossy(&target),
568+
"Goodbye World\n",
569+
"Uncompressed file did not match",
570+
);
571+
assert_eq!(written, 14, "Uncompressed bytes count did not match");
572+
}
573+
574+
#[test]
575+
fn iterate_rar_entries() {
576+
let source = std::fs::File::open("tests/fixtures/tree.rar").unwrap();
577+
578+
let mut names = Vec::new();
579+
let mut content = Vec::new();
580+
581+
for item in ArchiveIterator::from_read(source).expect("Failed to read archive") {
582+
match item {
583+
ArchiveContents::StartOfEntry(name, _) => names.push(name),
584+
ArchiveContents::DataChunk(chunk) => {
585+
if names
586+
.last()
587+
.map(|n| n == "tree/branch2/leaf")
588+
.unwrap_or(false)
589+
{
590+
content.extend_from_slice(&chunk);
591+
}
592+
}
593+
ArchiveContents::EndOfEntry => {}
594+
ArchiveContents::Err(e) => panic!("iterator errored: {}", e),
595+
}
596+
}
597+
598+
assert!(
599+
names.iter().any(|n| n == "tree/branch1"),
600+
"directory entry missing from iteration: {:?}",
601+
names
602+
);
603+
assert!(
604+
names.iter().any(|n| n == "tree/branch2/leaf"),
605+
"tree/branch2/leaf missing from iteration: {:?}",
606+
names
607+
);
608+
assert_eq!(String::from_utf8_lossy(&content), "Goodbye World\n");
609+
}
610+
611+
#[test]
612+
fn uncompress_rar_to_dir() {
613+
let dir = tempfile::TempDir::new().expect("Failed to create the tmp directory");
614+
let mut source = std::fs::File::open("tests/fixtures/tree.rar").unwrap();
615+
616+
uncompress_archive(&mut source, dir.path(), Ownership::Ignore)
617+
.expect("Failed to uncompress the file");
618+
619+
assert!(
620+
dir.path().join("tree/branch1/leaf").exists(),
621+
"the path doesn't exist"
622+
);
623+
assert!(
624+
dir.path().join("tree/branch2/leaf").exists(),
625+
"the path doesn't exist"
626+
);
627+
628+
let contents = std::fs::read_to_string(dir.path().join("tree/branch2/leaf")).unwrap();
629+
assert_eq!(
630+
contents, "Goodbye World\n",
631+
"Uncompressed file did not match"
632+
);
633+
}
634+
559635
#[test]
560636
fn uncompress_to_dir_with_utf8_pathname() {
561637
let dir = tempfile::TempDir::new().expect("Failed to create the tmp directory");

0 commit comments

Comments
 (0)