use crate::resource_limits::{MINIFIED_MEAN_LINE_BYTES, SOURCE_CLASSIFICATION_PREFIX_BYTES};
use crate::typescript::frameworks::registration_modules;
use std::collections::{BTreeMap, BTreeSet};
use std::fs::File;
use std::io::Read;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExcludedSource {
Compressed,
Binary,
Minified,
}
impl ExcludedSource {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Compressed => "compressed",
Self::Binary => "binary",
Self::Minified => "minified",
}
}
}
const COMPRESSED_MAGICS: [&[u8]; 5] = [
&[0x1f, 0x8b], &[0x50, 0x4b, 0x03, 0x04], &[0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00], &[0x28, 0xb5, 0x2f, 0xfd], &[0x04, 0x22, 0x4d, 0x18], ];
fn opens_compressed_container(prefix: &[u8]) -> bool {
COMPRESSED_MAGICS
.iter()
.any(|magic| prefix.starts_with(magic))
|| opens_bzip2_stream(prefix)
}
fn opens_bzip2_stream(prefix: &[u8]) -> bool {
const BLOCK_MAGIC: &[u8] = b"1AY&SY";
prefix.starts_with(b"BZh")
&& prefix
.get(3)
.is_some_and(|digit| (b'1'..=b'9').contains(digit))
&& prefix.get(4..4 + BLOCK_MAGIC.len()) == Some(BLOCK_MAGIC)
}
pub(crate) fn inspect(path: &Path, registrations: &[String]) -> Option<ExcludedSource> {
let mut file = File::open(path).ok()?;
let mut prefix = vec![0_u8; SOURCE_CLASSIFICATION_PREFIX_BYTES];
let mut filled = 0;
while filled < prefix.len() {
match file.read(&mut prefix[filled..]) {
Ok(0) => break,
Ok(read) => filled += read,
Err(_) => return None,
}
}
prefix.truncate(filled);
classify(&prefix, registrations)
}
pub(crate) fn classify(prefix: &[u8], registrations: &[String]) -> Option<ExcludedSource> {
if prefix.is_empty() {
return None;
}
if opens_compressed_container(prefix) {
return Some(ExcludedSource::Compressed);
}
if prefix.contains(&0) {
return Some(ExcludedSource::Binary);
}
(is_minified(prefix) && !mentions_registration(prefix, registrations))
.then_some(ExcludedSource::Minified)
}
const QUALIFIABLE_REGISTRATIONS: [&str; 8] = [
"Given",
"When",
"Then",
"And",
"But",
"Step",
"defineStep",
"createBdd",
];
const BARE_ONLY_REGISTRATIONS: [&str; 3] = ["given", "when", "then"];
fn mentions_registration(prefix: &[u8], registrations: &[String]) -> bool {
if registration_modules().any(|module| contains_bytes(prefix, module.as_bytes())) {
return true;
}
if imports_project_module(prefix) {
return true;
}
let qualifiable = QUALIFIABLE_REGISTRATIONS
.iter()
.map(|name| name.as_bytes())
.chain(registrations.iter().map(String::as_bytes))
.collect::<BTreeSet<_>>();
let bare_only = BARE_ONLY_REGISTRATIONS
.iter()
.map(|name| name.as_bytes())
.collect::<BTreeSet<_>>();
let angles = angle_bracket_pairs(prefix);
identifiers(prefix).any(|identifier| {
let name = &prefix[identifier.start..identifier.end];
(qualifiable.contains(&name) || (!identifier.qualified && bare_only.contains(&name)))
&& is_call_callee(prefix, identifier.end, &angles)
})
}
fn imports_project_module(prefix: &[u8]) -> bool {
identifiers(prefix).any(|identifier| {
if identifier.qualified {
return false;
}
let keyword = &prefix[identifier.start..identifier.end];
if !matches!(keyword, b"from" | b"import" | b"require" | b"export") {
return false;
}
let Some(mut index) = skip_trivia(prefix, identifier.end) else {
return false;
};
if prefix.get(index) == Some(&b'(') {
match skip_trivia(prefix, index + 1) {
Some(next) => index = next,
None => return false,
}
}
prefix
.get(index)
.is_some_and(|byte| matches!(byte, b'\'' | b'"' | b'`'))
&& opens_local_specifier(&prefix[index + 1..])
})
}
const LOCAL_SPECIFIER_PREFIXES: [&[u8]; 5] = [b"./", b"../", b"~/", b"@/", b"#"];
fn opens_local_specifier(after: &[u8]) -> bool {
LOCAL_SPECIFIER_PREFIXES
.iter()
.any(|prefix| after.starts_with(prefix))
}
fn skip_trivia(prefix: &[u8], at: usize) -> Option<usize> {
let mut index = at;
loop {
while let Some(length) = whitespace_length(prefix, index) {
index += length;
}
match prefix.get(index..index + 2) {
Some(b"/*") => index += 2 + find_subslice(&prefix[index + 2..], b"*/")? + 2,
Some(b"//") => {
index += 2 + prefix[index + 2..].iter().position(|byte| *byte == b'\n')? + 1;
}
_ => return Some(index),
}
}
}
struct Identifier {
start: usize,
end: usize,
qualified: bool,
}
fn identifiers(prefix: &[u8]) -> impl Iterator<Item = Identifier> + '_ {
let mut index = 0;
std::iter::from_fn(move || {
while index < prefix.len() && !is_identifier_byte(prefix[index]) {
index += 1;
}
if index == prefix.len() {
return None;
}
let start = index;
while index < prefix.len() && is_identifier_byte(prefix[index]) {
index += 1;
}
Some(Identifier {
start,
end: index,
qualified: start
.checked_sub(1)
.is_some_and(|before| prefix[before] == b'.'),
})
})
}
fn is_call_callee(prefix: &[u8], from: usize, angles: &BTreeMap<usize, usize>) -> bool {
let mut index = from;
loop {
let Some(next) = skip_trivia(prefix, index) else {
return false;
};
index = next;
match prefix.get(index) {
Some(b'(') => return true,
Some(b')' | b'!') => index += 1,
Some(b'<') => match angles.get(&index) {
Some(close) => index = close + 1,
None => return false,
},
_ => match skip_type_operator(prefix, index) {
Some(next) => index = next,
None => return false,
},
}
}
}
fn angle_bracket_pairs(prefix: &[u8]) -> BTreeMap<usize, usize> {
let mut unclosed = Vec::new();
let mut pairs = BTreeMap::new();
let mut braces: Vec<bool> = Vec::new();
for (index, byte) in prefix.iter().enumerate() {
match byte {
b'<' => unclosed.push(index),
b'>' if index > 0 && prefix[index - 1] == b'=' => {}
b'>' => {
if let Some(start) = unclosed.pop() {
pairs.insert(start, index);
}
}
b'{' => braces.push(false),
b'}' => {
braces.pop();
}
b':' => {
if let Some(brace) = braces.last_mut() {
*brace = true;
}
}
b';' if !braces.last().copied().unwrap_or(false) => unclosed.clear(),
_ => {}
}
}
pairs
}
fn skip_type_operator(prefix: &[u8], from: usize) -> Option<usize> {
let operator = [&b"as"[..], &b"satisfies"[..]].into_iter().find(|word| {
prefix[from..].starts_with(word)
&& !prefix[from + word.len()..]
.first()
.is_some_and(|byte| is_identifier_byte(*byte))
})?;
let body = &prefix[from + operator.len()..];
let mut depth = 0_usize;
for (offset, byte) in body.iter().enumerate() {
match byte {
b'(' | b'[' | b'{' | b'<' => depth += 1,
b'>' if offset > 0 && body[offset - 1] == b'=' => {}
b')' | b']' | b'}' | b'>' if depth > 0 => depth -= 1,
b')' | b',' | b';' if depth == 0 => return Some(from + operator.len() + offset),
_ => {}
}
}
None
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
(!needle.is_empty() && haystack.len() >= needle.len())
.then(|| {
haystack
.windows(needle.len())
.position(|window| window == needle)
})
.flatten()
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
find_subslice(haystack, needle).is_some()
}
fn is_identifier_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$'
}
fn whitespace_length(prefix: &[u8], at: usize) -> Option<usize> {
let byte = *prefix.get(at)?;
if byte.is_ascii() {
return byte.is_ascii_whitespace().then_some(1);
}
let length = match byte {
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => return None,
};
let text = std::str::from_utf8(prefix.get(at..at + length)?).ok()?;
(text.starts_with(char::is_whitespace) || text.starts_with('\u{feff}')).then_some(length)
}
fn is_minified(prefix: &[u8]) -> bool {
let mut lines = 0_usize;
let mut bytes = 0_usize;
for line in prefix.split(|byte| *byte == b'\n') {
let line = match line {
[body @ .., b'\r'] => body,
body => body,
};
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
lines += 1;
bytes += line.len();
}
lines > 0 && bytes / lines > MINIFIED_MEAN_LINE_BYTES
}
#[cfg(test)]
mod tests;