use std::fs::File;
use std::io::Read;
use std::path::Path;
pub trait Find {
fn extensions(&self) -> &'static [&'static str];
fn interpreters(&self) -> Option<&'static [&'static str]> {
None
}
fn is_path_invalid(&self, _path: &Path) -> bool {
false
}
fn is_valid_path(&self, path: &Path) -> bool {
if self.is_path_invalid(path) {
return false;
}
match (path.extension(), self.interpreters()) {
(Some(ext), _) => ext
.to_str()
.is_some_and(|ext| self.extensions().contains(&ext)),
(None, Some(interpreters)) => File::open(path).is_ok_and(|mut fh| {
find_interpreter(&mut fh)
.is_some_and(|interpreter| interpreters.contains(&interpreter.as_str()))
}),
_ => false,
}
}
}
pub(crate) fn find_interpreter(source: &mut impl Read) -> Option<String> {
let mut interpreter = String::new();
let mut seen_space = false;
let mut buf = [0u8; 32];
let mut n_char = 0;
'read: while let Ok(n) = source.read(&mut buf) {
let done = n == 0;
let maximum_exceeded = n_char >= 128;
if done || maximum_exceeded {
break;
}
for c in buf.into_iter().take(n) {
n_char += 1;
match (n_char - 1, c as char) {
(0, '#') | (1, '!') => { }
(0 | 1, _) | (_, '\n' | '\r') => break 'read,
(_, '/') => interpreter.clear(),
(_, ' ') if seen_space => break 'read,
(_, ' ') => {
seen_space = true;
interpreter.clear();
}
(_, c) => interpreter.push(c),
}
}
}
(!interpreter.is_empty()).then_some(interpreter)
}
#[cfg(test)]
mod test {
use std::io::Cursor;
use rstest::rstest;
use super::*;
#[rstest]
#[case("", None)]
#[case(&" ".repeat(64), None)]
#[case(&"/x".repeat(1000), None)]
#[case("python", None)]
#[case("[[]]", None)]
#[case("#", None)]
#[case("#!", None)]
#[case("#!/", None)]
#[case("#!/b", Some(String::from("b")))]
#[case("#!/bin/bash", Some(String::from("bash")))]
#[case("#!/bin/bash\n", Some(String::from("bash")))]
#[case("#!/bin/bash\r\n", Some(String::from("bash")))]
#[case("#!/bin/bash\nwhatever", Some(String::from("bash")))]
#[case("#!/bin/bash\r\nwhatever", Some(String::from("bash")))]
#[case("#!/usr/bin/env python\n", Some(String::from("python")))]
#[case("#!/usr/bin/env python\r\n", Some(String::from("python")))]
#[case("#!/usr/bin/env python3\n", Some(String::from("python3")))]
#[case("#!/usr/bin/env python3\r\n", Some(String::from("python3")))]
#[case("#!/usr/bin/env a b c d e", Some(String::from("a")))]
#[case("#!/usr/bin/env a b c d e\n", Some(String::from("a")))]
#[case("#!/usr/bin/env a b c d e\nf", Some(String::from("a")))]
#[case("#!/usr/bin/env a b c d e\r\n", Some(String::from("a")))]
#[case("#!/usr/bin/env a b c d e\r\nf", Some(String::from("a")))]
#[case("#!/usr/bin/env perl -w\n", Some(String::from("perl")))]
#[case("#!/usr/bin/env perl -w\r\n", Some(String::from("perl")))]
#[case(
"#!/some/very/long/path/which/is/not/expected/in/real/life/but/should/still/work/because/why/not/bin/bash",
Some(String::from("bash"))
)]
#[case(
"#!/some/very/long/path/which/is/not/expected/in/real/life/and/will/not/work/because/there/is/a/certain/limit/to/the/nonsense/we/accept/in/this/function/bin/nope",
None
)]
fn test_find_interpreter(#[case] input: &str, #[case] expected: Option<String>) {
assert_eq!(
find_interpreter(&mut Cursor::new(input.as_bytes())),
expected
);
}
}