pub const RISK_SAFE: &str = "safe";
pub const RISK_DESTRUCTIVE: &str = "destructive";
pub const VALID_RISK_LEVELS: &[&str] = &[RISK_SAFE, RISK_DESTRUCTIVE];
pub const CONFIRM_APPROVED_LABEL: &str = "approved";
pub const CONFIRM_DENIED_LABEL: &str = "denied";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgvToken {
Literal(String),
Placeholder(String),
Partial(String),
}
fn is_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn classify_argv_token(tok: &str) -> ArgvToken {
if let Some(inner) = tok.strip_prefix("${").and_then(|s| s.strip_suffix('}')) {
if !inner.contains("${") && is_ident(inner) {
return ArgvToken::Placeholder(inner.to_string());
}
return ArgvToken::Partial(tok.to_string());
}
if tok.contains("${") {
return ArgvToken::Partial(tok.to_string());
}
ArgvToken::Literal(tok.to_string())
}
pub fn classify_argv(argv: &[String]) -> Vec<ArgvToken> {
argv.iter().map(|t| classify_argv_token(t)).collect()
}
pub fn argv_canonical_bytes(argv: &[String]) -> Vec<u8> {
let mut out = Vec::new();
for (i, el) in argv.iter().enumerate() {
if i > 0 {
out.push(0u8);
}
out.extend_from_slice(el.as_bytes());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whole_placeholder_binds() {
assert_eq!(
classify_argv_token("${host}"),
ArgvToken::Placeholder("host".into())
);
assert_eq!(
classify_argv_token("${count_2}"),
ArgvToken::Placeholder("count_2".into())
);
}
#[test]
fn literals_pass_through() {
assert_eq!(classify_argv_token("ping"), ArgvToken::Literal("ping".into()));
assert_eq!(classify_argv_token("-c"), ArgvToken::Literal("-c".into()));
assert_eq!(classify_argv_token("$HOME"), ArgvToken::Literal("$HOME".into()));
}
#[test]
fn partial_tokens_are_rejected() {
for bad in ["${host}.txt", "pre${x}", "${a}${b}", "${}", "${1bad}", "x${y}z"] {
assert!(
matches!(classify_argv_token(bad), ArgvToken::Partial(_)),
"expected Partial for {bad:?}"
);
}
}
#[test]
fn canonical_bytes_separate_elements() {
let a = argv_canonical_bytes(&["a".into(), "b".into()]);
let b = argv_canonical_bytes(&["ab".into()]);
assert_ne!(a, b, "element boundary must be forgery-proof");
}
}