use super::*;
pub(crate) fn emit_git(operands: &[&str], out: &mut Vec<BashMutation>) {
if let Some(sub) = git_subcommand(operands) {
if GIT_MUTATING.contains(&sub) {
out.push(BashMutation {
path: format!("git:{sub}"),
verb: "git",
});
}
}
}
pub(crate) fn git_subcommand<'a>(operands: &[&'a str]) -> Option<&'a str> {
let mut i = 0usize;
while i < operands.len() {
let tok = operands[i];
if tok == "-c" || tok == "-C" {
i += 2; } else if tok.starts_with('-') {
i += 1; } else {
return Some(tok); }
}
None
}
pub(crate) const OUTPUT_FLAGS: &[&str] = &[
"junit-xml",
"junitxml",
"report-path",
"output",
"out-file",
"outfile",
"out",
"logfile",
"log-file",
];
pub(crate) const FORMAT_SELECTORS: &[&str] = &[
"json",
"yaml",
"yml",
"wide",
"table",
"text",
"name",
"jsonpath",
"go-template",
"gotemplate",
"template",
"csv",
"tsv",
"summary",
"none",
"raw",
"pretty",
];
pub(crate) fn flag_output_path(value: &str) -> Option<String> {
let stripped = strip_quotes(value);
if !stripped.contains(['/', '.']) && FORMAT_SELECTORS.contains(&stripped) {
return None;
}
concrete_path(value)
}
pub(crate) fn emit_download_output(
operands: &[&str],
verb: &'static str,
out: &mut Vec<BashMutation>,
) {
let mut i = 0usize;
while i < operands.len() {
let tok = operands[i];
let takes_next =
tok == "-o" || (verb == "wget" && matches!(tok, "-O" | "--output-document"));
if takes_next {
if let Some(next) = operands.get(i + 1) {
if let Some(path) = path_operand(next) {
out.push(BashMutation { path, verb });
}
i += 2;
continue;
}
}
i += 1;
}
}
pub(crate) fn emit_dd(operands: &[&str], out: &mut Vec<BashMutation>) {
for op in operands {
if let Some(rest) = strip_quotes(op).strip_prefix("of=") {
if is_dev_sink(rest) {
continue;
}
if let Some(path) = concrete_path(rest) {
out.push(BashMutation { path, verb: "dd" });
}
}
}
}
pub(crate) fn emit_zip(operands: &[&str], out: &mut Vec<BashMutation>) {
for op in operands {
if op.starts_with('-') {
continue; }
if let Some(path) = path_operand(op) {
out.push(BashMutation { path, verb: "zip" });
}
return; }
}
pub(crate) fn emit_tar(operands: &[&str], out: &mut Vec<BashMutation>) {
let mut has_create = false;
let mut file_long_inline: Option<&str> = None; let mut want_file_next = false; let mut glued_file: Option<&str> = None; let mut archive: Option<&str> = None;
for op in operands {
if want_file_next {
archive = Some(op);
want_file_next = false;
continue;
}
if let Some(rest) = op.strip_prefix("--file=") {
file_long_inline = Some(rest);
continue;
}
if *op == "--file" {
want_file_next = true;
continue;
}
if *op == "--create" {
has_create = true;
continue;
}
if op.starts_with("--") {
continue; }
let bundle = op.strip_prefix('-').unwrap_or(op);
if let Some(fpos) = bundle.find('f') {
let flags_part = &bundle[..fpos];
if flags_part.contains('c') {
has_create = true;
}
let tail = &bundle[fpos + 1..];
if tail.is_empty() {
want_file_next = true;
} else {
glued_file = Some(tail);
}
} else if bundle.chars().all(|c| c.is_ascii_alphabetic()) && bundle.contains('c') {
has_create = true;
}
}
if !has_create {
return;
}
let dest = archive.or(glued_file).or(file_long_inline);
if let Some(d) = dest {
if let Some(path) = concrete_path(d) {
out.push(BashMutation { path, verb: "tar" });
}
}
}
pub(crate) fn collect_flag_outputs(tokens: &[MaskedTok], out: &mut Vec<BashMutation>) {
let mut i = 0usize;
while i < tokens.len() {
let tok = tokens[i];
if let Some(name_eq) = tok.masked.strip_prefix("--") {
if let Some((name, _)) = name_eq.split_once('=') {
if OUTPUT_FLAGS.contains(&name) {
let value = &tok.orig[name.len() + 3..]; if let Some(path) = flag_output_path(value) {
out.push(BashMutation {
path,
verb: "flag-output",
});
}
}
i += 1;
continue;
}
if OUTPUT_FLAGS.contains(&name_eq) {
if let Some(next) = tokens.get(i + 1) {
if !next.masked.starts_with('-') {
if let Some(path) = flag_output_path(next.orig) {
out.push(BashMutation {
path,
verb: "flag-output",
});
}
i += 2;
continue;
}
}
}
}
i += 1;
}
}