const APIS: &[&str] = &[
"open(",
"open ",
"io.open",
"pathlib",
"Path(",
"read_text",
"read_bytes",
"readFile",
"createReadStream",
"readdir",
"require('fs')",
"require(\"fs\")",
"require('node:fs')",
"require(\"node:fs\")",
"fs.",
"fs/promises",
"File.",
"IO.",
"Dir.",
"<F>",
"<$",
"file_get_contents",
"fopen",
"readfile(",
"Deno.read",
"Deno.open",
"Bun.file",
"Get-Content",
"shutil",
"os.listdir",
"os.walk",
"os.scandir",
"glob.",
"urllib",
"urlopen",
"requests",
"httpx",
"aiohttp",
"http.client",
"socket",
"fetch(",
"XMLHttpRequest",
"http.",
"https.",
"net.",
"Net::HTTP",
"LWP",
"IO::Socket",
"HTTP::Tiny",
"curl_",
"fsockopen",
"open-uri",
"URI.open",
"Invoke-WebRequest",
"Invoke-RestMethod",
"WebClient",
"environ",
"getenv",
"expandvars",
"expanduser",
"process.env",
"Deno.env",
"Bun.env",
"$ENV",
"%ENV",
"ENV[",
"ENV.",
"$_ENV",
"$_SERVER",
"$env:",
"Path.home",
"homedir",
"Dir.home",
"subprocess",
"os.system",
"os.popen",
"os.exec",
"os.spawn",
"pty.",
"child_process",
"execSync",
"execFile",
"spawn(",
"spawnSync",
"exec(",
"eval(",
"system(",
"system ",
"popen",
"Process.",
"__import__",
"importlib",
"getattr(",
"compile(",
"Function(",
"Deno.Command",
"Deno.run",
"Bun.spawn",
"Start-Process",
"Invoke-Expression",
"qx",
];
const BUILDERS: &[&str] = &[
".format(",
"join(",
"chr(",
"fromCharCode",
"atob(",
"b64decode",
"base64",
"fromhex",
"unhexlify",
".decode(",
"[::-1]",
"reverse",
".replace(",
"rot13",
"codecs.",
"pack(",
"sprintf",
"String.raw",
"Buffer.from",
".concat(",
"%s",
];
const TAKE_A_NAME: &[&str] = &[
"open(",
"Path(",
"urlopen(",
"Request(",
"readFile(",
"readFileSync(",
"createReadStream(",
"readdirSync(",
"fetch(",
"requests.get(",
"requests.post(",
"httpx.get(",
"httpx.post(",
"http.get(",
"https.get(",
"http.request(",
"https.request(",
"create_connection(",
"net.connect(",
"net.createConnection(",
"fopen(",
"file_get_contents(",
"File.read(",
"File.open(",
"File.readlines(",
"IO.read(",
"IO.readlines(",
"os.system(",
"os.popen(",
"subprocess.run(",
"subprocess.call(",
"subprocess.Popen(",
"subprocess.check_output(",
"execSync(",
"spawn(",
"spawnSync(",
"system(",
"exec(",
"eval(",
"__import__(",
];
pub(super) fn opaque(code: &str) -> bool {
APIS.iter().any(|api| has(code, api)) && builds_text(code)
}
fn builds_text(code: &str) -> bool {
BUILDERS.iter().any(|b| has(code, b))
|| strings(code).iter().any(|s| s.interpolates || s.joined)
|| TAKE_A_NAME
.iter()
.any(|call| starts(code, call).any(|at| !literal_argument(&code[at + call.len()..])))
|| perl_open_takes_a_variable(code)
}
fn starts<'a>(code: &'a str, token: &'a str) -> impl Iterator<Item = usize> + 'a {
let word = token.starts_with(|c: char| c.is_ascii_alphanumeric());
code.match_indices(token)
.map(|(at, _)| at)
.filter(move |&at| {
!word
|| !code[..at]
.chars()
.next_back()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
})
}
fn has(code: &str, token: &str) -> bool {
starts(code, token).next().is_some()
}
fn literal_argument(rest: &str) -> bool {
let rest = rest.trim_start_matches(|c: char| c.is_whitespace() || c == '(' || c == '[');
let rest = rest.trim_start_matches(['r', 'b', 'u', 'R', 'B', 'U']);
rest.is_empty() || rest.starts_with(['\'', '"', ')', ']'])
}
fn perl_open_takes_a_variable(code: &str) -> bool {
starts(code, "open").any(|at| {
let statement = code[at..].split(';').next().unwrap_or_default();
statement
.rsplit(',')
.next()
.filter(|_| statement.contains(','))
.is_some_and(|last| !literal_argument(last.trim_end_matches([')', ' '])))
})
}
#[derive(Debug, Default)]
struct Literal {
interpolates: bool,
joined: bool,
}
fn strings(code: &str) -> Vec<Literal> {
let bytes = code.as_bytes();
let mut found: Vec<Literal> = Vec::new();
let mut last_end: Option<usize> = None;
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 start = i;
let closes = |at: usize| {
bytes.get(at) == Some("e)
&& (!triple || bytes.get(at..at + 3) == Some(&[quote; 3][..]))
};
let mut j = i + open;
let (body_end, end) = loop {
match bytes.get(j) {
None => break (bytes.len(), bytes.len()),
Some(b'\\') => j += 2,
Some(_) if closes(j) => break (j, j + open),
Some(_) => j += 1,
}
};
let body = code.get(start + open..body_end).unwrap_or_default();
let end = end.min(code.len());
let prefix = code[..start]
.chars()
.rev()
.take_while(char::is_ascii_alphabetic)
.collect::<String>();
let before = code[..start].trim_end();
let after = code[end.min(code.len())..].trim_start();
let follows_string = last_end.is_some_and(|e| code[e..start].trim().is_empty());
let literal = Literal {
interpolates: interpolates(quote, &prefix, body),
joined: follows_string
|| joins(before.chars().next_back(), true, before)
|| joins(after.chars().next(), false, after),
};
if follows_string && let Some(previous) = found.last_mut() {
previous.joined = true;
}
found.push(literal);
last_end = Some(end.min(code.len()));
i = end.max(i + 1);
}
found
}
fn interpolates(quote: u8, prefix: &str, body: &str) -> bool {
let f_string = prefix.len() <= 2 && prefix.contains(['f', 'F']) && body.contains('{');
let template = quote == b'`' && body.contains("${");
let double = quote == b'"'
&& (body.contains("#{")
|| body.contains("${")
|| body.match_indices('$').any(|(at, _)| {
body[at + 1..]
.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_' || c == '{')
}));
f_string || template || double
}
fn joins(next: Option<char>, left: bool, side: &str) -> bool {
match next {
Some('+') => true,
Some('%') => !left,
Some('.') => {
if left {
true
} else {
let rest = side[1..].trim_start_matches('.').trim_start();
rest.starts_with(['\'', '"', '$', '@'])
}
}
_ => false,
}
}