#![cfg(feature = "fs")]
mod support;
use std::io::Write;
use std::path::Path;
use support::container;
use slpc::{Destination, Error};
fn sandbox() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn entries(dir: &Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
}
#[test]
fn nothing_appears_until_it_is_committed() {
let s = sandbox();
let path = s.path().join("report.pdf.slpc");
let mut d = Destination::new(&path, false).unwrap();
d.writer()
.write_all(&container("report.pdf", b"x"))
.unwrap();
assert!(!path.exists(), "the destination appeared before the commit");
d.commit().unwrap();
assert!(path.exists());
assert_eq!(entries(s.path()), ["report.pdf.slpc"]);
}
#[test]
fn dropping_one_leaves_nothing_behind() {
let s = sandbox();
let mut d = Destination::new(s.path().join("c.slpc"), false).unwrap();
d.writer().write_all(b"half a container").unwrap();
drop(d);
assert!(entries(s.path()).is_empty(), "{:?}", entries(s.path()));
}
#[test]
fn refuses_an_existing_file_unless_forced() {
let s = sandbox();
let path = s.path().join("c.slpc");
std::fs::write(&path, b"already here").unwrap();
let e = Destination::new(&path, false).unwrap_err();
assert!(
matches!(&e, Error::Io(io) if io.kind() == std::io::ErrorKind::AlreadyExists),
"{e}"
);
assert!(e.to_string().contains("c.slpc"), "{e}");
assert_eq!(std::fs::read(&path).unwrap(), b"already here");
let mut d = Destination::new(&path, true).unwrap();
d.writer().write_all(b"replaced").unwrap();
d.commit().unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"replaced");
}
#[test]
fn refuses_a_directory_that_is_not_there_and_names_it() {
let s = sandbox();
let e = Destination::new(s.path().join("nowhere").join("c.slpc"), false).unwrap_err();
assert!(e.to_string().contains("nowhere"), "{e}");
}
#[test]
fn reads_back_what_was_written() {
let s = sandbox();
let bytes = container("report.pdf", b"%PDF");
let mut d = Destination::new(s.path().join("c.slpc"), false).unwrap();
d.writer().write_all(&bytes).unwrap();
assert!(slpc::validate(d.written().unwrap())
.unwrap()
.is_conformant());
d.commit().unwrap();
}
#[cfg(unix)]
mod permissions {
use super::{container, entries, sandbox, Destination};
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
fn mode(p: &Path) -> u32 {
std::fs::metadata(p).unwrap().permissions().mode() & 0o777
}
fn ordinary(dir: &Path) -> u32 {
let p = dir.join("ordinary.txt");
std::fs::write(&p, b"reference").unwrap();
let m = mode(&p);
std::fs::remove_file(&p).unwrap();
m
}
fn write_to(d: &mut Destination) {
d.writer().write_all(&container("a.txt", b"x")).unwrap();
}
#[test]
fn a_new_file_gets_what_the_umask_decided() {
let s = sandbox();
let want = ordinary(s.path());
let path = s.path().join("c.slpc");
let mut d = Destination::new(&path, false).unwrap();
write_to(&mut d);
d.commit().unwrap();
assert_eq!(mode(&path), want, "a renamed file must not come out 0600");
assert_eq!(entries(s.path()), ["c.slpc"], "the probe was left behind");
}
#[test]
fn forcing_over_a_file_still_gets_what_the_umask_decided() {
let s = sandbox();
let want = ordinary(s.path());
let path = s.path().join("c.slpc");
std::fs::write(&path, b"in the way").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
let mut d = Destination::new(&path, true).unwrap();
write_to(&mut d);
d.commit().unwrap();
assert_eq!(mode(&path), want);
}
#[test]
fn writing_in_place_keeps_the_files_own_permissions() {
let s = sandbox();
let path = s.path().join("c.slpc");
std::fs::write(&path, container("a.txt", b"x")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o640)).unwrap();
let mut d = Destination::in_place(&path).unwrap();
write_to(&mut d);
d.commit().unwrap();
assert_eq!(mode(&path), 0o640);
assert_eq!(entries(s.path()), ["c.slpc"], "in place needs no probe");
}
#[test]
fn writing_in_place_replaces_the_file_and_not_the_link_to_it() {
let s = sandbox();
let real = s.path().join("c.slpc");
let link = s.path().join("link.slpc");
std::fs::write(&real, container("a.txt", b"x")).unwrap();
std::os::unix::fs::symlink("c.slpc", &link).unwrap();
let mut d = Destination::in_place(&link).unwrap();
d.writer().write_all(&container("b.txt", b"y")).unwrap();
d.commit().unwrap();
assert!(link.is_symlink(), "the link was replaced by a file");
let c = slpc::Container::open(&real).unwrap();
assert_eq!(c.payload_name(), "b.txt");
}
#[test]
fn in_place_refuses_a_file_that_is_not_there() {
let s = sandbox();
assert!(Destination::in_place(s.path().join("absent.slpc")).is_err());
}
}
mod payload_paths {
use super::sandbox;
use slpc::{display_path, payload_path};
use std::path::Path;
#[cfg(windows)]
const DEVICE_NAMES: [&str; 8] = ["CON", "CON.txt", "con", "COM1", "AUX", "LPT1", "PRN", "NUL"];
#[test]
#[cfg(windows)]
fn a_name_windows_reads_as_a_device_still_lands_in_the_directory() {
for name in DEVICE_NAMES {
let dir = sandbox();
let bytes = format!("bytes for {name}").into_bytes();
let path = payload_path(dir.path(), name).expect("a path for the name");
std::fs::write(&path, &bytes).expect("writes the payload");
let listed: Vec<_> = std::fs::read_dir(dir.path())
.expect("the directory")
.map(|e| e.expect("an entry").file_name())
.collect();
assert!(
listed.iter().any(|entry| entry == name),
"{name}: nothing by that name is in the directory, so the payload went \
to a device rather than to a file. Listed: {listed:?}"
);
assert_eq!(
std::fs::read(&path).expect("reads the payload back"),
bytes,
"{name}: what came back is not what was written"
);
std::fs::remove_file(&path).expect("the payload is removable");
}
}
#[test]
fn an_ordinary_name_lands_in_the_directory_it_was_given() {
let dir = sandbox();
let path = payload_path(dir.path(), "report.pdf").expect("a path");
std::fs::write(&path, b"payload").expect("writes");
assert_eq!(std::fs::read(&path).expect("reads back"), b"payload");
assert_eq!(path.file_name().expect("a filename"), "report.pdf");
assert_eq!(
std::fs::canonicalize(&path).expect("the path resolves"),
std::fs::canonicalize(dir.path().join("report.pdf")).expect("so does the join"),
"the payload did not land in the directory it was given"
);
assert!(
!display_path(&path).contains(r"\\?\"),
"what a person is shown carries the verbatim prefix"
);
}
#[test]
#[cfg(windows)]
fn a_directory_that_is_not_there_is_an_error_that_names_it() {
let dir = sandbox();
let missing = dir.path().join("no-such-directory");
let said = payload_path(&missing, "report.pdf")
.expect_err("a directory that is not there is an error")
.to_string();
assert!(
said.contains("no-such-directory"),
"the error does not name the directory: {said}"
);
assert!(
!said.contains(r"\\?\"),
"the error shows the verbatim form: {said}"
);
}
#[test]
fn the_verbatim_prefix_is_not_shown_to_a_person() {
assert_eq!(
display_path(Path::new(r"\\?\C:\Users\a\report.pdf")),
r"C:\Users\a\report.pdf"
);
}
#[test]
fn a_verbatim_network_path_keeps_its_two_separators() {
assert_eq!(
display_path(Path::new(r"\\?\UNC\server\share\report.pdf")),
r"\\server\share\report.pdf"
);
}
#[test]
fn a_path_that_never_had_the_prefix_is_untouched() {
for ordinary in [r"C:\Users\a\report.pdf", "/home/a/report.pdf", "report.pdf"] {
assert_eq!(display_path(Path::new(ordinary)), ordinary);
}
}
}
#[test]
#[cfg(windows)]
fn a_refusal_names_the_file_the_way_a_person_wrote_it() {
let dir = sandbox();
let path = slpc::payload_path(dir.path(), "report.pdf").expect("a path");
std::fs::write(&path, b"already here").expect("the file in the way");
let refusal = Destination::new(&path, false).expect_err("must refuse");
let said = refusal.to_string();
assert!(
!said.contains(r"\\?\"),
"the refusal shows the verbatim form: {said}"
);
assert!(
said.contains("report.pdf"),
"the refusal does not name the file: {said}"
);
}
#[test]
fn a_file_that_arrives_after_the_check_is_still_not_replaced() {
let s = sandbox();
let path = s.path().join("c.slpc");
let mut d = Destination::new(&path, false).unwrap();
d.writer().write_all(b"mine").unwrap();
std::fs::write(&path, b"theirs").unwrap();
let e = d.commit().unwrap_err();
assert!(
matches!(&e, Error::Io(io) if io.kind() == std::io::ErrorKind::AlreadyExists),
"{e}"
);
assert_eq!(
std::fs::read(&path).unwrap(),
b"theirs",
"the file that was there first is the one still there"
);
}
#[test]
#[cfg(unix)]
fn a_setuid_payload_does_not_extract_as_setuid() {
use std::os::unix::fs::PermissionsExt as _;
let archive = support::raw_zip(&[
support::Member::new(slpc::METADATA_MEMBER, support::metadata("tool").as_bytes()),
support::Member::new("tool", b"\x7fELF payload\n").with_mode(0o104_755),
]);
let s = sandbox();
let mut c = slpc::Container::read(std::io::Cursor::new(archive)).unwrap();
assert_eq!(
c.payload_mode().unwrap(),
Some(0o4755),
"the fixture records it"
);
let out = s.path().join(c.payload_name());
let mut d = Destination::new(&out, false).unwrap();
std::io::copy(&mut c.payload().unwrap(), d.writer()).unwrap();
d.commit().unwrap();
let landed = std::fs::metadata(&out).unwrap().permissions().mode() & 0o7777;
assert_eq!(
landed & 0o4000,
0,
"setuid survived extraction: {landed:04o}"
);
assert_eq!(
landed & 0o111,
0,
"an execute bit survived extraction: {landed:04o}"
);
let ordinary = s.path().join("ordinary");
std::fs::write(&ordinary, b"").unwrap();
assert_eq!(
landed,
std::fs::metadata(&ordinary).unwrap().permissions().mode() & 0o7777
);
}