diff --git a/src/archive.rs b/src/archive.rs index e875124a..e6628006 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -217,12 +217,17 @@ impl Archive { for entry in self._entries(None)? { let mut file = entry.map_err(|e| TarError::new("failed to iterate over archive", e))?; if file.header().entry_type() == crate::EntryType::Directory { - directories.push(file); + directories.push((file.path()?.components().count(), file)); } else { file.unpack_in(dst)?; } } - for mut dir in directories { + + // Work from leaves up. Otherwise when a parent directory has + // no write permission, any empty subdirectories beneath it + // cannot be created. + directories.sort_by(|a, b| b.0.cmp(&a.0)); + for (_, mut dir) in directories { dir.unpack_in(dst)?; } diff --git a/tests/all.rs b/tests/all.rs index fa38ef62..4abcab57 100644 --- a/tests/all.rs +++ b/tests/all.rs @@ -1317,6 +1317,32 @@ fn read_only_directory_containing_files() { assert!(ar.unpack(td.path()).is_ok()); } +#[test] +fn read_only_directory_containing_empty_directory() { + let test_dirs_with_modes: Box = Box::new(|dirs| { + let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + + let mut b = Builder::new(Vec::::new()); + + for (path, mode) in dirs { + let mut h = Header::new_gnu(); + t!(h.set_path(path)); + h.set_size(0); + h.set_entry_type(EntryType::dir()); + h.set_mode(*mode); + h.set_cksum(); + t!(b.append(&h, "".as_bytes())); + } + + let contents = t!(b.into_inner()); + let mut ar = Archive::new(&contents[..]); + assert!(ar.unpack(td.path()).is_ok()); + }); + + test_dirs_with_modes(&[("dir/", 0o444), ("dir/subdir", 0o755)]); + test_dirs_with_modes(&[("dir/subdir", 0o755), ("dir/", 0o444)]); +} + // This test was marked linux only due to macOS CI can't handle `set_current_dir` correctly #[test] #[cfg(target_os = "linux")]