use std::ffi::{OsStr, OsString};
use super::{AllowedPath, SandboxEnforcer, SandboxPolicy, WrappedCommand};
use crate::error::SandboxError;
pub struct WindowsEnforcer;
impl WindowsEnforcer {
pub fn new() -> Self {
Self
}
}
impl Default for WindowsEnforcer {
fn default() -> Self {
Self::new()
}
}
impl SandboxEnforcer for WindowsEnforcer {
fn name(&self) -> &str {
"appcontainer"
}
fn probe(&self) -> Result<(), SandboxError> {
#[cfg(target_os = "windows")]
{
use windows_sys::Win32::Security::CreateAppContainerProfile;
let _ = CreateAppContainerProfile as usize;
return Ok(());
}
#[cfg(not(target_os = "windows"))]
Err(SandboxError::EnforcerUnavailable {
enforcer: "appcontainer".to_string(),
message: "AppContainer is only available on Windows 8 or later.".to_string(),
})
}
fn wrap_command(
&self,
program: &OsStr,
args: &[OsString],
policy: &SandboxPolicy,
) -> Result<WrappedCommand, SandboxError> {
canonicalize_paths(&policy.allowed_paths)?;
Ok(WrappedCommand { program: program.to_owned(), args: args.to_vec() })
}
fn configure_command(
&self,
_cmd: &mut tokio::process::Command,
_policy: &SandboxPolicy,
) -> Result<(), SandboxError> {
#[cfg(target_os = "windows")]
{
return Err(SandboxError::EnforcerFailed {
enforcer: "appcontainer".to_string(),
message: "Windows AppContainer configuration not yet implemented. \
The enforcer structure is in place; Win32 API calls \
will be added in a Windows-specific implementation pass."
.to_string(),
});
}
#[cfg(not(target_os = "windows"))]
Err(SandboxError::EnforcerUnavailable {
enforcer: "appcontainer".to_string(),
message: "AppContainer is only available on Windows.".to_string(),
})
}
}
fn canonicalize_paths(paths: &[AllowedPath]) -> Result<Vec<AllowedPath>, SandboxError> {
let mut result = Vec::with_capacity(paths.len());
for entry in paths {
let canonical = std::fs::canonicalize(&entry.path).map_err(|e| {
SandboxError::PolicyViolation(format!(
"failed to canonicalize allowed path '{}': {e}",
entry.path.display()
))
})?;
if canonical != entry.path {
tracing::warn!(
original = %entry.path.display(),
resolved = %canonical.display(),
"allowed path resolved to a different location (possible symlink)"
);
}
result.push(AllowedPath { path: canonical, mode: entry.mode });
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_name() {
let enforcer = WindowsEnforcer::new();
assert_eq!(enforcer.name(), "appcontainer");
}
#[test]
fn test_probe_fails_on_non_windows() {
let enforcer = WindowsEnforcer::new();
let result = enforcer.probe();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, SandboxError::EnforcerUnavailable { .. }),
"expected EnforcerUnavailable, got: {err:?}"
);
}
#[test]
fn test_wrap_command_returns_original_program() {
let enforcer = WindowsEnforcer::new();
let policy = SandboxPolicy {
allowed_paths: vec![],
allow_network: false,
network_rules: vec![],
allow_process_spawn: false,
env: std::collections::HashMap::new(),
};
let wrapped = enforcer
.wrap_command(
OsStr::new("python.exe"),
&[OsString::from("-c"), OsString::from("print(1)")],
&policy,
)
.unwrap();
assert_eq!(wrapped.program, OsString::from("python.exe"));
assert_eq!(wrapped.args.len(), 2);
assert_eq!(wrapped.args[0], OsString::from("-c"));
assert_eq!(wrapped.args[1], OsString::from("print(1)"));
}
}