use std::ffi::OsStr;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use crate::bridge::{WorkspaceRoots, lexically_normalize};
use crate::config::{HostOs, ServerCommand};
use crate::error::UntrustedRefusal;
use crate::lsp::ChildWorkingDir;
#[derive(Debug, Clone)]
pub struct UntrustedBoundary(WorkspaceRoots);
impl UntrustedBoundary {
pub const fn new(roots: WorkspaceRoots) -> Self {
Self(roots)
}
pub const fn roots(&self) -> &WorkspaceRoots {
&self.0
}
pub fn refuse_arguments(
&self,
args: &[String],
working_dir: &ChildWorkingDir,
host: HostOs,
) -> Result<(), UntrustedRefusal> {
for (index, arg) in args.iter().enumerate() {
let after_equals = arg.split_once('=').map(|(_, value)| value);
let candidates = std::iter::once(arg.as_str()).chain(after_equals);
for candidate in candidates.filter(|candidate| !candidate.is_empty()) {
if let Some(refusal) =
argument_refusal(index, candidate, &self.0, working_dir, host)
{
return Err(refusal);
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct UntrustedVetting {
boundary: UntrustedBoundary,
configured: ServerCommand,
}
impl UntrustedVetting {
pub const fn new(boundary: WorkspaceRoots, configured: ServerCommand) -> Self {
Self {
boundary: UntrustedBoundary::new(boundary),
configured,
}
}
pub const fn boundary(&self) -> &UntrustedBoundary {
&self.boundary
}
pub const fn configured(&self) -> &ServerCommand {
&self.configured
}
}
const WINDOWS_EXECUTABLE_EXTENSIONS: [&str; 4] = ["exe", "com", "bat", "cmd"];
pub fn is_started_as_program(host: HostOs, path: &Path) -> bool {
host.is_executable_file(path)
&& (host == HostOs::Other
|| path
.extension()
.and_then(OsStr::to_str)
.is_some_and(|extension| {
WINDOWS_EXECUTABLE_EXTENSIONS
.iter()
.any(|known| extension.eq_ignore_ascii_case(known))
}))
}
fn argument_refusal(
index: usize,
candidate: &str,
boundary: &WorkspaceRoots,
working_dir: &ChildWorkingDir,
host: HostOs,
) -> Option<UntrustedRefusal> {
let resolved = match working_dir {
ChildWorkingDir::Fixed(dir) => dir.join(candidate),
ChildWorkingDir::Inherit => PathBuf::from(candidate),
};
let siblings = (host == HostOs::Windows && resolved.extension().is_none())
.then(|| {
WINDOWS_EXECUTABLE_EXTENSIONS
.iter()
.map(|extension| resolved.with_added_extension(extension))
})
.into_iter()
.flatten();
std::iter::once(resolved.clone())
.chain(siblings)
.find_map(|path| path_refusal(index, &path, boundary, host))
}
fn path_refusal(
index: usize,
resolved: &Path,
boundary: &WorkspaceRoots,
host: HostOs,
) -> Option<UntrustedRefusal> {
match dunce::canonicalize(resolved) {
Ok(canonical) => (is_started_as_program(host, &canonical)
&& boundary.contains_canonical(&canonical))
.then_some(UntrustedRefusal::WorkspaceExecutableArgument {
index,
executable: canonical,
}),
Err(error) if error.kind() == ErrorKind::PermissionDenied => {
let path = lexically_normalize(resolved);
boundary
.admits_lexically(&path)
.then_some(UntrustedRefusal::UnreadableWorkspaceArgument { index, path })
}
Err(_) => None,
}
}