mod line;
mod options;
mod simple;
use options::{Kind, Opt};
const PREFIXES: &[&str] = &["call", "."];
const EVALUATORS: &[&str] = &[
"iex",
"invoke-expression",
"icm",
"invoke-command",
"gcm",
"get-command",
];
const LINKERS: &[&str] = &["mklink", "new-item", "ni"];
#[derive(Debug, Default, PartialEq, Eq)]
pub(super) struct Found {
pub(super) launchers: Vec<String>,
pub(super) share: bool,
pub(super) opaque: bool,
}
#[derive(Debug, PartialEq, Eq)]
enum Tok {
Word(String),
Call,
Open,
Sep,
}
pub(super) fn scan(program: &str, args: &[String]) -> Found {
let name = super::path::program(program);
let mut found = Found::default();
let Some(kind) = Kind::of(&name) else {
return found;
};
let line = args.join(" ");
found.opaque = name == "wt" || names_launcher(&line);
let (tokens, subexpression) = tokens(&line, kind.single_quotes());
found.opaque |= subexpression || line::opaque(&line, kind);
let mut state = State::new(kind, !kind.single_quotes());
for token in tokens {
match token {
Tok::Word(word) => state.word(&word, &mut found),
Tok::Call => state.segment(true),
Tok::Open => {
found.opaque |= state.called || (state.launching() && state.next == Next::Program);
state.segment(false);
}
Tok::Sep => state.segment(false),
}
}
found
}
fn names_launcher(line: &str) -> bool {
line.split(|c: char| !(c.is_ascii_alphanumeric() || ".~$:_-".contains(c)))
.map(super::path::program)
.any(|name| {
super::BACKSTOP.contains(&name.as_str()) || super::is_short_name(&name, "wslcon")
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Next {
Program,
Value,
Arguments,
Done,
}
struct State {
kind: Kind,
cmd_line: bool,
next: Next,
called: bool,
links: bool,
}
impl State {
fn new(kind: Kind, cmd_line: bool) -> Self {
Self {
kind,
cmd_line,
next: Next::Program,
called: false,
links: false,
}
}
fn launching(&self) -> bool {
matches!(self.kind, Kind::Start | Kind::StartProcess | Kind::Launcher)
}
fn segment(&mut self, called: bool) {
*self = Self {
called,
..Self::new(Kind::Plain, self.cmd_line)
};
}
fn word(&mut self, word: &str, found: &mut Found) {
found.share |= super::names_share(word);
found.opaque |= built_unc(word) || wildcard_launcher_path(word);
if self.links {
found.launchers.extend(super::launcher_path(word));
}
match self.next {
Next::Value => {
self.next = Next::Program;
return;
}
Next::Done => return,
Next::Program | Next::Arguments => {}
}
match self.kind.option(word) {
Some(Opt::Flag) => self.next = Next::Program,
Some(Opt::Value) => self.next = Next::Value,
Some(Opt::Arguments) => self.next = Next::Arguments,
Some(Opt::Opaque) => found.opaque = true,
Some(Opt::Glued(rest)) => self.program(&rest, found),
None if self.next == Next::Arguments => {}
None => self.program(word, found),
}
}
fn program(&mut self, word: &str, found: &mut Found) {
let word = if self.cmd_line {
word.trim_matches(['\'', '`'])
} else {
word
};
let program = super::path::program(word);
if PREFIXES.contains(&word) || PREFIXES.contains(&program.as_str()) {
self.called |= word == ".";
return;
}
if let Some(launcher) = super::launcher(word) {
found.launchers.push(launcher);
} else if program == "bash" && super::path::read(word).parts.len() == 1 {
found.launchers.push(program.clone());
}
let computed = (self.called || self.launching()) && word.contains('$');
let wildcard = word != "?" && word.contains(['*', '?']);
found.opaque |= computed
|| wildcard
|| built_at_run_time(word)
|| EVALUATORS.contains(&program.as_str())
|| word.to_ascii_lowercase().contains("[scriptblock]");
*self = Self::new(Kind::Plain, self.cmd_line);
self.next = Next::Done;
if let Some(kind) = Kind::of(&program) {
found.opaque |= program == "wt";
*self = Self::new(kind, self.cmd_line);
} else if LINKERS.contains(&program.as_str()) {
self.links = true;
}
}
}
fn wildcard_launcher_path(word: &str) -> bool {
let lower = word.replace('\\', "/").to_ascii_lowercase();
(lower.contains("windows/") || lower.contains("program files/wsl"))
&& lower.contains(['*', '?'])
}
fn built_at_run_time(word: &str) -> bool {
word.contains(['^', '%', '!', '+', '`'])
}
fn built_unc(word: &str) -> bool {
let unc = word.starts_with("\\\\") || word.starts_with("//");
unc && (word.contains(['^', '%', '!', '+', '`']) || word.contains("$("))
}
fn tokens(line: &str, single_quotes: bool) -> (Vec<Tok>, bool) {
let mut out = Vec::new();
let mut word = String::new();
let mut in_word = false;
let mut quote: Option<char> = None;
let mut subexpression = false;
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
match quote {
Some(q) if c == q => quote = None,
Some(q) => {
subexpression |=
single_quotes && q == '"' && c == '$' && chars.peek() == Some(&'(');
word.push(c);
}
None => match c {
'"' => {
quote = Some(c);
in_word = true;
}
'\'' if single_quotes => {
quote = Some(c);
in_word = true;
}
'&' | '(' | '|' | ';' | ')' | '{' | '}' | '\n' => {
flush(&mut word, &mut in_word, &mut out);
out.push(match c {
'&' => Tok::Call,
'(' => Tok::Open,
_ => Tok::Sep,
});
}
c if c.is_whitespace() => flush(&mut word, &mut in_word, &mut out),
c => {
word.push(c);
in_word = true;
}
},
}
}
flush(&mut word, &mut in_word, &mut out);
(out, subexpression)
}
fn flush(word: &mut String, in_word: &mut bool, out: &mut Vec<Tok>) {
if *in_word && !word.is_empty() {
out.push(Tok::Word(std::mem::take(word)));
}
word.clear();
*in_word = false;
}