use super::fixtures::{Put, scratch, sevenz, tar, zip};
use super::*;
fn names(index: &Index, inner: &str) -> Vec<String> {
let mut names: Vec<String> = index.children(Path::new(inner)).map(|entry| entry.name().to_string_lossy().into_owned()).collect();
names.sort();
names
}
#[test]
fn a_zip_lists_as_a_tree_with_its_implied_directories() {
let dir = scratch("zip-tree");
let path = dir.join("photos.zip");
zip(&path, &[("2024/june/beach.jpg", Put::File(b"sand")), ("2024/june/sea.jpg", Put::File(b"waves!")), ("readme.txt", Put::File(b"hi")), ("empty/", Put::Dir)]);
let index = super::index(&path).unwrap();
assert_eq!(index.kind, Kind::Zip);
assert_eq!(names(&index, ""), ["2024", "empty", "readme.txt"]);
assert_eq!(names(&index, "2024/june"), ["beach.jpg", "sea.jpg"]);
assert!(index.is_dir(Path::new("2024")) && index.is_dir(Path::new("")) && !index.is_dir(Path::new("readme.txt")));
assert_eq!(index.size_of(Path::new("2024")), 10);
assert_eq!(index.size_of(Path::new("")), 12);
let implied = index.entry(index.find(Path::new("2024")).unwrap());
assert_eq!(implied.position, None);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_tar_gz_lists_with_what_tar_keeps() {
let dir = scratch("tar-tree");
let path = dir.join("src.tar.gz");
tar(&path, &[(b"src/", Put::Dir), (b"src/main.rs", Put::File(b"old")), (b"src/link", Put::Link("main.rs")), (b"src/main.rs", Put::File(b"newer"))]);
let index = super::index(&path).unwrap();
assert_eq!(index.kind, Kind::TarGz);
assert_eq!(names(&index, "src"), ["link", "main.rs"]);
let main = index.entry(index.find(Path::new("src/main.rs")).unwrap());
assert_eq!((main.size, main.position, main.mode.map(|mode| mode & 0o777)), (5, Some(3), Some(0o640)));
assert_eq!(main.modified, Some(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000)));
let link = index.entry(index.find(Path::new("src/link")).unwrap());
assert_eq!((link.kind, link.link.as_deref()), (EntryKind::Link, Some(Path::new("main.rs"))));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn names_that_reach_outside_are_refused() {
let dir = scratch("refused");
let path = dir.join("evil.tar");
tar(
&path,
&[
(b"../escape", Put::File(b"x")),
(b"a/../../escape", Put::File(b"x")),
(b"/etc/motd", Put::File(b"rooted")),
(b"out", Put::Link("/tmp")),
(b"out/through", Put::File(b"x")),
(b"fine.txt", Put::File(b"ok")),
],
);
let index = super::index(&path).unwrap();
assert_eq!(names(&index, ""), ["etc", "fine.txt", "out"]);
assert_eq!(names(&index, "etc"), ["motd"]);
assert_eq!(index.refused, 3);
let path = dir.join("evil.zip");
zip(&path, &[("../escape", Put::File(b"x")), ("fine.txt", Put::File(b"ok"))]);
let index = super::index(&path).unwrap();
assert_eq!((names(&index, ""), index.refused), (vec!["fine.txt".to_string()], 1));
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn a_tar_name_need_not_be_utf8() {
use std::os::unix::ffi::OsStrExt;
let dir = scratch("latin1");
let path = dir.join("old.tar");
tar(&path, &[(b"caf\xe9.txt", Put::File(b"x"))]);
let index = super::index(&path).unwrap();
let entry = index.children(Path::new("")).next().unwrap();
assert_eq!(entry.name().as_bytes(), b"caf\xe9.txt");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_path_through_an_archive_is_located() {
let dir = scratch("locate");
let path = dir.join("a.zip");
zip(&path, &[("x/y.txt", Put::File(b"y"))]);
let located = |path: &Path| locate(path).map(|found| (found.archive, found.inner));
assert_eq!(located(&path.join("x").join("y.txt")), Some((path.clone(), PathBuf::from("x/y.txt"))));
assert_eq!(located(&path.join("not/there")), Some((path.clone(), PathBuf::from("not/there"))));
assert_eq!(located(&path), Some((path.clone(), PathBuf::new())));
assert_eq!(located(&dir), None);
assert_eq!(located(&dir.join("gone").join("deeper")), None);
std::fs::write(dir.join("plain.txt"), "").unwrap();
assert_eq!(located(&dir.join("plain.txt").join("x")), None);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn an_index_is_kept_until_its_archive_changes() {
let dir = scratch("cache");
let path = dir.join("a.tar");
tar(&path, &[(b"one", Put::File(b"1"))]);
let first = super::index(&path).unwrap();
assert!(Arc::ptr_eq(&first, &super::index(&path).unwrap()));
tar(&path, &[(b"one", Put::File(b"1")), (b"two", Put::File(b"22"))]);
let second = super::index(&path).unwrap();
assert_eq!(names(&second, ""), ["one", "two"]);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn entries_are_read_and_read_again() {
let dir = scratch("read");
for name in ["a.zip", "a.tar.gz"] {
let path = dir.join(name);
if name.ends_with("zip") {
zip(&path, &[("first", Put::File(b"1st")), ("second", Put::File(b"2nd"))]);
} else {
tar(&path, &[(b"first", Put::File(b"1st")), (b"second", Put::File(b"2nd"))]);
}
let index = super::index(&path).unwrap();
let positions: Vec<usize> = ["second", "first"].iter().map(|inner| index.entry(index.find(Path::new(inner)).unwrap()).position.unwrap()).collect();
let mut read = Vec::new();
let mut asked_again = false;
read_entries(&path, index.kind, &positions, &mut |_, contents| {
let mut text = String::new();
contents.unwrap().read_to_string(&mut text).unwrap();
read.push(text.clone());
if text == "1st" && !asked_again {
asked_again = true;
return Next::Again;
}
Next::Go
})
.unwrap();
read.sort();
assert_eq!(read, ["1st", "1st", "2nd"], "{name}");
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_file_named_like_an_archive_that_is_not_one_fails_to_read() {
let dir = scratch("fake");
for name in ["fake.zip", "fake.tar.gz"] {
std::fs::write(dir.join(name), "plain text, not an archive").unwrap();
assert!(super::index(&dir.join(name)).is_err(), "{name}");
}
std::fs::remove_dir_all(&dir).unwrap();
}
fn every_format(dir: &Path) -> Vec<PathBuf> {
let mut made = Vec::new();
for name in ["a.zip", "a.tar", "a.tar.gz", "a.tar.zst", "a.tar.xz", "a.tar.bz2"] {
let path = dir.join(name);
if name.ends_with("zip") {
zip(&path, &[("docs/notes.txt", Put::File(b"notes")), ("docs/deep/more.txt", Put::File(b"more!")), ("top.txt", Put::File(b"top"))]);
} else {
tar(&path, &[(b"docs/notes.txt", Put::File(b"notes")), (b"docs/deep/more.txt", Put::File(b"more!")), (b"top.txt", Put::File(b"top"))]);
}
made.push(path);
}
for (name, solid) in [("solid.7z", true), ("loose.7z", false)] {
let path = dir.join(name);
sevenz(&path, &[("docs/notes.txt", Put::File(b"notes")), ("docs/deep/more.txt", Put::File(b"more!")), ("top.txt", Put::File(b"top"))], solid);
made.push(path);
}
made
}
#[test]
fn every_format_lists_and_reads_alike() {
let dir = scratch("formats");
for path in every_format(&dir) {
let name = path.file_name().unwrap().to_string_lossy().into_owned();
let index = super::index(&path).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(names(&index, ""), ["docs", "top.txt"], "{name}");
assert_eq!(names(&index, "docs"), ["deep", "notes.txt"], "{name}");
assert_eq!(index.size_of(Path::new("")), 13, "{name}");
let wanted = ["top.txt", "docs/deep/more.txt", "docs/notes.txt"];
let positions: Vec<usize> = wanted.iter().map(|inner| index.entry(index.find(Path::new(inner)).unwrap()).position.unwrap()).collect();
let mut read = Vec::new();
read_entries(&path, index.kind, &positions, &mut |position, contents| {
let mut text = String::new();
contents.unwrap().read_to_string(&mut text).unwrap();
read.push((position, text));
Next::Go
})
.unwrap();
read.sort();
let mut expected: Vec<(usize, String)> = positions.iter().zip(["top", "more!", "notes"]).map(|(&position, text)| (position, text.to_string())).collect();
expected.sort();
assert_eq!(read, expected, "{name}");
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_solid_7z_entry_left_half_read_does_not_spoil_the_next() {
let dir = scratch("7z-drain");
let path = dir.join("a.7z");
sevenz(&path, &[("first", Put::File(b"0123456789")), ("second", Put::File(b"second"))], true);
let index = super::index(&path).unwrap();
let positions: Vec<usize> = ["first", "second"].iter().map(|inner| index.entry(index.find(Path::new(inner)).unwrap()).position.unwrap()).collect();
let mut second = String::new();
read_entries(&path, index.kind, &positions, &mut |position, contents| {
let contents = contents.unwrap();
if position == positions[0] {
let mut half = [0; 3];
contents.read_exact(&mut half).unwrap();
} else {
contents.read_to_string(&mut second).unwrap();
}
Next::Go
})
.unwrap();
assert_eq!(second, "second");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_tar_zst_of_several_frames_reads_whole() {
let dir = scratch("zst-frames");
let plain = dir.join("a.tar");
tar(&plain, &[(b"one", Put::File(b"first")), (b"two", Put::File(b"second"))]);
let bytes = std::fs::read(&plain).unwrap();
let (front, back) = bytes.split_at(700);
let mut joined = ruzstd::encoding::compress_to_vec(front, ruzstd::encoding::CompressionLevel::Fastest);
joined.extend(ruzstd::encoding::compress_to_vec(back, ruzstd::encoding::CompressionLevel::Fastest));
let path = dir.join("a.tar.zst");
std::fs::write(&path, joined).unwrap();
assert_eq!(names(&super::index(&path).unwrap(), ""), ["one", "two"]);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_7z_reads_either_separator_and_refuses_what_reaches_out() {
let dir = scratch("7z-names");
let path = dir.join("a.7z");
sevenz(&path, &[("win\\dir\\file.txt", Put::File(b"w")), ("..\\escape", Put::File(b"x")), ("link", Put::Link("win/dir/file.txt"))], false);
let index = super::index(&path).unwrap();
assert_eq!(names(&index, ""), ["link", "win"]);
assert_eq!(names(&index, "win/dir"), ["file.txt"]);
assert_eq!(index.refused, 1);
assert_eq!(index.entry(index.find(Path::new("link")).unwrap()).kind, EntryKind::Link);
std::fs::remove_dir_all(&dir).unwrap();
}
fn seven_zip_made(dir: &Path, name: &str) -> PathBuf {
let bytes: &[u8] = match name {
"plain.7z" => include_bytes!("testdata/plain.7z"),
"secret.7z" => include_bytes!("testdata/secret.7z"),
_ => include_bytes!("testdata/hidden.7z"),
};
let path = dir.join(name);
std::fs::write(&path, bytes).unwrap();
path
}
#[test]
fn a_7z_from_7_zip_reads_as_it_was_made() {
use chrono::TimeZone;
let dir = scratch("7-zip");
let path = seven_zip_made(&dir, "plain.7z");
let index = super::index(&path).unwrap();
assert_eq!(names(&index, ""), ["b.txt", "dir"]);
assert_eq!(names(&index, "dir"), ["a.txt"]);
let b = index.entry(index.find(Path::new("b.txt")).unwrap());
assert_eq!(b.mode.map(|mode| mode & 0o777), Some(0o644));
let made = chrono::Local.with_ymd_and_hms(2024, 1, 2, 3, 4, 5).unwrap();
assert_eq!(b.modified, Some(SystemTime::from(made)));
let positions: Vec<usize> = ["dir/a.txt", "b.txt"].iter().map(|inner| index.entry(index.find(Path::new(inner)).unwrap()).position.unwrap()).collect();
let mut read = Vec::new();
read_entries(&path, index.kind, &positions, &mut |_, contents| {
let mut text = String::new();
contents.unwrap().read_to_string(&mut text).unwrap();
read.push(text);
Next::Go
})
.unwrap();
read.sort();
assert_eq!(read, ["alpha", "bravo"]);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn an_encrypted_7z_says_it_needs_the_password() {
let dir = scratch("7z-secret");
let path = seven_zip_made(&dir, "secret.7z");
let index = super::index(&path).unwrap();
assert_eq!(names(&index, ""), ["b.txt"]);
let position = index.entry(index.find(Path::new("b.txt")).unwrap()).position.unwrap();
let mut said = None;
read_entries(&path, index.kind, &[position], &mut |_, contents| {
said = Some(contents.err().map(|e| e.to_string()));
Next::Go
})
.unwrap();
let said = said.flatten().unwrap_or_default();
assert!(said.starts_with("Encrypted"), "{said}");
let hidden = seven_zip_made(&dir, "hidden.7z");
let said = super::index(&hidden).unwrap_err().to_string();
assert!(said.starts_with("Encrypted"), "{said}");
std::fs::remove_dir_all(&dir).unwrap();
}