refuse a diff that makes agents.md anything but a regular file
This commit is contained in:
parent
9add99838c
commit
e59c01059a
1 changed files with 69 additions and 0 deletions
|
|
@ -204,6 +204,13 @@ fn apply_diff(state_dir: &Path, apply: &Apply) -> ApplyReply {
|
||||||
if at != apply.base_revision {
|
if at != apply.base_revision {
|
||||||
return ApplyReply::Stale { revision: at };
|
return ApplyReply::Stale { revision: at };
|
||||||
}
|
}
|
||||||
|
if let Some(mode) = non_regular_mode(&apply.diff) {
|
||||||
|
return ApplyReply::Rejected {
|
||||||
|
error: format!(
|
||||||
|
"the diff sets file mode {mode}; AGENTS.md must stay a regular file (100644)"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
let work = match tempfile::tempdir() {
|
let work = match tempfile::tempdir() {
|
||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
Err(e) => return failed("creating a directory to apply in", &e),
|
Err(e) => return failed("creating a directory to apply in", &e),
|
||||||
|
|
@ -234,6 +241,12 @@ fn apply_diff(state_dir: &Path, apply: &Apply) -> ApplyReply {
|
||||||
error: format!("the diff must change AGENTS.md and nothing else; it left {left:?}"),
|
error: format!("the diff must change AGENTS.md and nothing else; it left {left:?}"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Belt and braces for the mode check above: never read through a link.
|
||||||
|
if !std::fs::symlink_metadata(©).is_ok_and(|m| m.file_type().is_file()) {
|
||||||
|
return ApplyReply::Rejected {
|
||||||
|
error: "the diff left AGENTS.md as something other than a regular file".to_owned(),
|
||||||
|
};
|
||||||
|
}
|
||||||
let edited = match std::fs::read(©) {
|
let edited = match std::fs::read(©) {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(e) => return failed("reading the applied AGENTS.md", &e),
|
Err(e) => return failed("reading the applied AGENTS.md", &e),
|
||||||
|
|
@ -248,8 +261,20 @@ fn apply_diff(state_dir: &Path, apply: &Apply) -> ApplyReply {
|
||||||
|
|
||||||
/// Run `git <args>` in `dir` with `diff` on stdin. The outer error is git not
|
/// Run `git <args>` in `dir` with `diff` on stdin. The outer error is git not
|
||||||
/// running; the inner one is git's stderr when it exits non-zero.
|
/// running; the inner one is git's stderr when it exits non-zero.
|
||||||
|
/// The first file mode a diff header sets that is not `100644`, if any.
|
||||||
|
fn non_regular_mode(diff: &str) -> Option<&str> {
|
||||||
|
diff.lines()
|
||||||
|
.filter_map(|l| {
|
||||||
|
l.strip_prefix("new file mode ")
|
||||||
|
.or_else(|| l.strip_prefix("new mode "))
|
||||||
|
})
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|m| *m != "100644")
|
||||||
|
}
|
||||||
|
|
||||||
fn git(dir: &Path, args: &[&str], diff: &str) -> std::io::Result<Result<(), String>> {
|
fn git(dir: &Path, args: &[&str], diff: &str) -> std::io::Result<Result<(), String>> {
|
||||||
let mut child = std::process::Command::new("git")
|
let mut child = std::process::Command::new("git")
|
||||||
|
.args(["-c", "core.symlinks=false"])
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(dir)
|
.current_dir(dir)
|
||||||
// Outside a repository `git apply` patches plain files, as `patch`
|
// Outside a repository `git apply` patches plain files, as `patch`
|
||||||
|
|
@ -396,6 +421,50 @@ mod tests {
|
||||||
assert_eq!(file(&dir), "# atlas\n");
|
assert_eq!(file(&dir), "# atlas\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A symlink diff must not make the harness read the link's target into
|
||||||
|
/// AGENTS.md, which the mirror then publishes.
|
||||||
|
#[test]
|
||||||
|
fn a_diff_that_creates_a_symlink_is_refused_and_nothing_is_read_through_it() {
|
||||||
|
let secret = tempfile::tempdir().expect("tempdir");
|
||||||
|
let target = secret.path().join("secret");
|
||||||
|
std::fs::write(&target, "do not publish\n").expect("write");
|
||||||
|
let link = format!(
|
||||||
|
"diff --git a/AGENTS.md b/AGENTS.md\nnew file mode 120000\n--- /dev/null\n+++ b/AGENTS.md\n@@ -0,0 +1 @@\n+{}\n\\ No newline at end of file\n",
|
||||||
|
target.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fresh create.
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let reply = apply_diff(dir.path(), &apply("", &link));
|
||||||
|
assert!(matches!(reply, ApplyReply::Rejected { .. }), "{reply:?}");
|
||||||
|
assert!(!dir.path().join("AGENTS.md").exists());
|
||||||
|
|
||||||
|
// Typechange of an existing file.
|
||||||
|
let dir = state(BEFORE);
|
||||||
|
let typechange = format!(
|
||||||
|
"diff --git a/AGENTS.md b/AGENTS.md\ndeleted file mode 100644\n--- a/AGENTS.md\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-# atlas\n-keep notes short\n{link}"
|
||||||
|
);
|
||||||
|
let reply = apply_diff(dir.path(), &apply(BEFORE, &typechange));
|
||||||
|
assert!(matches!(reply, ApplyReply::Rejected { .. }), "{reply:?}");
|
||||||
|
assert_eq!(file(&dir), BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mode check is the first layer; with it out of the way git must
|
||||||
|
/// still not write a link (`core.symlinks=false`) and the regular-file
|
||||||
|
/// check must hold.
|
||||||
|
#[test]
|
||||||
|
fn a_link_never_survives_the_apply_even_without_the_mode_check() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let work = tempfile::tempdir().expect("tempdir");
|
||||||
|
let diff = "diff --git a/AGENTS.md b/AGENTS.md\nnew file mode 120000\n--- /dev/null\n+++ b/AGENTS.md\n@@ -0,0 +1 @@\n+/etc/hostname\n\\ No newline at end of file\n";
|
||||||
|
super::git(work.path(), &["apply"], diff)
|
||||||
|
.expect("git runs")
|
||||||
|
.expect("applies");
|
||||||
|
let meta = std::fs::symlink_metadata(work.path().join("AGENTS.md")).expect("exists");
|
||||||
|
assert!(meta.file_type().is_file(), "git wrote a link");
|
||||||
|
drop(dir);
|
||||||
|
}
|
||||||
|
|
||||||
/// The next mirror write, or `None` after ten seconds without one.
|
/// The next mirror write, or `None` after ten seconds without one.
|
||||||
async fn next(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Mirror>) -> Option<Mirror> {
|
async fn next(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Mirror>) -> Option<Mirror> {
|
||||||
tokio::time::timeout(Duration::from_secs(10), rx.recv())
|
tokio::time::timeout(Duration::from_secs(10), rx.recv())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue