use std::ffi::OsStr;
use std::path::{Component, Path};
use super::read_only_programs::Arg;
use crate::commands::pm_guard_write_boundary::{canonical_existing_ancestor, scratchpad_root};
fn allowed_flags(program: &str) -> &'static str {
match program {
"cp" => "rRpfvna",
_ => "rRfvd",
}
}
pub(super) fn scratchpad_write(
program: &str,
rest: &[Arg],
session: Option<&str>,
) -> Result<(), String> {
let Some(session) = session else {
return Err(format!(
"`{program}` with no session id in the hook payload to bind the scratchpad to"
));
};
let mut operands = 0;
let mut after_dashdash = false;
for arg in rest {
let Some(text) = arg.text() else {
return Err(format!("a `{program}` operand held in a variable"));
};
if !after_dashdash && text == "--" {
after_dashdash = true;
continue;
}
if !after_dashdash && text.starts_with('-') {
let cluster = &text[1..];
let ok =
!cluster.is_empty() && cluster.chars().all(|c| allowed_flags(program).contains(c));
if !ok {
return Err(format!("`{program} {text}`, a flag outside its short list"));
}
continue;
}
let path = Path::new(text);
if has_git_component(path) {
return Err(format!(
"`{program}` naming {text}, which has a `.git` component"
));
}
if !in_scratchpad(path, session) {
return Err(format!(
"`{program}` naming {text}, which is not a literal absolute path inside the \
session scratchpad, or resolves through a `.git` component"
));
}
operands += 1;
}
let needed = if program == "cp" { 2 } else { 1 };
if operands < needed {
return Err(format!("`{program}` without its operands"));
}
Ok(())
}
fn in_scratchpad(path: &Path, session: &str) -> bool {
let plain = path.is_absolute()
&& path
.components()
.all(|c| matches!(c, Component::RootDir | Component::Normal(_)));
if !plain {
return false;
}
let Some(root) = scratchpad_root(path).filter(|root| root.as_path() != path) else {
return false;
};
let Ok(real_root) = root.canonicalize() else {
return false;
};
if !is_session_root(&root, session)
|| !is_session_root(&real_root, session)
|| scratchpad_root(&real_root).as_deref() != Some(real_root.as_path())
{
return false;
}
let real = match path.symlink_metadata() {
Ok(_) => path
.canonicalize()
.ok()
.filter(|real| *real != real_root && real.starts_with(&real_root)),
Err(_) => canonical_existing_ancestor(path).filter(|real| real.starts_with(&real_root)),
};
real.is_some_and(|real| !has_git_component(&real))
}
fn has_git_component(path: &Path) -> bool {
path.components()
.any(|c| c.as_os_str().eq_ignore_ascii_case(".git"))
}
fn is_session_root(root: &Path, session: &str) -> bool {
root.parent().and_then(Path::file_name) == Some(OsStr::new(session))
}