#![cfg(unix)]
use super::write_files_report;
use crate::core::backend::GeneratedFile;
use crate::core::config::Language;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
const EMITTED: &str = "packages/node/index.ts";
const CONTENT: &str = "export const generated = true;\n";
fn emitted_files() -> Vec<(Language, Vec<GeneratedFile>)> {
vec![(
Language::Node,
vec![GeneratedFile {
path: EMITTED.into(),
content: CONTENT.into(),
generated_header: true,
}],
)]
}
fn symlink(target: &Path, link: &Path) {
std::os::unix::fs::symlink(target, link).expect("symlink");
}
#[test]
fn symlinked_ancestor_directory_is_refused_before_the_write_escapes_the_project() {
let temporary = tempfile::tempdir().expect("temporary directory");
let base = temporary.path().join("base");
let outside = temporary.path().join("outside");
std::fs::create_dir_all(base.join("packages")).expect("packages directory");
std::fs::create_dir(&outside).expect("outside directory");
symlink(&outside, &base.join("packages/node"));
let error = write_files_report(&emitted_files(), &base).expect_err("symlinked ancestor must be refused");
assert!(error.to_string().contains("not contained"), "{error}");
assert!(
!outside.join("index.ts").exists(),
"the emitted file reached {} through a symlinked ancestor",
outside.display()
);
let residue: Vec<_> = std::fs::read_dir(&outside)
.expect("read outside directory")
.map(|entry| entry.expect("directory entry").path())
.collect();
assert!(residue.is_empty(), "write left {residue:?} outside the project");
}
#[test]
fn symlinked_leaf_pointing_outside_the_project_is_refused() {
let temporary = tempfile::tempdir().expect("temporary directory");
let base = temporary.path().join("base");
let outside = temporary.path().join("outside");
std::fs::create_dir_all(base.join("packages/node")).expect("node directory");
std::fs::create_dir(&outside).expect("outside directory");
std::fs::write(outside.join("index.ts"), "// foreign\n").expect("outside file");
symlink(&outside.join("index.ts"), &base.join(EMITTED));
let error = write_files_report(&emitted_files(), &base).expect_err("symlinked leaf must be refused");
assert!(error.to_string().contains("not contained"), "{error}");
assert_eq!(
std::fs::read_to_string(outside.join("index.ts")).expect("outside file"),
"// foreign\n"
);
}
#[test]
fn ordinary_existing_output_directory_still_accepts_writes() {
let temporary = tempfile::tempdir().expect("temporary directory");
let base = temporary.path().join("base");
std::fs::create_dir_all(base.join("packages/node")).expect("node directory");
write_files_report(&emitted_files(), &base).expect("ordinary existing directory must still be written to");
let written = std::fs::read_to_string(base.join(EMITTED)).expect("generated file");
assert!(written.contains("export const generated = true;"), "{written}");
}
#[test]
fn symlinked_ancestor_that_stays_inside_the_project_is_accepted() {
let temporary = tempfile::tempdir().expect("temporary directory");
let base = temporary.path().join("base");
let real = base.join("real-node");
std::fs::create_dir_all(&real).expect("real node directory");
std::fs::create_dir_all(base.join("packages")).expect("packages directory");
symlink(&real, &base.join("packages/node"));
write_files_report(&emitted_files(), &base).expect("a symlink that stays inside the project is not an escape");
let written = std::fs::read_to_string(real.join("index.ts")).expect("generated file");
assert!(written.contains("export const generated = true;"), "{written}");
}
#[test]
fn base_directory_reached_through_a_symlink_is_not_an_escape() {
let temporary = tempfile::tempdir().expect("temporary directory");
let real_root = temporary.path().join("real-root");
std::fs::create_dir_all(real_root.join("base/packages/node")).expect("node directory");
let linked_root = temporary.path().join("linked-root");
symlink(&real_root, &linked_root);
let base = linked_root.join("base");
write_files_report(&emitted_files(), &base).expect("a symlinked base_dir must not be treated as an escape");
let written = std::fs::read_to_string(real_root.join("base").join(EMITTED)).expect("generated file");
assert!(written.contains("export const generated = true;"), "{written}");
}
#[test]
fn inaccessible_existing_ancestor_fails_closed() {
let temporary = tempfile::tempdir().expect("temporary directory");
let base = temporary.path().join("base");
let blocked = base.join("blocked");
std::fs::create_dir_all(&blocked).expect("blocked directory");
std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o000)).expect("remove permissions");
let result = super::contained_output_path(&base, Path::new("blocked/leaf.txt"));
std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o700)).expect("restore permissions");
let error = result.expect_err("an unreadable ancestor must not be treated as absent");
assert!(
error.to_string().contains("failed to inspect existing output path"),
"{error}"
);
}
#[test]
fn inaccessible_base_directory_fails_closed() {
let temporary = tempfile::tempdir().expect("temporary directory");
let blocked = temporary.path().join("blocked");
let base = blocked.join("base");
std::fs::create_dir_all(&base).expect("base directory");
std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o000)).expect("remove permissions");
let result = super::contained_output_path(&base, Path::new("leaf.txt"));
std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o700)).expect("restore permissions");
let error = result.expect_err("an inaccessible base must not be treated as absent");
assert!(error.to_string().contains("failed to resolve output base"), "{error}");
}