use super::{ClassifyError, ShellContext, Sink};
use crate::action::AtomicAction;
mod path;
mod runner;
#[cfg(test)]
mod tests;
const LAUNCHERS: &[&str] = &["wsl", "wslg", "wslconfig"];
const DISTRIBUTION_LAUNCHERS: &[&str] = &["ubuntu", "debian", "kali", "fedoraremix"];
const VERSIONED_LAUNCHERS: &[&str] = &["opensuse-", "sles-", "oraclelinux", "almalinux"];
const SYSTEM_DIRS: &[&str] = &["system32", "sysnative", "syswow64"];
const SHARES: &[&str] = &["wsl$", "wsl.localhost"];
const BACKSTOP: &[&str] = &["wsl", "wslg", "wslconfig", "bash"];
pub(super) fn note(
argv: &[String],
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<(), ClassifyError> {
let Some(first) = argv.first() else {
return Ok(());
};
if let Some(launcher) = launcher(first) {
push(sink, "@wsl", &launcher)?;
}
relative(first, ctx, sink)?;
if path::program(first) == "fsutil" && argv.iter().any(|a| a.eq_ignore_ascii_case("hardlink")) {
for launcher in argv[1..].iter().filter_map(|a| launcher_path(a)) {
push(sink, "@wsl", &launcher)?;
}
}
let found = runner::scan(first, &argv[1..]);
for launcher in &found.launchers {
push(sink, "@wsl", launcher)?;
}
if found.opaque {
push(sink, "@opaque-code", &path::program(first))?;
}
if found.share || argv.iter().any(|word| names_share(word)) {
push(sink, "@wsl", "share")?;
}
Ok(())
}
fn relative(first: &str, ctx: &ShellContext<'_>, sink: &mut Sink) -> Result<(), ClassifyError> {
let unified = first.replace('\\', "/");
let explicit = unified.starts_with("./") || unified.starts_with("../");
let name = path::program(first);
if !explicit || !(BACKSTOP.contains(&name.as_str()) || launcher(&name).is_some()) {
return Ok(());
}
match ctx.paths(first) {
Ok(paths) => {
for found in paths.iter().filter_map(|p| launcher(p)) {
push(sink, "@wsl", &found)?;
}
Ok(())
}
Err(_) => push(sink, "@opaque-code", &name),
}
}
fn launcher_path(word: &str) -> Option<String> {
word.contains(['/', '\\']).then(|| launcher(word)).flatten()
}
fn push(sink: &mut Sink, atom: &str, how: &str) -> Result<(), ClassifyError> {
sink.push(AtomicAction::Shell {
argv: vec![atom.to_owned(), how.to_owned()],
})
}
fn launcher(word: &str) -> Option<String> {
let path = path::read(word);
let name = path.program();
let parent = path.parts.iter().rev().nth(1).map(String::as_str);
let system_bash = name == "bash"
&& (parent.is_some_and(|d| SYSTEM_DIRS.contains(&d))
|| path.parts.iter().any(|part| part == "winsxs"));
let is_launcher = LAUNCHERS.contains(&name.as_str())
|| is_short_name(&name, "wslcon")
|| system_bash
|| is_distribution(&name);
is_launcher.then_some(name)
}
fn names_share(word: &str) -> bool {
let value = word.split_once('=').map_or(word, |(_, value)| value);
let option = (word.starts_with('-') || (word.starts_with('/') && !word.starts_with("//")))
.then(|| word.split_once(':').map(|(_, value)| value))
.flatten()
.unwrap_or(word);
[word, value, option].into_iter().any(|word| {
let unified = word.replace('\\', "/");
let after_scheme = unified
.get(..5)
.filter(|scheme| scheme.eq_ignore_ascii_case("file:"))
.map_or(unified.as_str(), |_| &unified[5..]);
after_scheme.starts_with("//")
&& path::read(after_scheme)
.parts
.iter()
.any(|part| SHARES.contains(&part.as_str()))
})
}
fn is_short_name(name: &str, prefix: &str) -> bool {
name.strip_prefix(prefix)
.and_then(|rest| rest.strip_prefix('~'))
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
}
fn is_distribution(name: &str) -> bool {
let versioned_ubuntu = name
.strip_prefix("ubuntu")
.is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()));
DISTRIBUTION_LAUNCHERS.contains(&name)
|| versioned_ubuntu
|| is_short_name(name, "ubuntu")
|| VERSIONED_LAUNCHERS.iter().any(|prefix| {
name.strip_prefix(prefix)
.is_some_and(|rest| !rest.is_empty())
})
}