#![allow(clippy::unwrap_used, clippy::expect_used)]
use exarch_core::ExtractionOptions;
use exarch_core::SecurityConfig;
use exarch_core::extract_archive_with_options;
use sevenz_rust2::ArchiveEntry;
use sevenz_rust2::ArchiveWriter;
use std::io::Write as _;
use tempfile::NamedTempFile;
use tempfile::TempDir;
fn make_tar_with_duplicate(path: &str, first: &[u8], second: &[u8]) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut hdr = tar::Header::new_gnu();
hdr.set_size(first.len() as u64);
hdr.set_mode(0o644);
hdr.set_cksum();
builder.append_data(&mut hdr, path, first).unwrap();
let mut hdr = tar::Header::new_gnu();
hdr.set_size(second.len() as u64);
hdr.set_mode(0o644);
hdr.set_cksum();
builder.append_data(&mut hdr, path, second).unwrap();
builder.into_inner().unwrap()
}
fn write_tar(data: &[u8]) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(".tar").unwrap();
f.write_all(data).unwrap();
f.flush().unwrap();
f
}
#[cfg(unix)]
fn make_tar_single(path: &str, content: &[u8]) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut hdr = tar::Header::new_gnu();
hdr.set_size(content.len() as u64);
hdr.set_mode(0o644);
hdr.set_cksum();
builder.append_data(&mut hdr, path, content).unwrap();
builder.into_inner().unwrap()
}
#[test]
fn tar_skip_duplicates_true_keeps_first_entry() {
let data = make_tar_with_duplicate("file.txt", b"first", b"second");
let archive = write_tar(&data);
let dest = TempDir::new().unwrap();
let config = SecurityConfig::default();
let options = ExtractionOptions::default();
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("extraction with skip_duplicates=true must succeed");
assert_eq!(
report.files_extracted, 1,
"only the first entry is extracted"
);
assert_eq!(
report.files_skipped, 1,
"second entry must be counted as skipped"
);
assert_eq!(
report.warnings,
vec!["skipped 1 entry as pre-existing duplicates".to_string()],
"TAR now emits one aggregated duplicate-skip warning, not a per-path one"
);
let content = std::fs::read(dest.path().join("file.txt")).unwrap();
assert_eq!(content, b"first", "first entry content must be preserved");
}
#[test]
fn tar_skip_duplicates_false_overwrites_with_last_entry() {
let data = make_tar_with_duplicate("file.txt", b"first", b"second");
let archive = write_tar(&data);
let dest = TempDir::new().unwrap();
let config = SecurityConfig::default();
let options = ExtractionOptions::default().with_skip_duplicates(false);
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("extraction with skip_duplicates=false must succeed");
assert_eq!(
report.files_extracted, 2,
"both entries must be counted as extracted (second overwrites first)"
);
assert_eq!(report.files_skipped, 0, "no entries must be skipped");
let content = std::fs::read(dest.path().join("file.txt")).unwrap();
assert_eq!(
content, b"second",
"second entry must have overwritten the first"
);
}
fn make_sevenz_with_duplicate(path: &str, first: &[u8], second: &[u8]) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(".7z").unwrap();
{
let mut writer = ArchiveWriter::new(&mut f).unwrap();
writer
.push_archive_entry(ArchiveEntry::new_file(path), Some(first))
.unwrap();
writer
.push_archive_entry(ArchiveEntry::new_file(path), Some(second))
.unwrap();
writer.finish().unwrap();
}
f
}
fn make_sevenz_single(path: &str, content: &[u8]) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(".7z").unwrap();
{
let mut writer = ArchiveWriter::new(&mut f).unwrap();
writer
.push_archive_entry(ArchiveEntry::new_file(path), Some(content))
.unwrap();
writer.finish().unwrap();
}
f
}
#[cfg(unix)]
fn make_sevenz_two_entries(
path_a: &str,
content_a: &[u8],
path_b: &str,
content_b: &[u8],
) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(".7z").unwrap();
{
let mut writer = ArchiveWriter::new(&mut f).unwrap();
writer
.push_archive_entry(ArchiveEntry::new_file(path_a), Some(content_a))
.unwrap();
writer
.push_archive_entry(ArchiveEntry::new_file(path_b), Some(content_b))
.unwrap();
writer.finish().unwrap();
}
f
}
#[test]
fn sevenz_skip_duplicates_true_keeps_first_entry() {
let archive = make_sevenz_with_duplicate("file.txt", b"first", b"second");
let dest = TempDir::new().unwrap();
let config = SecurityConfig::default();
let options = ExtractionOptions::default();
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("extraction with skip_duplicates=true must succeed");
assert_eq!(
report.files_extracted, 1,
"only the first entry is extracted"
);
assert_eq!(
report.files_skipped, 1,
"second entry must be counted as skipped"
);
assert_eq!(
report.warnings,
vec!["skipped 1 entry as pre-existing duplicates".to_string()],
"7z emits one aggregated duplicate-skip warning, not a per-path one"
);
let content = std::fs::read(dest.path().join("file.txt")).unwrap();
assert_eq!(content, b"first", "first entry content must be preserved");
}
#[test]
fn sevenz_skip_duplicates_false_overwrites_with_last_entry() {
let archive = make_sevenz_with_duplicate("file.txt", b"first", b"second");
let dest = TempDir::new().unwrap();
let config = SecurityConfig::default();
let options = ExtractionOptions::default().with_skip_duplicates(false);
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("extraction with skip_duplicates=false must succeed");
assert_eq!(
report.files_extracted, 2,
"both entries must be counted as extracted (second overwrites first)"
);
assert_eq!(report.files_skipped, 0, "no entries must be skipped");
let content = std::fs::read(dest.path().join("file.txt")).unwrap();
assert_eq!(
content, b"second",
"second entry must have overwritten the first"
);
}
#[test]
fn tar_skip_duplicates_false_overwrites_nested_path() {
let data = make_tar_with_duplicate("subdir/nested.txt", b"original", b"overwritten");
let archive = write_tar(&data);
let dest = TempDir::new().unwrap();
let config = SecurityConfig::default();
let options = ExtractionOptions::default().with_skip_duplicates(false);
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("extraction with skip_duplicates=false must succeed for nested paths");
assert_eq!(report.files_extracted, 2);
assert_eq!(report.files_skipped, 0);
let content = std::fs::read(dest.path().join("subdir/nested.txt")).unwrap();
assert_eq!(content, b"overwritten");
}
#[cfg(unix)]
#[test]
fn tar_dangling_symlink_at_destination_skipped_not_followed() {
use std::os::unix::fs::symlink;
let dest = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let escape_target = outside.path().join("escaped.txt");
symlink(&escape_target, dest.path().join("file.txt")).unwrap();
assert!(
!escape_target.exists(),
"symlink target must be dangling before extraction"
);
let data = make_tar_single("file.txt", b"payload");
let archive = write_tar(&data);
let config = SecurityConfig::default();
let options = ExtractionOptions::default();
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("skip_duplicates=true must skip the symlinked path, not error");
assert_eq!(
report.files_skipped, 1,
"the symlinked path must be counted as skipped"
);
assert_eq!(report.files_extracted, 0);
assert!(
!escape_target.exists(),
"payload must never land outside dest via the dangling symlink"
);
assert!(
dest.path()
.join("file.txt")
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink(),
"the planted symlink must be left untouched, not replaced"
);
}
#[cfg(unix)]
#[test]
fn tar_dangling_symlink_at_destination_rejected_when_overwrite_requested() {
use std::os::unix::fs::symlink;
let dest = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let escape_target = outside.path().join("escaped.txt");
symlink(&escape_target, dest.path().join("file.txt")).unwrap();
assert!(
!escape_target.exists(),
"symlink target must be dangling before extraction"
);
let data = make_tar_single("file.txt", b"payload");
let archive = write_tar(&data);
let config = SecurityConfig::default();
let options = ExtractionOptions::default().with_skip_duplicates(false);
let result = extract_archive_with_options(archive.path(), dest.path(), &config, &options);
assert!(
result.is_err(),
"writing through a symlink at the destination must be rejected, not silently followed"
);
assert!(
!escape_target.exists(),
"payload must never land outside dest via the dangling symlink"
);
}
#[cfg(unix)]
#[test]
fn sevenz_dangling_symlink_at_destination_rejected_when_overwrite_requested() {
use std::os::unix::fs::symlink;
let dest = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let escape_target = outside.path().join("escaped.txt");
symlink(&escape_target, dest.path().join("file.txt")).unwrap();
assert!(
!escape_target.exists(),
"symlink target must be dangling before extraction"
);
let archive = make_sevenz_single("file.txt", b"payload");
let config = SecurityConfig::default();
let options = ExtractionOptions::default().with_skip_duplicates(false);
let result = extract_archive_with_options(archive.path(), dest.path(), &config, &options);
assert!(
result.is_err(),
"writing through a symlink at the destination must be rejected, not silently followed"
);
assert!(
!escape_target.exists(),
"payload must never land outside dest via the dangling symlink"
);
assert!(
dest.path()
.join("file.txt")
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink(),
"the pre-existing symlink must be left untouched, not unlinked and replaced"
);
}
#[cfg(unix)]
#[test]
fn sevenz_dangling_symlink_at_destination_quota_not_consumed_when_skipped() {
use std::os::unix::fs::symlink;
let dest = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let escape_target = outside.path().join("escaped.txt");
symlink(&escape_target, dest.path().join("link.txt")).unwrap();
assert!(
!escape_target.exists(),
"symlink target must be dangling before extraction"
);
let archive = make_sevenz_two_entries("link.txt", b"payload", "other.txt", b"second-file");
let config = SecurityConfig::default().with_max_file_count(1);
let options = ExtractionOptions::default();
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("a skipped duplicate must not permanently consume the file-count quota");
assert_eq!(
report.files_skipped, 1,
"the symlinked path must be counted as skipped"
);
assert_eq!(
report.files_extracted, 1,
"the second entry must still fit within max_file_count=1: it only does if the \
skipped entry never reserved quota in the first place"
);
assert!(dest.path().join("other.txt").exists());
assert!(
!escape_target.exists(),
"payload must never land outside dest via the dangling symlink"
);
}
#[test]
fn sevenz_regular_file_at_destination_still_overwritten_when_overwrite_requested() {
let dest = TempDir::new().unwrap();
std::fs::write(dest.path().join("file.txt"), b"pre-existing").unwrap();
let archive = make_sevenz_single("file.txt", b"payload");
let config = SecurityConfig::default();
let options = ExtractionOptions::default().with_skip_duplicates(false);
let report = extract_archive_with_options(archive.path(), dest.path(), &config, &options)
.expect("overwriting a pre-existing regular file must still succeed");
assert_eq!(report.files_extracted, 1);
let content = std::fs::read(dest.path().join("file.txt")).unwrap();
assert_eq!(content, b"payload", "regular file must be overwritten");
}
#[cfg(unix)]
#[test]
fn sevenz_live_symlink_at_destination_rejected_when_overwrite_requested() {
use std::os::unix::fs::symlink;
let dest = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let live_target = outside.path().join("real.txt");
std::fs::write(&live_target, b"outside content").unwrap();
symlink(&live_target, dest.path().join("file.txt")).unwrap();
assert!(live_target.exists(), "symlink target must resolve");
let archive = make_sevenz_single("file.txt", b"payload");
let config = SecurityConfig::default();
let options = ExtractionOptions::default().with_skip_duplicates(false);
let result = extract_archive_with_options(archive.path(), dest.path(), &config, &options);
assert!(
result.is_err(),
"a live symlink at the destination must be rejected just like a dangling one"
);
assert_eq!(
std::fs::read(&live_target).unwrap(),
b"outside content",
"the symlink's real target must never be written through"
);
assert!(
dest.path()
.join("file.txt")
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink(),
"the pre-existing symlink must be left untouched, not unlinked and replaced"
);
}