mod script;
#[cfg(test)]
mod tests;
use super::text::{Arg, Operands, Spec, parse, unproven};
use super::{ClassifyError, ShellContext, Sink};
const SED: Spec = Spec {
flags: "nErsuz",
valued: "ef",
optional: "i",
long_flags: &[
"quiet",
"silent",
"regexp-extended",
"separate",
"unbuffered",
"null-data",
"posix",
"debug",
"sandbox",
"follow-symlinks",
],
long_valued: &["expression", "file"],
long_optional: &["in-place"],
permute: true,
};
#[derive(Default)]
struct Line<'a> {
scripts: Vec<&'a str>,
script_first: bool,
operands: Vec<usize>,
in_place: bool,
suffixes: Vec<&'a str>,
empty_suffix: bool,
}
pub(super) fn classify(
argv: &[String],
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<Operands, ClassifyError> {
let mut ops = Operands::default();
let mut line = Line::default();
for arg in parse(argv, &SED) {
match arg {
Arg::Opt {
name: "e" | "expression",
value: Some(script),
data,
} => {
line.script_first |= line.operands.is_empty();
line.scripts.push(script);
ops.data.extend(data);
}
Arg::Opt {
name: name @ ("i" | "in-place"),
value,
data,
} => {
line.in_place = true;
line.suffixes.extend(value);
let next = data.filter(|_| name == "i" && value.is_none());
match next.and_then(|at| argv.get(at + 1)).map(String::as_str) {
Some("") => line.empty_suffix = true,
Some(word) if word.starts_with('-') => line.suffixes.push(word),
Some(_) if next.is_some_and(|at| bsd_stops(argv, at, &line)) => {}
Some(_) => unproven(argv, "-i", sink)?,
None => {}
}
}
Arg::Opt {
name: "f" | "file",
value,
..
} => {
unproven(argv, "-f", sink)?;
if let Some(file) = value {
sink.read(ctx, file)?;
}
}
Arg::Unknown(word) => unproven(argv, word, sink)?,
Arg::Opt { .. } => {}
Arg::Operand(at) => line.operands.push(at),
}
}
for (at, is_data) in operand_scripts(argv, &line) {
if is_data {
ops.data.push(at);
}
check(argv, &argv[at], ctx, sink)?;
}
for script in &line.scripts {
check(argv, script, ctx, sink)?;
}
if line.in_place {
in_place(argv, &line, &mut ops, ctx, sink)?;
}
Ok(ops)
}
fn bsd_stops(argv: &[String], at: usize, line: &Line<'_>) -> bool {
line.scripts.is_empty()
&& argv.len() == at + 3
&& !argv[at + 2].starts_with('-')
&& !argv[at + 2].contains(['w', 'W'])
}
fn operand_scripts(argv: &[String], line: &Line<'_>) -> Vec<(usize, bool)> {
let first = line.operands.first().copied();
if !line.scripts.is_empty() {
return first
.filter(|_| !line.script_first)
.map(|at| vec![(at, false)])
.unwrap_or_default();
}
let mut found: Vec<_> = first.map(|at| (at, true)).into_iter().collect();
if line.empty_suffix && first.is_some_and(|at| argv[at].is_empty()) {
found.extend(line.operands.get(1).map(|&at| (at, false)));
}
found
}
fn check(
argv: &[String],
script: &str,
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<(), ClassifyError> {
let Some(effects) = script::effects(script) else {
return unproven(argv, script, sink);
};
for file in &effects.reads {
sink.read(ctx, &literal(file))?;
}
for file in &effects.writes {
sink.write(ctx, &literal(file))?;
}
Ok(())
}
fn in_place(
argv: &[String],
line: &Line<'_>,
ops: &mut Operands,
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<(), ClassifyError> {
if let Some(suffix) = line.suffixes.iter().find(|s| s.contains(['/', '*'])) {
return unproven(argv, suffix, sink);
}
let files: Vec<usize> = line
.operands
.iter()
.copied()
.filter(|i| !ops.data.contains(i))
.collect();
for &file in &files {
for suffix in line.suffixes.iter().filter(|s| !s.is_empty()) {
sink.write(ctx, &format!("{}{suffix}", argv[file]))?;
}
}
ops.writes.extend(files);
Ok(())
}
fn literal(file: &str) -> String {
if file.starts_with('~') {
format!("./{file}")
} else {
file.to_owned()
}
}