use super::*;
pub(crate) fn collect_redirections(
tokens: &[MaskedTok],
out: &mut Vec<BashMutation>,
) -> std::collections::BTreeSet<usize> {
let mut consumed: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
let mut i = 0usize;
while i < tokens.len() {
let tok = tokens[i];
let body = strip_fd_qualifier(tok.masked);
if body == ">|" {
consumed.insert(i);
if let Some(next) = tokens.get(i + 1) {
push_redirect_target(next.orig, ">", out);
consumed.insert(i + 1);
}
i += 1;
} else if body.starts_with(">|") {
let off = tok.masked.len() - body.len() + 2;
push_redirect_target(&tok.orig[off..], ">", out);
consumed.insert(i);
i += 1;
} else if body == ">>" || body == ">" {
let verb = if body == ">>" { ">>" } else { ">" };
consumed.insert(i);
if let Some(next) = tokens.get(i + 1) {
push_redirect_target(next.orig, verb, out);
consumed.insert(i + 1);
}
i += 1;
} else if body.starts_with(">>") {
let off = tok.masked.len() - body.len() + 2;
push_redirect_target(&tok.orig[off..], ">>", out);
consumed.insert(i);
i += 1;
} else if body == ">&" {
consumed.insert(i);
if let Some(next) = tokens.get(i + 1) {
if !is_fd_number(next.orig) {
push_redirect_target(next.orig, ">", out);
}
consumed.insert(i + 1);
}
i += 1;
} else if body.starts_with(">&") {
let off = tok.masked.len() - body.len() + 2;
let tail = &tok.orig[off..];
if !is_fd_number(tail) {
push_redirect_target(tail, ">", out);
}
consumed.insert(i);
i += 1;
} else if body.starts_with('>') {
let off = tok.masked.len() - body.len() + 1;
push_redirect_target(&tok.orig[off..], ">", out);
consumed.insert(i);
i += 1;
} else {
i += 1;
}
}
consumed
}
pub(crate) fn strip_fd_qualifier(tok: &str) -> &str {
let bytes = tok.as_bytes();
let mut k = 0usize;
if bytes.first() == Some(&b'&') {
k = 1;
} else {
while k < bytes.len() && bytes[k].is_ascii_digit() {
k += 1;
}
}
if k > 0 && bytes.get(k) == Some(&b'>') {
&tok[k..]
} else {
tok
}
}
pub(crate) fn is_fd_number(tail: &str) -> bool {
let t = strip_quotes(tail);
t == "-" || (!t.is_empty() && t.bytes().all(|b| b.is_ascii_digit()))
}
pub(crate) fn push_redirect_target(tail: &str, verb: &'static str, out: &mut Vec<BashMutation>) {
if is_dev_sink(tail) || tail.starts_with('&') {
return; }
if let Some(path) = path_operand(tail) {
out.push(BashMutation { path, verb });
}
}
pub(crate) fn is_dev_sink(tail: &str) -> bool {
matches!(
strip_quotes(tail),
"/dev/null" | "/dev/stderr" | "/dev/stdout"
)
}
pub(crate) fn path_operand(token: &str) -> Option<String> {
let stripped = strip_quotes(token);
let stripped = trim_structural_tail(stripped);
if stripped.is_empty() || stripped == "-" {
return None;
}
if stripped.starts_with('-') {
return None; }
if is_assignment(stripped) {
return None; }
if has_unresolved_var(stripped) {
return None; }
if is_dev_sink(stripped) || stripped.starts_with('&') {
return None;
}
if has_syntax_noise(stripped) {
return None;
}
Some(stripped.to_string())
}
pub(crate) fn trim_structural_tail(token: &str) -> &str {
let mut s = token;
loop {
s = match s.as_bytes().last() {
Some(b';' | b',') => &s[..s.len() - 1],
Some(b'"' | b'\'') => &s[..s.len() - 1],
Some(b')') if s.matches('(').count() < s.matches(')').count() => &s[..s.len() - 1],
Some(b'}') if s.matches('{').count() < s.matches('}').count() => &s[..s.len() - 1],
_ => break,
};
}
s
}
pub(crate) fn has_syntax_noise(token: &str) -> bool {
if token.matches('"').count() % 2 == 1 || token.matches('\'').count() % 2 == 1 {
return true;
}
if token.starts_with(">(") || token.starts_with("<(") || token.starts_with('(') {
return true;
}
if token.contains('>') || token.contains('<') {
return true;
}
if token.contains('\\') || token.contains('|') || token.contains('^') {
return true;
}
if token.contains('`') {
return true;
}
if token.matches('(').count() != token.matches(')').count()
|| token.matches('[').count() != token.matches(']').count()
|| token.matches('{').count() != token.matches('}').count()
{
return true;
}
if token.starts_with('=') {
return true;
}
if !token.is_empty() && token.bytes().all(|b| b.is_ascii_digit()) {
return true;
}
if !token.contains('/') && (token.contains(',') || token.ends_with(':')) {
return true;
}
false
}
pub(crate) fn concrete_path(token: &str) -> Option<String> {
let path = path_operand(token)?;
if path.contains(['*', '?', '[']) {
return None; }
Some(path)
}
pub(crate) fn has_unresolved_var(token: &str) -> bool {
token.contains('$') || token.starts_with('~')
}
pub(crate) fn strip_quotes(token: &str) -> &str {
let b = token.as_bytes();
if b.len() >= 2 {
let first = b[0];
let last = b[b.len() - 1];
if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
return &token[1..token.len() - 1];
}
}
token
}