use std::fs;
#[cfg(unix)]
use super::open_regular_file_allow_hard_links;
use super::{open_regular_file, path_exists, promote, validate_regular_file, validate_workspace};
const ENTITIES: [&str; 2] = ["controller", "online migration"];
#[test]
fn a_workspace_must_be_a_real_directory_and_says_so_under_each_stores_name() {
let root = tempfile::tempdir().expect("root");
let file = root.path().join("file");
fs::write(&file, b"x").expect("file");
for entity in ENTITIES {
validate_workspace(root.path(), entity).expect("a real directory");
let refused = validate_workspace(&file, entity).expect_err("a file is no workspace");
assert!(
refused
.to_string()
.contains(&format!("{entity} workspace must be a real directory")),
"{refused}"
);
let missing = validate_workspace(&root.path().join("absent"), entity).expect_err("absent");
assert!(
missing
.to_string()
.contains(&format!("cannot inspect {entity} workspace: ")),
"{missing}"
);
}
}
#[test]
fn a_file_must_be_a_regular_file_and_says_so_under_each_stores_name() {
let root = tempfile::tempdir().expect("root");
let file = root.path().join("file");
fs::write(&file, b"x").expect("file");
for entity in ENTITIES {
validate_regular_file(&file, entity).expect("a regular file");
let refused = validate_regular_file(root.path(), entity).expect_err("a directory");
assert!(
refused
.to_string()
.contains(&format!("{entity} path must be a regular file")),
"{refused}"
);
let missing =
validate_regular_file(&root.path().join("absent"), entity).expect_err("absent");
assert!(
missing
.to_string()
.contains(&format!("cannot inspect {entity} file: ")),
"{missing}"
);
}
}
#[cfg(unix)]
#[test]
fn a_symlink_is_refused_as_a_workspace_and_as_a_file() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().expect("root");
let real_dir = root.path().join("real");
fs::create_dir(&real_dir).expect("dir");
let victim = root.path().join("victim");
fs::write(&victim, b"untouched").expect("victim");
let dir_link = root.path().join("dir-link");
let file_link = root.path().join("file-link");
symlink(&real_dir, &dir_link).expect("dir link");
symlink(&victim, &file_link).expect("file link");
for entity in ENTITIES {
let workspace = validate_workspace(&dir_link, entity).expect_err("linked workspace");
assert!(
workspace
.to_string()
.contains(&format!("{entity} workspace must be a real directory")),
"{workspace}"
);
let file = validate_regular_file(&file_link, entity).expect_err("linked file");
assert!(
file.to_string()
.contains(&format!("{entity} path must be a regular file")),
"{file}"
);
}
assert_eq!(fs::read(&victim).expect("victim"), b"untouched");
}
#[test]
fn open_regular_file_reads_and_writes_a_real_file() {
let root = tempfile::tempdir().expect("root");
let path = root.path().join("state");
fs::write(&path, b"first").expect("seed");
let mut options = fs::OpenOptions::new();
options.read(true).write(true);
let mut file = open_regular_file(&path, "test", options).expect("a regular file opens");
let mut read = Vec::new();
std::io::Read::read_to_end(&mut file, &mut read).expect("read");
assert_eq!(read, b"first");
}
#[cfg(unix)]
#[test]
fn open_regular_file_refuses_a_symlink_and_says_so_under_each_stores_name() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().expect("root");
let victim = root.path().join("victim");
fs::write(&victim, b"untouched").expect("victim");
let link = root.path().join("link");
symlink(&victim, &link).expect("link");
for entity in ENTITIES {
let mut options = fs::OpenOptions::new();
options.read(true);
let refused = open_regular_file(&link, entity, options).expect_err("a symlink");
assert!(
refused
.to_string()
.contains(&format!("{entity} path must be a regular file")),
"{refused}"
);
}
assert_eq!(fs::read(&victim).expect("victim"), b"untouched");
}
#[cfg(unix)]
#[test]
fn open_regular_file_refuses_a_symlink_present_at_call_time_with_no_check_step_of_its_own() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().expect("root");
let victim = root.path().join("victim");
fs::write(&victim, b"untouched").expect("victim");
let path = root.path().join("state");
fs::write(&path, b"regular").expect("a real file first");
fs::remove_file(&path).expect("remove the regular file");
symlink(&victim, &path).expect("swap in a link to the victim");
let mut options = fs::OpenOptions::new();
options.read(true);
let refused = open_regular_file(&path, "test", options).expect_err("swapped-in symlink");
assert!(
refused
.to_string()
.contains("test path must be a regular file"),
"{refused}"
);
assert_eq!(fs::read(&victim).expect("victim"), b"untouched");
}
#[cfg(unix)]
#[test]
fn open_regular_file_refuses_a_fifo_without_blocking() {
let root = tempfile::tempdir().expect("root");
let fifo = root.path().join("pipe");
let status = std::process::Command::new("mkfifo")
.arg(&fifo)
.status()
.expect("test: run mkfifo");
assert!(status.success(), "mkfifo failed: {status}");
let (tx, rx) = std::sync::mpsc::channel();
let path = fifo.clone();
std::thread::spawn(move || {
let mut options = fs::OpenOptions::new();
options.read(true);
let result = open_regular_file(&path, "test", options).map(|_| ());
let _ = tx.send(result.map_err(|err| err.to_string()));
});
let refused = rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("open_regular_file must return instead of blocking on the FIFO")
.expect_err("a FIFO is not a regular file");
assert!(
refused.contains("test path must be a regular file"),
"{refused}"
);
}
#[cfg(unix)]
#[test]
fn open_regular_file_refuses_a_hard_link() {
let root = tempfile::tempdir().expect("root");
let victim = root.path().join("victim");
fs::write(&victim, b"untouched").expect("victim");
let path = root.path().join("state");
fs::hard_link(&victim, &path).expect("hard link");
let mut options = fs::OpenOptions::new();
options.read(true);
let refused = open_regular_file(&path, "test", options).expect_err("a hard link");
assert!(
refused
.to_string()
.contains("test path must be a regular file"),
"{refused}"
);
assert_eq!(fs::read(&victim).expect("victim"), b"untouched");
}
#[cfg(unix)]
#[test]
fn open_regular_file_never_refuses_a_legitimate_rename_replace_race() {
use std::sync::atomic::{AtomicUsize, Ordering};
const ITERATIONS: usize = 20_000;
let root = tempfile::tempdir().expect("root");
let path = root.path().join("state");
let staging = root.path().join("state.tmp");
fs::write(&path, b"v0").expect("seed");
let spurious_refusals = AtomicUsize::new(0);
std::thread::scope(|scope| {
scope.spawn(|| {
for i in 0..ITERATIONS {
fs::write(&staging, format!("v{i}").as_bytes()).expect("write staging");
promote(&staging, &path).expect("promote");
}
});
for _ in 0..ITERATIONS {
let mut options = fs::OpenOptions::new();
options.read(true);
if open_regular_file(&path, "test", options).is_err() {
spurious_refusals.fetch_add(1, Ordering::Relaxed);
}
}
});
assert_eq!(
spurious_refusals.load(Ordering::Relaxed),
0,
"a reader racing a legitimate rename-replace must never be refused as a hard link"
);
}
#[cfg(unix)]
#[test]
fn open_regular_file_allow_hard_links_accepts_a_hard_link_but_still_refuses_a_symlink() {
let root = tempfile::tempdir().expect("root");
let victim = root.path().join("victim");
fs::write(&victim, b"content").expect("victim");
let aliased = root.path().join("aliased");
fs::hard_link(&victim, &aliased).expect("hard link");
let mut options = fs::OpenOptions::new();
options.read(true);
open_regular_file_allow_hard_links(&aliased, "test", options)
.expect("a hard link must be accepted by the hard-link-tolerant open");
let outside = tempfile::tempdir().expect("outside");
let secret = outside.path().join("secret");
fs::write(&secret, b"do-not-read-me").expect("secret");
let linked = root.path().join("state");
std::os::unix::fs::symlink(&secret, &linked).expect("symlink");
let mut options = fs::OpenOptions::new();
options.read(true);
let refused = open_regular_file_allow_hard_links(&linked, "test", options)
.expect_err("a symlink must still be refused");
assert!(
refused
.to_string()
.contains("test path must be a regular file"),
"{refused}"
);
}
#[cfg(unix)]
#[test]
fn open_regular_file_refuses_a_socket_with_the_clean_message() {
use std::os::unix::net::UnixListener;
let root = tempfile::tempdir().expect("root");
let path = root.path().join("state");
let _listener = UnixListener::bind(&path).expect("bind a unix socket");
let mut options = fs::OpenOptions::new();
options.read(true);
let refused = open_regular_file(&path, "test", options).expect_err("a socket");
assert!(
refused
.to_string()
.contains("test path must be a regular file"),
"{refused}"
);
}
#[cfg(unix)]
#[test]
fn open_regular_file_keeps_the_raw_message_for_a_permission_denied_regular_file() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("root");
let path = root.path().join("state");
fs::write(&path, b"regular").expect("a real file");
fs::set_permissions(&path, fs::Permissions::from_mode(0o000)).expect("chmod 000");
if fs::File::open(&path).is_ok() {
eprintln!("test: skipped, permission bits are not enforced here");
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore");
return;
}
let mut options = fs::OpenOptions::new();
options.read(true);
let refused = open_regular_file(&path, "test", options).expect_err("permission denied");
let message = refused.to_string();
assert!(
message.contains("cannot open test file"),
"a permission error must keep the raw message, got: {message}"
);
assert!(
!message.contains("must be a regular file"),
"a real regular file must never be relabeled as the wrong kind, got: {message}"
);
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore permissions");
}
#[test]
fn path_exists_tells_an_absent_path_from_a_present_one() {
let root = tempfile::tempdir().expect("root");
assert!(!path_exists(&root.path().join("absent")).expect("absent"));
assert!(path_exists(root.path()).expect("present"));
}
#[cfg(unix)]
#[test]
fn path_exists_does_not_follow_a_link_and_reports_any_other_error() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().expect("root");
let dangling = root.path().join("dangling");
symlink(root.path().join("gone"), &dangling).expect("dangling link");
assert!(path_exists(&dangling).expect("a dangling link exists"));
let file = root.path().join("file");
fs::write(&file, b"x").expect("file");
let error = path_exists(&file.join("child")).expect_err("below a file");
assert!(error.to_string().contains("cannot inspect "), "{error}");
}
#[test]
fn promote_replaces_the_target_whole_and_consumes_the_staging_file() {
let root = tempfile::tempdir().expect("root");
let staging = root.path().join("state.json.tmp");
let target = root.path().join("state.json");
fs::write(&staging, b"first").expect("staging");
promote(&staging, &target).expect("publish over nothing");
assert_eq!(fs::read(&target).expect("target"), b"first");
assert!(!staging.exists());
fs::write(&staging, b"second").expect("staging");
promote(&staging, &target).expect("publish over a file");
assert_eq!(fs::read(&target).expect("target"), b"second");
assert!(!staging.exists());
}
#[cfg(unix)]
#[test]
fn sync_directory_fails_on_an_absent_directory() {
use super::sync_directory;
let root = tempfile::tempdir().expect("root");
sync_directory(root.path()).expect("sync a directory");
sync_directory(&root.path().join("absent")).expect_err("an absent directory");
}