use std::os::unix::fs::{OpenOptionsExt as _, symlink};
use std::path::Path;
use super::super::Outcome;
use super::{Scratch, edit, list, perform, read, search};
fn refused(outcome: &Outcome) -> String {
assert!(outcome.is_error, "the action should not have been done: {}", outcome.text);
assert!(!outcome.text.is_empty(), "an error with no sentence in it");
outcome.text.clone()
}
#[test]
fn a_working_folder_that_is_deleted_while_the_app_is_open_refuses_every_path() {
let scratch = Scratch::new("hostile-deleted");
scratch.write("notes.txt", "still here\n");
let workspace = scratch.workspace();
let root = workspace.root().to_path_buf();
std::fs::remove_dir_all(&root).expect("the folder is gone");
for action in [read("notes.txt"), list(""), search("still", "")] {
let said = refused(&perform(&workspace, &action));
assert!(
said.contains("cannot be resolved")
|| said.contains("outside the working folder")
|| said.contains("does not exist"),
"the folder is not there any more, and the sentence says so: {said}"
);
}
assert!(!root.exists(), "nothing was made where the folder was");
let made = perform(&workspace, &edit("made.txt", "", "x"));
assert!(!made.is_error && made.text.contains("Created `made.txt`"), "{}", made.text);
assert_eq!(std::fs::read_to_string(root.join("made.txt")).expect("read"), "x", "and only inside it");
}
#[test]
fn a_working_folder_replaced_by_a_file_is_named_and_not_written_over() {
let scratch = Scratch::new("hostile-replaced");
let root = scratch.path().to_path_buf();
let workspace = scratch.workspace();
std::fs::remove_dir_all(&root).expect("the folder is gone");
std::fs::write(&root, "not a folder\n").expect("a file in its place");
let listed = perform(&workspace, &list(""));
assert!(refused(&listed).contains("use read_file"), "{}", listed.text);
let read = perform(&workspace, &read("notes.txt"));
assert!(refused(&read).contains("Not a directory"), "the file is in the way: {}", read.text);
refused(&perform(&workspace, &edit("notes.txt", "not a folder", "changed")));
assert_eq!(std::fs::read_to_string(&root).expect("read"), "not a folder\n", "the file is as it was");
std::fs::remove_file(&root).expect("the file goes too");
}
#[test]
fn a_path_of_three_thousand_characters_is_reported_and_nothing_is_created() {
let scratch = Scratch::new("hostile-long-path");
let workspace = scratch.workspace();
let long = "n".repeat(3000);
let deep: String = (0..100).map(|number| format!("d{number:02}/")).collect();
let said = refused(&perform(&workspace, &read(&long)));
assert!(said.contains("too long"), "a name that long is not one a file has: {said}");
let spread = format!("{deep}{long}");
let said = refused(&perform(&workspace, &read(&spread)));
assert!(said.contains("does not exist"), "{said}");
for path in [&long, &spread] {
refused(&perform(&workspace, &list(path)));
refused(&perform(&workspace, &search("x", path)));
refused(&perform(&workspace, &edit(path, "", "x")));
}
assert!(!scratch.path().join(&long).exists(), "no file of that name was made");
assert_eq!(std::fs::read_dir(scratch.path()).expect("read").count(), 0, "the folder is as it was");
}
#[test]
fn a_path_with_a_newline_or_a_nul_byte_in_it_is_reported_and_nothing_is_created() {
let scratch = Scratch::new("hostile-nul-path");
let workspace = scratch.workspace();
let newline = perform(&workspace, &read("two\nlines"));
assert!(refused(&newline).contains("does not exist"), "{}", newline.text);
let nul = perform(&workspace, &read("two\0lines"));
assert!(refused(&nul).contains("NUL"), "a NUL byte is not a name: {}", nul.text);
refused(&perform(&workspace, &edit("two\0lines", "", "x")));
refused(&perform(&workspace, &search("x", "two\0lines")));
assert_eq!(std::fs::read_dir(scratch.path()).expect("read").count(), 0, "nothing with such a name was made");
}
#[test]
fn a_link_that_leads_nowhere_or_never_ends_is_refused_with_its_own_words() {
let scratch = Scratch::new("hostile-links");
symlink(scratch.path().join("not-there.txt"), scratch.path().join("dangling")).unwrap();
symlink(scratch.path().join("b"), scratch.path().join("a")).unwrap();
symlink(scratch.path().join("a"), scratch.path().join("b")).unwrap();
let workspace = scratch.workspace();
for path in ["dangling", "a", "a/under", "b/deeper/still"] {
let said = refused(&perform(&workspace, &read(path)));
assert!(said.contains("leads nowhere"), "{path}: {said}");
}
assert!(refused(&perform(&workspace, &list("a"))).contains("leads nowhere"));
assert!(refused(&perform(&workspace, &search("x", "dangling"))).contains("leads nowhere"));
refused(&perform(&workspace, &edit("dangling", "", "x")));
assert!(!scratch.path().join("dangling").exists(), "the broken link is left as it is");
assert!(!scratch.path().join("a").exists(), "and the loop is not resolved into anything");
}
#[test]
fn a_link_to_the_root_of_the_file_system_is_refused() {
let scratch = Scratch::new("hostile-root-link");
symlink("/", scratch.path().join("up")).unwrap();
let workspace = scratch.workspace();
for action in [read("up"), read("up/etc/hostname"), list("up"), search("root", "up")] {
let said = refused(&perform(&workspace, &action));
assert!(said.contains("outside the working folder"), "{said}");
}
assert!(refused(&perform(&workspace, &edit("up/made-here", "", "x"))).contains("outside the working folder"));
assert!(!Path::new("/made-here").exists(), "nothing was made in the root of the file system");
}
#[test]
fn five_hundred_dots_going_up_are_refused_and_nothing_is_created_above() {
let scratch = Scratch::new("hostile-many-dots");
let workspace = scratch.workspace();
let up = "../".repeat(500);
for action in [read(&up), list(&up), search("x", &up)] {
let said = refused(&perform(&workspace, &action));
assert!(said.contains("outside the working folder"), "{said}");
}
let said = refused(&perform(&workspace, &edit(&format!("{up}made.txt"), "", "x")));
assert!(said.contains("outside the working folder"), "{said}");
assert!(!scratch.path().parent().expect("a parent").join("made.txt").exists());
assert_eq!(std::fs::read_dir(scratch.path()).expect("read").count(), 0, "the folder is as it was");
}
#[test]
fn a_file_that_goes_between_the_card_and_the_allow_is_not_written_over() {
let scratch = Scratch::new("hostile-vanished");
scratch.write("target.txt", "before\n");
let workspace = scratch.workspace();
assert!(workspace.preview(&edit("target.txt", "before", "after")).is_ok());
std::fs::remove_file(scratch.path().join("target.txt")).expect("the file is gone");
let said = refused(&perform(&workspace, &edit("target.txt", "before", "after")));
assert!(said.contains("does not exist"), "{said}");
assert!(!scratch.path().join("target.txt").exists(), "an allowed edit does not make the file again");
assert_eq!(std::fs::read_dir(scratch.path()).expect("read").count(), 0, "no temporary file was left");
}
fn within_a_while(workspace: &super::Workspace, action: super::super::Action) -> Option<Outcome> {
let workspace = workspace.clone();
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = sender.send(perform(&workspace, &action));
});
receiver.recv_timeout(std::time::Duration::from_secs(30)).ok()
}
#[test]
fn a_named_pipe_is_refused_at_once_and_nothing_waits_for_a_writer() {
let scratch = Scratch::new("hostile-pipe");
rustix::fs::mkfifoat(rustix::fs::CWD, scratch.path().join("pipe"), rustix::fs::Mode::from_raw_mode(0o600))
.expect("a named pipe");
let workspace = scratch.workspace();
let read_it = within_a_while(&workspace, read("pipe")).expect("reading a pipe ends");
let said = refused(&read_it);
assert!(said.contains("`pipe` is not a regular file"), "{said}");
let edit_it = within_a_while(&workspace, edit("pipe", "", "x")).expect("editing a pipe ends");
assert!(refused(&edit_it).contains("`pipe` is not a regular file"), "{}", edit_it.text);
let _ = std::fs::OpenOptions::new()
.write(true)
.custom_flags(rustix::fs::OFlags::NONBLOCK.bits().cast_signed())
.open(scratch.path().join("pipe"));
}
#[test]
fn a_file_larger_than_what_is_read_is_read_only_that_far_and_says_so() {
let scratch = Scratch::new("hostile-huge");
let line = format!("{}\n", "x".repeat(99));
let size = 20 * 1024 * 1024;
scratch.write("big.log", line.repeat(size / line.len()));
let workspace = scratch.workspace();
let head = perform(&workspace, &read("big.log"));
assert!(!head.is_error, "{}", head.text);
assert!(
head.text.contains("only its first 16 MB are read"),
"the model is told: {}",
&head.text[head.text.len() - 300..]
);
let past = perform(
&workspace,
&super::super::Action::Read {
path: "big.log".to_owned(),
lines: super::super::Lines { start: Some(200_000), end: Some(200_010) },
},
);
assert!(refused(&past).contains("only its first 16 MB are read"), "{}", past.text);
let change = perform(&workspace, &edit("big.log", &line, "y\n"));
assert!(refused(&change).contains("larger than the 16 MB"), "{}", change.text);
}