pub(crate) use crate::pattern_load::PATTERNS;
use crate::pattern::{self, Pattern};
const MAX_REWRITE_INPUT_BYTES: usize = 16 * 1024;
pub fn rewrite(command: &str, patterns: &[Pattern]) -> Option<String> {
let trimmed = command.trim();
if trimmed.is_empty() || trimmed.len() > MAX_REWRITE_INPUT_BYTES {
return None;
}
let split = split_segments(trimmed)?;
let mut out = String::with_capacity(trimmed.len());
let mut changed = false;
for (seg_text, sep_after) in split.iter() {
let rewritten = rewrite_segment(seg_text, patterns);
match &rewritten {
Some(r) => {
changed = true;
out.push_str(r);
out.push_str(sep_after);
}
None => {
out.push_str(seg_text);
out.push_str(sep_after);
}
}
}
changed.then_some(out)
}
fn rewrite_segment(segment: &str, patterns: &[Pattern]) -> Option<String> {
if has_unquoted_newline(segment) {
return None;
}
let tokens = scan_tokens(segment)?;
let first = tokens
.first()
.map(|t| segment[t.start..t.end].to_owned())
.filter(|w| !w.is_empty())?;
if first == "oo" {
return None;
}
let mut env_count = 0;
for (i, t) in tokens.iter().enumerate() {
let word = &segment[t.start..t.end];
let is_env = word
.split_once('=')
.is_some_and(|(name, _)| is_valid_var_name(name));
if !is_env {
env_count = i;
break;
}
env_count = i + 1;
}
let rest_start = if env_count == 0 {
0
} else if env_count < tokens.len() {
tokens[env_count].start
} else {
return None; };
let rest = normalize_whitespace(&segment[rest_start..]);
pattern::find_matching(&rest, patterns)?;
let mut out = String::with_capacity(segment.len() + 3);
out.push_str(&segment[..rest_start]);
out.push_str("oo ");
out.push_str(&segment[rest_start..]);
Some(out)
}
fn is_valid_var_name(name: &str) -> bool {
!name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TokenRange {
start: usize,
end: usize,
}
fn scan_tokens(segment: &str) -> Option<Vec<TokenRange>> {
let chars: Vec<(usize, char)> = segment.char_indices().collect();
let n = chars.len();
let mut tokens = Vec::new();
let mut i = 0usize;
while i < n {
let (byte_idx, c) = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
let token_start = byte_idx;
let token_end = scan_token(&chars, &mut i)?;
tokens.push(TokenRange {
start: token_start,
end: token_end,
});
}
Some(tokens)
}
fn scan_token(chars: &[(usize, char)], i: &mut usize) -> Option<usize> {
let n = chars.len();
let start = chars[*i].0;
let mut in_sq = false;
let mut in_dq = false;
let mut escaped = false;
let mut env_value = false;
let mut j = *i;
while j < n {
let (_, c) = chars[j];
if escaped {
escaped = false;
j += 1;
continue;
}
if in_sq {
if c == '\'' {
in_sq = false;
j += 1;
continue;
}
j += 1;
continue;
}
if in_dq {
if c == '\\' {
escaped = true;
j += 1;
continue;
}
if c == '"' {
in_dq = false;
j += 1;
continue;
}
j += 1;
continue;
}
if c == '\'' {
if env_value {
return None; }
in_sq = true;
j += 1;
continue;
}
if c == '"' {
if env_value {
return None; }
in_dq = true;
j += 1;
continue;
}
if c == '\\' {
if env_value {
return None; }
escaped = true;
j += 1;
continue;
}
if c == '=' && !env_value && is_env_token_prefix(chars, j) {
env_value = true;
j += 1;
continue;
}
if c.is_whitespace() {
break;
}
j += 1;
}
*i = j;
let end = if *i < n {
chars[*i].0
} else {
segment_len(chars)
};
Some(end.max(start))
}
fn is_env_token_prefix(chars: &[(usize, char)], i: usize) -> bool {
let mut j = i;
while j > 0 {
let jc = chars[j - 1].1;
if jc.is_whitespace() || jc == '=' || jc == '\'' || jc == '"' || jc == '\\' {
return false;
}
j -= 1;
}
true
}
fn has_unquoted_newline(segment: &str) -> bool {
let mut quote: Option<char> = None;
let mut escaped = false;
for c in segment.chars() {
if escaped {
escaped = false;
continue;
}
match quote {
Some('"') => {
if c == '\\' {
escaped = true;
} else if c == '"' {
quote = None;
}
}
Some('\'') if c == '\'' => quote = None,
Some('\'') => {}
None => {
if c == '\n' || c == '\r' {
return true;
}
if c == '\'' || c == '"' {
quote = Some(c);
} else if c == '\\' {
escaped = true;
}
}
_ => {}
}
}
false
}
fn normalize_whitespace(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn segment_len(chars: &[(usize, char)]) -> usize {
chars.last().map(|(idx, c)| idx + c.len_utf8()).unwrap_or(0)
}
fn split_segments(command: &str) -> Option<Vec<(String, &'static str)>> {
let mut iter = command.char_indices().peekable();
let mut result: Vec<(String, &'static str)> = Vec::new();
let mut start = 0usize;
let mut quote: Option<char> = None;
while let Some((byte_idx, c)) = iter.next() {
match quote {
Some('"') if c == '\\' => {
iter.peek()?;
iter.next();
}
Some(q) if c == q => quote = None,
Some(_) => {}
None if c == '\'' || c == '"' => quote = Some(c),
None if c == '\\' => {
iter.peek()?;
iter.next();
}
None => {
let next = iter.peek().map(|(_, c)| *c);
if c == '&' && next == Some('&') {
result.push((command[start..byte_idx].trim().to_string(), " && "));
start = byte_idx + 2;
iter.next();
continue;
}
if c == '|' && next == Some('|') {
result.push((command[start..byte_idx].trim().to_string(), " || "));
start = byte_idx + 2;
iter.next();
continue;
}
if c == ';' {
result.push((command[start..byte_idx].trim().to_string(), " ; "));
start = byte_idx + 1;
continue;
}
if c == '|' || c == '<' || c == '>' || c == '&' {
return None;
}
if (c == '$' && next == Some('(')) || c == '`' {
return None;
}
}
}
}
if quote.is_some() {
return None;
}
result.push((command[start..].trim().to_string(), ""));
Some(result)
}
#[cfg(test)]
#[path = "rewrite_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "rewrite_ws_tests.rs"]
mod ws_tests;