Skip to main content

perl_subprocess_runtime/
lib.rs

1//! Subprocess execution abstraction for provider purity
2//!
3//! This crate provides a trait-based abstraction for subprocess execution,
4//! enabling testing with mock implementations and WASM compatibility.
5
6#![deny(unsafe_code)]
7#![cfg_attr(test, allow(clippy::panic, clippy::unwrap_used, clippy::expect_used))]
8#![warn(rust_2018_idioms)]
9#![warn(missing_docs)]
10
11mod error;
12#[cfg(not(target_arch = "wasm32"))]
13mod os_runtime;
14mod output;
15
16/// Mock subprocess runtime implementations for tests.
17pub mod mock;
18
19pub use error::SubprocessError;
20#[cfg(not(target_arch = "wasm32"))]
21pub use os_runtime::OsSubprocessRuntime;
22pub use output::SubprocessOutput;
23
24/// Abstraction trait for subprocess execution.
25pub trait SubprocessRuntime: Send + Sync {
26    /// Execute a command with the given arguments and optional stdin.
27    fn run_command(
28        &self,
29        program: &str,
30        args: &[&str],
31        stdin: Option<&[u8]>,
32    ) -> Result<SubprocessOutput, SubprocessError>;
33}
34
35/// Resolve a bare program name to an absolute path that is safe to pass to
36/// `std::process::Command::new`.
37///
38/// On Windows, `Command::new(bare_name)` triggers CreateProcess's CWD-first
39/// executable search — a binary planted in the LSP workspace root would run
40/// instead of the legitimate tool (binary-planting RCE, #2764/#3028).  This
41/// function delegates to the already-hardened `resolve_windows_program` resolver
42/// which searches only absolute `PATH` directories and excludes the CWD.
43///
44/// # Return value
45///
46/// - **Windows**: returns the absolute path of the resolved binary, or
47///   `Err(SubprocessError)` if the program is not found in any absolute `PATH`
48///   directory (fail closed — refusing to run is safer than running a planted
49///   binary).
50/// - **Non-Windows**: CreateProcess CWD-search semantics do not apply; returns
51///   `Ok(program.to_string())` unchanged so callers can use this function
52///   unconditionally without `#[cfg(windows)]` guards.
53///
54/// # Usage
55///
56/// ```ignore
57/// let resolved = perl_subprocess_runtime::resolve_program("yath")?;
58/// let mut cmd = std::process::Command::new(resolved);
59/// ```
60#[cfg(not(target_arch = "wasm32"))]
61pub fn resolve_program(program: &str) -> Result<String, SubprocessError> {
62    #[cfg(windows)]
63    {
64        os_runtime::resolve_windows_program_pub(program).ok_or_else(|| {
65            SubprocessError::new(format!(
66                "command not found in any absolute PATH directory \
67                 (current directory excluded for security): {program}"
68            ))
69        })
70    }
71    #[cfg(not(windows))]
72    {
73        Ok(program.to_string())
74    }
75}
76
77#[cfg(test)]
78mod tests;