use std::path::{Path, PathBuf};
use cageforge_path::contains_parent_traversal;
use crate::PolicyError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct AbsolutePath(PathBuf);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NamedPipeName(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum LocalIpcEndpoint {
UnixSocket(AbsolutePath),
WindowsNamedPipe(NamedPipeName),
}
impl AbsolutePath {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
let path = path.into();
let value = path.to_string_lossy();
if value.is_empty() {
return Err(PolicyError::EmptyPath);
}
if value.contains('\0') {
return Err(PolicyError::PathContainsNul { path });
}
if !value.starts_with('/') {
return Err(PolicyError::ExpectedAbsolute { path });
}
if contains_parent_traversal(Path::new(value.as_ref())) {
return Err(PolicyError::ParentTraversal { path });
}
Ok(Self(path))
}
pub fn as_path(&self) -> &Path {
&self.0
}
pub fn into_path(self) -> PathBuf {
self.0
}
}
impl NamedPipeName {
pub fn new(value: impl Into<String>) -> Result<Self, PolicyError> {
let value = value.into();
const PREFIX: &str = "\\\\.\\pipe\\";
if !value
.get(..PREFIX.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(PREFIX))
{
return Err(PolicyError::InvalidLocalIpcEndpoint {
endpoint: value,
reason: "named pipe must use the local \\\\.\\pipe\\ namespace",
});
}
let name = &value[PREFIX.len()..];
if name.is_empty()
|| name.contains('\0')
|| name.contains('/')
|| name.contains('\\')
|| name == "."
|| name == ".."
|| name.contains(':')
|| !name.is_ascii()
{
return Err(PolicyError::InvalidLocalIpcEndpoint {
endpoint: value,
reason: "named pipe name is empty or contains an unsafe component",
});
}
if value.encode_utf16().count() > 256 {
return Err(PolicyError::InvalidLocalIpcEndpoint {
endpoint: value,
reason: "named pipe name exceeds the supported length",
});
}
Ok(Self(format!("{PREFIX}{}", name.to_ascii_lowercase())))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl LocalIpcEndpoint {
pub fn unix_socket(path: impl Into<PathBuf>) -> Result<Self, PolicyError> {
Ok(Self::UnixSocket(AbsolutePath::new(path)?))
}
pub fn windows_named_pipe(name: impl Into<String>) -> Result<Self, PolicyError> {
Ok(Self::WindowsNamedPipe(NamedPipeName::new(name)?))
}
pub fn unix_path(&self) -> Option<&Path> {
match self {
Self::UnixSocket(path) => Some(path.as_path()),
Self::WindowsNamedPipe(_) => None,
}
}
pub fn named_pipe(&self) -> Option<&str> {
match self {
Self::UnixSocket(_) => None,
Self::WindowsNamedPipe(name) => Some(name.as_str()),
}
}
pub fn validate(&self) -> Result<(), PolicyError> {
match self {
Self::UnixSocket(path) => {
if contains_parent_traversal(path.as_path()) {
return Err(PolicyError::InvalidLocalIpcEndpoint {
endpoint: path.as_path().display().to_string(),
reason: "parent traversal is not allowed",
});
}
Ok(())
}
Self::WindowsNamedPipe(name) => NamedPipeName::new(name.as_str()).map(|_| ()),
}
}
}