use super::*;
pub(crate) fn parse_segment(
segment: &str,
mask: &str,
bodies: &[String],
out: &mut Vec<BashMutation>,
) {
let toks = masked_tokens(segment, mask);
if toks.is_empty() {
return;
}
let redirect_consumed = collect_redirections(&toks, out);
collect_flag_outputs(&toks, out);
let cmd_all: Vec<&str> = toks
.iter()
.enumerate()
.filter(|(idx, t)| !is_fully_masked(t.masked) && !redirect_consumed.contains(idx))
.map(|(_, t)| t.orig)
.collect();
let cmd_tokens = strip_prefixes(&cmd_all);
let Some((&verb_tok, operands)) = cmd_tokens.split_first() else {
return;
};
match verb_tok {
"rm" => emit_operands(operands, "rm", out),
"mkdir" => emit_operands(operands, "mkdir", out),
"touch" => emit_touch(operands, out),
"tee" => emit_tee(operands, out),
"cp" => emit_copy_like(operands, "cp", out),
"mv" => emit_mv(operands, out),
"install" => emit_copy_like(operands, "install", out),
"ln" => emit_ln(operands, out),
"rsync" => emit_last_operand(operands, "rsync", out),
"sed" => emit_sed(operands, out),
"git" => emit_git(operands, out),
"curl" => emit_download_output(operands, "curl", out),
"wget" => emit_download_output(operands, "wget", out),
"dd" => emit_dd(operands, out),
"zip" => emit_zip(operands, out),
"tar" => {
emit_tar(operands, out); tar_extract_marker(operands, out); }
"perl" => emit_perl(operands, out),
other => {
if let Some(lang) = interp_lang(other) {
emit_interp(lang, operands, bodies, out);
} else {
emit_class(other, operands, out);
}
}
}
}
pub(crate) fn emit_perl(operands: &[&str], out: &mut Vec<BashMutation>) {
if !operands.iter().any(|t| is_perl_in_place_flag(t)) {
return;
}
const SCRIPT_FLAGS: &[&str] = &["-e", "-E"];
if !operands.iter().any(|t| SCRIPT_FLAGS.contains(t)) {
return;
}
let kept = strip_value_flags(strip_input_redirects(operands), SCRIPT_FLAGS);
for op in &kept {
if op.starts_with('-') {
continue; }
if let Some(path) = path_operand(op) {
out.push(BashMutation {
path,
verb: "perl-i",
cwd_at: CwdAt::Spawn,
});
}
}
}
pub(crate) fn is_perl_in_place_flag(tok: &str) -> bool {
let Some(body) = tok.strip_prefix('-') else {
return false;
};
if body.starts_with('-') {
return false;
}
let cluster = body.split('.').next().unwrap_or(body);
!cluster.is_empty()
&& cluster.contains('i')
&& cluster.chars().all(|c| c.is_ascii_alphanumeric())
}
pub(crate) fn strip_prefixes<'a>(tokens: &[&'a str]) -> Vec<&'a str> {
let mut idx = 0usize;
loop {
match tokens.get(idx) {
Some(&"sudo") => idx += 1,
Some(&"env") => {
idx += 1;
while matches!(tokens.get(idx), Some(t) if is_assignment(t)) {
idx += 1;
}
}
_ => break,
}
}
tokens[idx..].to_vec()
}
pub(crate) fn is_assignment(tok: &str) -> bool {
if tok.starts_with('-') {
return false;
}
match tok.split_once('=') {
Some((name, _)) => {
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')
}
None => false,
}
}
pub(crate) fn emit_operands(operands: &[&str], verb: &'static str, out: &mut Vec<BashMutation>) {
let kept = strip_input_redirects(operands);
for op in &kept {
if let Some(path) = path_operand(op) {
out.push(BashMutation {
path,
verb,
cwd_at: CwdAt::Spawn,
});
}
}
}
pub(crate) fn emit_touch(operands: &[&str], out: &mut Vec<BashMutation>) {
const VALUE_FLAGS: &[&str] = &["-d", "--date", "-r", "--reference", "-t"];
let kept = strip_value_flags(strip_input_redirects(operands), VALUE_FLAGS);
for op in &kept {
if let Some(path) = path_operand(op) {
out.push(BashMutation {
path,
verb: "touch",
cwd_at: CwdAt::Spawn,
});
}
}
}
pub(crate) fn emit_tee(operands: &[&str], out: &mut Vec<BashMutation>) {
let append = operands.iter().any(|t| *t == "-a" || *t == "--append");
let verb = if append { "tee-a" } else { "tee" };
let kept = strip_input_redirects(operands);
for op in &kept {
if let Some(path) = path_operand(op) {
out.push(BashMutation {
path,
verb,
cwd_at: CwdAt::Spawn,
});
}
}
}
pub(crate) fn strip_value_flags<'a>(operands: Vec<&'a str>, value_flags: &[&str]) -> Vec<&'a str> {
let mut out = Vec::new();
let mut i = 0usize;
while i < operands.len() {
let tok = operands[i];
if value_flags.contains(&tok) {
i += 2; continue;
}
if let Some((name, _)) = tok.split_once('=') {
if name.starts_with("--") && value_flags.contains(&name) {
i += 1;
continue;
}
}
out.push(tok);
i += 1;
}
out
}
pub(crate) fn strip_input_redirects<'a>(operands: &[&'a str]) -> Vec<&'a str> {
let mut out = Vec::new();
let mut i = 0usize;
while i < operands.len() {
let tok = operands[i];
if tok == "<" {
i += 2; } else if tok.starts_with('<') {
i += 1; } else {
out.push(tok);
i += 1;
}
}
out
}
pub(crate) fn non_flag_operands<'a>(operands: &[&'a str]) -> Vec<&'a str> {
strip_input_redirects(operands)
.into_iter()
.filter(|t| !t.starts_with('-'))
.collect()
}
pub(crate) fn emit_last_operand(
operands: &[&str],
verb: &'static str,
out: &mut Vec<BashMutation>,
) {
let positional = non_flag_operands(operands);
if let Some(dest_tok) = positional.last() {
if let Some(path) = path_operand(dest_tok) {
if is_remote_spec(&path) {
return;
}
out.push(BashMutation {
path,
verb,
cwd_at: CwdAt::Spawn,
});
}
}
}
pub(crate) fn is_remote_spec(path: &str) -> bool {
let head = path.split(['/', '\\']).next().unwrap_or(path);
match head.split_once(':') {
Some((h, _)) => h.len() > 1 || h.chars().next().is_none_or(|c| !c.is_ascii_alphabetic()),
None => false,
}
}
pub(crate) fn emit_ln(operands: &[&str], out: &mut Vec<BashMutation>) {
if let Some(dir) = target_directory_value(operands) {
if let Some(path) = path_operand(dir) {
out.push(BashMutation {
path,
verb: "ln",
cwd_at: CwdAt::Spawn,
});
}
return; }
emit_last_operand(operands, "ln", out);
}
pub(crate) fn emit_copy_like(operands: &[&str], verb: &'static str, out: &mut Vec<BashMutation>) {
if let Some(dir) = target_directory_value(operands) {
if let Some(path) = path_operand(dir) {
out.push(BashMutation {
path,
verb,
cwd_at: CwdAt::Spawn,
});
}
return; }
emit_last_operand(operands, verb, out);
}
pub(crate) fn target_directory_value<'a>(operands: &[&'a str]) -> Option<&'a str> {
let mut i = 0usize;
while i < operands.len() {
let tok = operands[i];
if tok == "-t" || tok == "--target-directory" {
return operands.get(i + 1).copied();
}
if let Some(v) = tok.strip_prefix("--target-directory=") {
return Some(v);
}
i += 1;
}
None
}
pub(crate) fn emit_mv(operands: &[&str], out: &mut Vec<BashMutation>) {
if let Some(dir) = target_directory_value(operands) {
if let Some(path) = path_operand(dir) {
out.push(BashMutation {
path,
verb: "mv",
cwd_at: CwdAt::Spawn,
});
}
for src in non_flag_operands(operands) {
if src == dir {
continue;
}
if let Some(path) = path_operand(src) {
out.push(BashMutation {
path,
verb: "mv-from",
cwd_at: CwdAt::Spawn,
});
}
}
return;
}
let positional = non_flag_operands(operands);
let Some((dest_tok, source_toks)) = positional.split_last() else {
return;
};
if let Some(path) = path_operand(dest_tok) {
out.push(BashMutation {
path,
verb: "mv",
cwd_at: CwdAt::Spawn,
});
}
for src in source_toks {
if let Some(path) = path_operand(src) {
out.push(BashMutation {
path,
verb: "mv-from",
cwd_at: CwdAt::Spawn,
});
}
}
}
pub(crate) fn emit_sed(operands: &[&str], out: &mut Vec<BashMutation>) {
if !operands.iter().any(|t| is_sed_in_place_flag(t)) {
return;
}
const SCRIPT_FLAGS: &[&str] = &["-e", "--expression", "-f", "--file"];
let has_explicit_script = operands
.iter()
.any(|t| SCRIPT_FLAGS.contains(t) || script_flag_inline(t));
let kept = strip_value_flags(strip_input_redirects(operands), SCRIPT_FLAGS);
let mut seen_script = has_explicit_script;
for op in &kept {
if op.starts_with('-') {
continue; }
if strip_quotes(op).is_empty() {
continue; }
if !seen_script {
seen_script = true; continue;
}
if let Some(path) = path_operand(op) {
out.push(BashMutation {
path,
verb: "sed-i",
cwd_at: CwdAt::Spawn,
});
}
}
}
pub(crate) fn script_flag_inline(tok: &str) -> bool {
matches!(tok.split_once('='), Some((name, _)) if name == "--expression" || name == "--file")
}
pub(crate) fn is_sed_in_place_flag(tok: &str) -> bool {
tok == "--in-place" || tok.starts_with("--in-place=") || tok.starts_with("-i")
}