use super::*;
use crate::archive::fixtures::scratch;
struct Asking {
reply: Reply,
failures: Vec<PathBuf>,
files_left: Option<usize>,
}
impl Asking {
fn new(reply: Reply) -> Self {
Asking { reply, failures: Vec::new(), files_left: None }
}
}
impl Progress for Asking {
fn step(&mut self, step: Step<'_>) -> bool {
if let (Step::Starting(_), Some(left)) = (step, &mut self.files_left) {
if *left == 0 {
return false;
}
*left -= 1;
}
true
}
fn failed(&mut self, path: &Path, _error: &Error) -> Reply {
self.failures.push(path.to_path_buf());
self.reply
}
}
fn pack_into(sources: &[(PathBuf, bool)], dest: &Path, asking: &mut Asking) -> Result<Transfer, Error> {
let format = PackFormat::of(dest).expect("named as an archive");
let Some(plan) = plan_pack(sources, dest, format, asking)? else {
return Ok(Transfer::Cancelled);
};
pack(&plan, dest, format, false, asking)
}
fn contents(archive: &Path) -> Vec<(String, String)> {
use crate::archive::{EntryKind, Next};
let index = crate::archive::index(archive).unwrap();
let mut found = Vec::new();
let mut reading = Vec::new();
for at in index.subtree(Path::new("")) {
let entry = index.entry(at);
let path = entry.path.to_string_lossy().replace('\\', "/");
match (entry.kind, &entry.link) {
(EntryKind::Dir, _) => found.push((path, "/".to_string())),
(EntryKind::Link, Some(target)) => found.push((path, format!("-> {}", target.display()))),
_ => reading.push((entry.position.unwrap(), path, entry.kind)),
}
}
let positions: Vec<usize> = reading.iter().map(|(position, _, _)| *position).collect();
crate::archive::read_entries(archive, index.kind, &positions, &mut |position, read| {
let (_, path, kind) = reading.iter().find(|(at, _, _)| *at == position).unwrap();
let mut text = String::new();
read.unwrap().read_to_string(&mut text).unwrap();
found.push((path.clone(), if *kind == EntryKind::Link { format!("-> {text}") } else { text }));
Next::Go
})
.unwrap();
found.sort();
found
}
fn tree(dir: &Path) -> Vec<(PathBuf, bool)> {
let docs = dir.join("docs");
fs::create_dir_all(docs.join("deep")).unwrap();
fs::write(docs.join("notes.txt"), "notes").unwrap();
fs::write(docs.join("empty.txt"), "").unwrap();
fs::write(docs.join("deep/more.txt"), "more!").unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink("notes.txt", docs.join("link")).unwrap();
fs::write(dir.join("top.txt"), "top").unwrap();
vec![(docs, true), (dir.join("top.txt"), false)]
}
#[test]
fn every_format_packs_what_it_is_given() {
let dir = scratch("pack-formats");
let sources = tree(&dir.join("in"));
for format in PackFormat::ALL {
let dest = dir.join(format!("out{}", format.extension()));
let done = pack_into(&sources, &dest, &mut Asking::new(Reply::Abort)).unwrap_or_else(|e| panic!("{format:?}: {e}"));
assert_eq!(done, Transfer::Done, "{format:?}");
let mut expected = vec![("docs", "/"), ("docs/deep", "/"), ("docs/deep/more.txt", "more!"), ("docs/empty.txt", ""), ("docs/notes.txt", "notes"), ("top.txt", "top")];
if cfg!(unix) {
expected.push(("docs/link", "-> notes.txt"));
}
let mut expected: Vec<(String, String)> = expected.into_iter().map(|(path, held)| (path.to_string(), held.to_string())).collect();
expected.sort();
assert_eq!(contents(&dest), expected, "{format:?}");
}
let left: Vec<String> = fs::read_dir(&dir).unwrap().map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()).filter(|name| name.contains(".fm84-")).collect();
assert!(left.is_empty(), "{left:?}");
fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn a_tar_keeps_modes_and_times() {
use std::os::unix::fs::PermissionsExt;
let dir = scratch("pack-modes");
let script = dir.join("run.sh");
fs::write(&script, "#!/bin/sh\n").unwrap();
fs::set_permissions(&script, fs::Permissions::from_mode(0o750)).unwrap();
let when = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_600_000_000);
File::options().write(true).open(&script).unwrap().set_modified(when).unwrap();
for name in ["out.tar.gz", "out.zip", "out.7z"] {
let dest = dir.join(name);
pack_into(&[(script.clone(), false)], &dest, &mut Asking::new(Reply::Abort)).unwrap();
let index = crate::archive::index(&dest).unwrap();
let entry = index.entry(index.find(Path::new("run.sh")).unwrap());
assert_eq!(entry.mode.map(|mode| mode & 0o777), Some(0o750), "{name}");
assert_eq!(entry.modified, Some(when), "{name}");
}
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn an_archive_inside_what_is_packed_leaves_itself_out() {
let dir = scratch("pack-inside");
fs::write(dir.join("a.txt"), "a").unwrap();
fs::write(dir.join("self.zip"), "an old one").unwrap();
let dest = dir.join("self.zip");
let format = PackFormat::Zip;
let mut asking = Asking::new(Reply::Abort);
let plan = plan_pack(&[(dir.clone(), true)], &dest, format, &mut asking).unwrap().unwrap();
assert_eq!(pack(&plan, &dest, format, true, &mut asking).unwrap(), Transfer::Done);
let name = dir.file_name().unwrap().to_string_lossy().into_owned();
assert_eq!(contents(&dest), [(name.clone(), "/".to_string()), (format!("{name}/a.txt"), "a".to_string())]);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_cancel_leaves_nothing() {
let dir = scratch("pack-cancel");
let sources = tree(&dir.join("in"));
for format in PackFormat::ALL {
let dest = dir.join(format!("out{}", format.extension()));
let mut asking = Asking::new(Reply::Abort);
asking.files_left = Some(1);
assert_eq!(pack_into(&sources, &dest, &mut asking).unwrap(), Transfer::Cancelled, "{format:?}");
assert!(!dest.exists(), "{format:?}");
}
let left: Vec<_> = fs::read_dir(&dir).unwrap().map(|entry| entry.unwrap().file_name()).collect();
assert_eq!(left, ["in"]);
fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn an_unreadable_file_is_asked_about_and_can_be_left_out() {
use std::os::unix::fs::PermissionsExt;
let dir = scratch("pack-unreadable");
fs::write(dir.join("fine.txt"), "fine").unwrap();
fs::write(dir.join("locked.txt"), "locked").unwrap();
fs::set_permissions(dir.join("locked.txt"), fs::Permissions::from_mode(0o000)).unwrap();
if File::open(dir.join("locked.txt")).is_ok() {
return fs::remove_dir_all(&dir).unwrap();
}
let sources = [(dir.join("fine.txt"), false), (dir.join("locked.txt"), false)];
for name in ["out.tar.gz", "out.zip", "out.7z"] {
let dest = dir.join(name);
let mut asking = Asking::new(Reply::Skip);
assert_eq!(pack_into(&sources, &dest, &mut asking).unwrap(), Transfer::Done, "{name}");
assert_eq!(asking.failures, [dir.join("locked.txt")], "{name}");
assert_eq!(contents(&dest), [("fine.txt".to_string(), "fine".to_string())], "{name}");
}
fs::set_permissions(dir.join("locked.txt"), fs::Permissions::from_mode(0o600)).unwrap();
fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn a_name_that_is_not_utf8_goes_only_into_a_tar() {
use std::os::unix::ffi::OsStrExt;
let dir = scratch("pack-latin1");
let odd = dir.join(std::ffi::OsStr::from_bytes(b"caf\xe9.txt"));
fs::write(&odd, "x").unwrap();
let mut asking = Asking::new(Reply::Skip);
pack_into(&[(odd.clone(), false)], &dir.join("out.tar"), &mut asking).unwrap();
assert!(asking.failures.is_empty());
let index = crate::archive::index(&dir.join("out.tar")).unwrap();
assert_eq!(index.children(Path::new("")).next().unwrap().name().as_bytes(), b"caf\xe9.txt");
for name in ["out.zip", "out.7z"] {
let mut asking = Asking::new(Reply::Skip);
pack_into(&[(odd.clone(), false)], &dir.join(name), &mut asking).unwrap();
assert_eq!(asking.failures, std::slice::from_ref(&odd), "{name}");
}
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn an_archive_already_there_is_kept_unless_replacing() {
let dir = scratch("pack-taken");
fs::write(dir.join("a.txt"), "a").unwrap();
let dest = dir.join("out.zip");
fs::write(&dest, "somebody's").unwrap();
let err = pack_into(&[(dir.join("a.txt"), false)], &dest, &mut Asking::new(Reply::Abort)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::AlreadyExists);
assert_eq!(fs::read_to_string(&dest).unwrap(), "somebody's");
fs::remove_dir_all(&dir).unwrap();
}