use std::collections::BTreeMap;
use crate::paths;
const WRITERS: &[&str] = &[
"write_text",
"write_bytes",
".touch(",
"shutil.copy",
"shutil.move",
"os.rename",
"os.replace",
"os.symlink",
"os.link",
"os.mkfifo",
"writeFileSync",
"writeFile(",
"appendFileSync",
"appendFile(",
"createWriteStream",
"copyFileSync",
"copyFile(",
"renameSync",
"symlinkSync",
"Deno.writeTextFile",
"Deno.writeFile",
"Deno.copyFile",
"Deno.rename",
"Bun.write",
"File.write",
"IO.write",
"File.rename",
"binwrite",
"FileUtils.",
"file_put_contents",
"Set-Content",
"Add-Content",
"Out-File",
];
const OPENERS: &[&str] = &["open(", "fopen(", "openSync(", "File.new("];
const WRITE_FLAGS: &[&str] = &["O_WRONLY", "O_RDWR", "O_CREAT", "O_APPEND", "O_TRUNC"];
pub(super) fn written(code: &str) -> Option<Vec<String>> {
let literals = literals(code);
let writes = WRITERS.iter().any(|w| code.contains(w))
|| OPENERS.iter().any(|call| {
code.match_indices(call)
.any(|(at, _)| opens_for_writing(arguments(&code[at + call.len()..])))
})
|| perl_writes(code, &literals);
writes.then(|| {
literals
.iter()
.map(|l| l.trim_start_matches(['+', '>', '<', ' ']))
.filter(|l| names_a_file(l))
.map(str::to_owned)
.collect()
})
}
pub(super) fn targets(code: &str) -> Vec<String> {
let mut places: BTreeMap<usize, Vec<String>> = BTreeMap::new();
let offset = |part: &str| part.as_ptr() as usize - code.as_ptr() as usize;
let mut receiver = |at: usize| {
let before = code[..at].trim_end_matches('.');
if let Some(inside) = before.strip_suffix(')').and_then(opened) {
places.insert(offset(inside), literals(inside));
}
};
let mut calls: Vec<(usize, &str)> = Vec::new();
for writer in WRITERS {
for (at, _) in code.match_indices(writer) {
receiver(at);
let after = code[at + writer.len()..]
.trim_start_matches(|c: char| c.is_ascii_alphanumeric() || c == '_');
let after = if writer.ends_with('(') {
Some(after)
} else {
after.strip_prefix('(')
};
calls.extend(after.map(|rest| (offset(rest), arguments(rest))));
}
}
for call in OPENERS {
for (at, _) in code.match_indices(call) {
let rest = &code[at + call.len()..];
let arguments = arguments(rest);
if !opens_for_writing(arguments) {
continue;
}
receiver(at);
if arguments.trim_start().starts_with(['"', '\'', '`']) {
calls.push((
offset(rest),
&arguments[..arguments.find(',').unwrap_or(arguments.len())],
));
}
}
}
for (at, arguments) in calls {
places.insert(at, literals(arguments));
}
let mut found: Vec<String> = places.into_values().flatten().collect();
let literals = literals(code);
if perl_writes(code, &literals) {
let mut after_mode = false;
for literal in literals {
let name = literal.trim_start_matches(['+', '>', '<', ' ']);
let writes = literal.starts_with(['>', '+']);
if after_mode || (writes && !name.is_empty()) {
found.push(name.to_owned());
}
after_mode = writes && name.is_empty();
}
}
found.retain(|name| !name.is_empty());
found
}
fn opened(before: &str) -> Option<&str> {
let mut depth = 0usize;
for (at, c) in before.char_indices().rev() {
match c {
')' => depth += 1,
'(' if depth == 0 => return Some(&before[at + 1..]),
'(' => depth -= 1,
_ => {}
}
}
None
}
fn arguments(rest: &str) -> &str {
let mut depth = 0usize;
for (at, c) in rest.char_indices() {
match c {
'(' => depth += 1,
')' if depth == 0 => return &rest[..at],
')' => depth -= 1,
_ => {}
}
}
rest
}
fn opens_for_writing(arguments: &str) -> bool {
literals(arguments).iter().any(|l| is_write_mode(l))
|| WRITE_FLAGS.iter().any(|flag| arguments.contains(flag))
}
fn is_write_mode(text: &str) -> bool {
!text.is_empty()
&& text.len() <= 4
&& text.chars().all(|c| "rwaxbtU+".contains(c))
&& text.contains(['w', 'a', 'x', '+'])
}
fn perl_writes(code: &str, literals: &[String]) -> bool {
code.contains("open")
&& literals
.iter()
.any(|l| l.starts_with('>') || l.starts_with("+<") || l.starts_with("+>"))
}
fn names_a_file(text: &str) -> bool {
if text.is_empty() || text.contains("://") || text.contains(char::is_whitespace) {
return false;
}
if text.contains(['$', '{', '}', '%', '*', '?', '`']) {
return false;
}
paths::looks_like_path(text)
|| text.rsplit_once('.').is_some_and(|(stem, ext)| {
!stem.is_empty()
&& (1..=5).contains(&ext.len())
&& ext.bytes().all(|b| b.is_ascii_alphanumeric())
})
}
fn literals(code: &str) -> Vec<String> {
let bytes = code.as_bytes();
let mut found = Vec::new();
let mut i = 0;
while i < bytes.len() {
let quote = bytes[i];
if !matches!(quote, b'\'' | b'"' | b'`') {
i += 1;
continue;
}
let triple = bytes.get(i..i + 3) == Some(&[quote; 3][..]);
let open = if triple { 3 } else { 1 };
let mut j = i + open;
let mut end = bytes.len();
while j < bytes.len() {
if bytes[j] == b'\\' {
j += 2;
continue;
}
if bytes[j] == quote && (!triple || bytes.get(j..j + 3) == Some(&[quote; 3][..])) {
end = j;
break;
}
j += 1;
}
if let Some(body) = code.get(i + open..end) {
found.push(body.to_owned());
}
i = end + open;
}
found
}