#![cfg_attr(not(windows), forbid(unsafe_code))]
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use slipcase_open::{extract, session};
const LONG_ENOUGH: Duration = Duration::from_secs(3);
struct Alone {
dir: tempfile::TempDir,
}
impl Alone {
fn new() -> Self {
let dir = tempfile::tempdir().unwrap();
for sub in ["state", "run", "config", "bin"] {
std::fs::create_dir_all(dir.path().join(sub)).unwrap();
}
#[cfg(unix)]
an_inert_launcher(&dir.path().join("bin"));
Self { dir }
}
fn path(&self) -> &Path {
self.dir.path()
}
fn sessions(&self) -> PathBuf {
#[cfg(target_os = "macos")]
{
self.path()
.join("Library/Application Support/slipcase-open/sessions")
}
#[cfg(not(target_os = "macos"))]
{
self.path().join("state/slipcase-open/sessions")
}
}
fn run(&self, args: &[&str]) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_slipcase-open"));
command
.args(args)
.env("HOME", self.path())
.env("XDG_STATE_HOME", self.path().join("state"))
.env("XDG_RUNTIME_DIR", self.path().join("run"))
.env("XDG_CONFIG_HOME", self.path().join("config"))
.env("LOCALAPPDATA", self.path().join("state"))
.env("DBUS_SESSION_BUS_ADDRESS", "unix:path=/nonexistent/no-bus")
.env("PATH", with_this_world_first(&self.path().join("bin")))
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command
}
fn container(&self, name: &str, payload: &[u8]) -> PathBuf {
let doc: slpc::toml_edit::DocumentMut =
format!("slipcase_version = \"1.0\"\n\n[payload]\nfile = \"{name}\"\n")
.parse()
.unwrap();
let path = self.path().join(format!("{name}.slpc"));
slpc::pack_reader(name, payload, doc, std::fs::File::create(&path).unwrap()).unwrap();
path
}
fn a_crashed_session(&self, container: &Path, name: &str, edit: &[u8]) {
let mut left = session::create(&self.sessions(), container, name).unwrap();
extract::extract(&mut slpc::Container::open(container).unwrap(), &mut left).unwrap();
std::fs::write(left.payload_path(), edit).unwrap();
}
fn a_diverged_session(&self, container: &Path, name: &str, edit: &[u8]) {
self.a_crashed_session(container, name, edit);
self.container(name, b"what somebody else put there in the meantime");
}
}
#[cfg(unix)]
fn an_inert_launcher(bin: &Path) {
use std::os::unix::fs::PermissionsExt;
let name = if cfg!(target_os = "macos") {
"open"
} else {
"xdg-open"
};
let stub = bin.join(name);
std::fs::write(&stub, "#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&stub, std::fs::Permissions::from_mode(0o755)).unwrap();
}
fn with_this_world_first(bin: &Path) -> std::ffi::OsString {
let rest = std::env::var_os("PATH").unwrap_or_default();
std::env::join_paths(std::iter::once(bin.to_path_buf()).chain(std::env::split_paths(&rest)))
.unwrap()
}
fn finished(child: &mut Child, within: Duration) -> bool {
let deadline = Instant::now() + within;
while Instant::now() < deadline {
if child.try_wait().unwrap().is_some() {
return true;
}
std::thread::sleep(Duration::from_millis(25));
}
false
}
#[test]
fn a_refusal_that_raised_a_question_stays_to_be_answered() {
let world = Alone::new();
let c = world.container("report.txt", b"first");
world.a_diverged_session(&c, "report.txt", b"an edit that never landed");
let mut child = world.run(&["open", c.to_str().unwrap()]).spawn().unwrap();
let left_early = finished(&mut child, LONG_ENOUGH);
let _ = child.kill();
let done = child.wait_with_output().unwrap();
assert!(
!left_early,
"it returned on the refusal, leaving the question with nobody behind it: {}",
String::from_utf8_lossy(&done.stderr)
);
let said = String::from_utf8_lossy(&done.stderr);
assert!(said.contains("left behind"), "{said}");
assert!(said.contains("--write-back"), "{said}");
}
#[test]
fn a_refusal_that_raised_nothing_returns() {
let world = Alone::new();
let c = world.container("inner.zip", b"not a document");
let mut child = world.run(&["open", c.to_str().unwrap()]).spawn().unwrap();
assert!(
finished(&mut child, Duration::from_secs(30)),
"a refusal holding nothing should return at once"
);
let done = child.wait_with_output().unwrap();
assert!(!done.status.success());
let said = String::from_utf8_lossy(&done.stderr);
assert!(said.contains("not in the allowed set"), "{said}");
assert!(
session::scan(&world.sessions()).map_or(true, |s| s.is_empty()),
"a refused open should leave no session"
);
}
#[test]
fn asking_what_is_open_with_nobody_running_reads_the_state_directory() {
let world = Alone::new();
let done = world.run(&["sessions"]).output().unwrap();
assert!(done.status.success());
assert!(String::from_utf8_lossy(&done.stdout).contains("No sessions."));
let c = world.container("report.txt", b"first");
world.a_crashed_session(&c, "report.txt", b"edited");
let done = world.run(&["sessions"]).output().unwrap();
let said = String::from_utf8_lossy(&done.stdout);
assert!(said.contains("report.txt"), "{said}");
assert!(said.contains("--write-back"), "{said}");
}
#[test]
fn an_edit_left_by_a_crash_goes_back_when_the_container_is_opened() {
let world = Alone::new();
let c = world.container("report.txt", b"first");
world.a_crashed_session(&c, "report.txt", b"the edit that never landed");
let mut child = world.run(&["open", c.to_str().unwrap()]).spawn().unwrap();
let returned = finished(&mut child, LONG_ENOUGH);
let _ = child.kill();
let done = child.wait_with_output().unwrap();
let said = String::from_utf8_lossy(&done.stderr);
assert!(
!said.contains("left behind"),
"the person was asked about their own save: {said}"
);
assert!(!returned || said.contains("is open"), "{said}");
let mut held = slpc::Container::open(&c).unwrap();
let mut bytes = Vec::new();
std::io::Read::read_to_end(&mut held.payload().unwrap(), &mut bytes).unwrap();
assert_eq!(
bytes, b"the edit that never landed",
"the edit did not reach the container"
);
}
#[cfg(unix)]
#[test]
fn the_settings_verb_names_the_files_this_world_would_read() {
let world = Alone::new();
let done = world.run(&["policy"]).output().unwrap();
let said = String::from_utf8_lossy(&done.stdout);
assert!(done.status.success(), "{said}");
assert!(said.contains("/etc/slipcase/open.toml"), "{said}");
assert!(
said.contains(
world
.path()
.join("config/slipcase-open/policy.toml")
.to_str()
.unwrap()
),
"{said}"
);
assert!(said.contains(world.sessions().to_str().unwrap()), "{said}");
assert!(
said.contains(
world
.path()
.join("run/slipcase-open/front-door")
.to_str()
.unwrap()
),
"{said}"
);
assert!(said.contains("not there"), "{said}");
}
#[cfg(unix)]
#[test]
fn a_settings_file_that_will_not_parse_is_named_before_it_is_refused() {
let world = Alone::new();
let config = world.path().join("config/slipcase-open");
std::fs::create_dir_all(&config).unwrap();
std::fs::write(config.join("policy.toml"), "denied = [\"exe\"\n").unwrap();
let done = world.run(&["policy"]).output().unwrap();
let printed = String::from_utf8_lossy(&done.stdout);
let complained = String::from_utf8_lossy(&done.stderr);
assert!(!done.status.success(), "{printed}{complained}");
assert!(
printed.contains(config.join("policy.toml").to_str().unwrap()),
"the broken file was not named in the listing: {printed}"
);
assert!(printed.contains("cannot be read"), "{printed}");
assert!(complained.contains("unclosed array"), "{complained}");
}
#[test]
fn a_lone_path_is_an_open() {
let world = Alone::new();
let c = world.container("inner.zip", b"not a document");
let bare = world.run(&[c.to_str().unwrap()]).output().unwrap();
let spelled = world.run(&["open", c.to_str().unwrap()]).output().unwrap();
let said = String::from_utf8_lossy(&bare.stderr).to_string();
assert!(!bare.status.success(), "{said}");
assert!(said.contains("not in the allowed set"), "{said}");
assert_eq!(bare.status.code(), spelled.status.code());
assert_eq!(said, String::from_utf8_lossy(&spelled.stderr));
}