use std::path::PathBuf;
pub use crate::daemon_registration::validation::{
validate_service_name, validate_version, PipePathError,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PipePath {
pub windows: Option<String>,
pub unix: Option<PathBuf>,
}
pub const WINDOWS_MAX_PATH: usize = 260;
pub const MACOS_SUN_PATH_MAX: usize = 104;
pub const LINUX_SUN_PATH_MAX: usize = 108;
const PIPE_PREFIX: &str = "rpb-v1";
pub fn shared_broker_pipe(user_sid_hash: &str) -> Result<PipePath, PipePathError> {
validate_sid_hash(user_sid_hash)?;
build_pipe_path(&format!("{PIPE_PREFIX}-{user_sid_hash}-shared"))
}
pub fn private_broker_pipe(user_sid_hash: &str, service: &str) -> Result<PipePath, PipePathError> {
validate_sid_hash(user_sid_hash)?;
validate_service_name(service)?;
build_pipe_path(&format!("{PIPE_PREFIX}-{user_sid_hash}-svc-{service}"))
}
pub fn explicit_instance_pipe(user_sid_hash: &str, name: &str) -> Result<PipePath, PipePathError> {
validate_sid_hash(user_sid_hash)?;
validate_service_name(name)?; build_pipe_path(&format!("{PIPE_PREFIX}-{user_sid_hash}-inst-{name}"))
}
pub fn backend_pipe(user_sid_hash: &str, random128: &[u8; 16]) -> Result<PipePath, PipePathError> {
validate_sid_hash(user_sid_hash)?;
let mut suffix = String::with_capacity(32);
for b in random128 {
suffix.push(nibble_to_hex(b >> 4));
suffix.push(nibble_to_hex(b & 0x0F));
}
build_pipe_path(&format!("{PIPE_PREFIX}-{user_sid_hash}-be-{suffix}"))
}
fn validate_sid_hash(s: &str) -> Result<(), PipePathError> {
if s.len() != 16 {
return Err(PipePathError::InvalidName {
name: s.into(),
reason: "user_sid_hash must be exactly 16 hex characters",
});
}
for c in s.chars() {
if !(c.is_ascii_digit() || ('a'..='f').contains(&c)) {
return Err(PipePathError::InvalidName {
name: s.into(),
reason: "user_sid_hash must be lowercase hex",
});
}
}
Ok(())
}
#[inline]
fn nibble_to_hex(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'a' + (n - 10)) as char,
_ => unreachable!("nibble out of range"),
}
}
fn build_pipe_path(name: &str) -> Result<PipePath, PipePathError> {
let address = crate::platform::ipc::broker_v1_endpoint_path(name).map_err(|err| {
PipePathError::PathTooLong {
len: err.len,
max: err.max,
limit_label: err.limit_label,
}
})?;
Ok(if crate::platform::ipc::endpoint_is_filesystem_backed() {
PipePath {
windows: None,
unix: Some(PathBuf::from(address)),
}
} else {
PipePath {
windows: Some(address),
unix: None,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_HASH: &str = "0123456789abcdef";
fn sole_address(path: &PipePath) -> String {
match (&path.windows, &path.unix) {
(Some(windows), None) => windows.clone(),
(None, Some(unix)) => unix.to_string_lossy().into_owned(),
_ => panic!("exactly one form must be populated"),
}
}
#[test]
fn shared_broker_pipe_builds() {
let path = shared_broker_pipe(SAMPLE_HASH).expect("shared pipe should build");
assert!(!sole_address(&path).is_empty());
}
#[test]
fn populated_form_follows_the_selected_transport() {
let path = shared_broker_pipe(SAMPLE_HASH).expect("shared pipe should build");
assert_eq!(
path.unix.is_some(),
crate::platform::ipc::endpoint_is_filesystem_backed(),
"filesystem-backed hosts must populate the unix form and no other"
);
}
#[test]
fn the_derived_address_respects_the_host_budget() {
let limit = crate::platform::ipc::endpoint_name_limit();
let path = backend_pipe(SAMPLE_HASH, &[0xABu8; 16]).expect("backend pipe");
assert!(
sole_address(&path).len() <= limit.max_bytes,
"derived address exceeds the {} budget of {} bytes",
limit.label,
limit.max_bytes
);
}
#[test]
fn the_host_budget_is_one_of_the_documented_ceilings() {
let limit = crate::platform::ipc::endpoint_name_limit();
assert!(
matches!(
(limit.max_bytes, limit.label),
(WINDOWS_MAX_PATH, "Windows MAX_PATH")
| (MACOS_SUN_PATH_MAX, "macOS sun_path")
| (LINUX_SUN_PATH_MAX, "Linux sun_path")
),
"facade reported {} / {} bytes, which matches no documented ceiling",
limit.label,
limit.max_bytes
);
}
#[test]
fn private_broker_pipe_rejects_uppercase() {
let err = private_broker_pipe(SAMPLE_HASH, "Zccache").unwrap_err();
match err {
PipePathError::InvalidName { .. } => {}
_ => panic!("expected InvalidName, got {err:?}"),
}
}
#[test]
fn validate_version_accepts_semver() {
validate_version("1.0.0").unwrap();
validate_version("1.11.20").unwrap();
validate_version("0.0.1-alpha.1").unwrap();
validate_version("2.3.4-rc.1.beta").unwrap();
}
#[test]
fn validate_version_rejects_invalid() {
assert!(validate_version("").is_err());
assert!(validate_version("1.0").is_err());
assert!(validate_version("1.0.0.0").is_err());
assert!(validate_version("1.0.0-").is_err());
assert!(validate_version("1.0.0-ALPHA").is_err()); assert!(validate_version("v1.0.0").is_err());
}
#[test]
fn backend_pipes_are_distinct_per_random_suffix() {
let first = backend_pipe(SAMPLE_HASH, &[0xABu8; 16]).expect("backend pipe");
let second = backend_pipe(SAMPLE_HASH, &[0xCDu8; 16]).expect("backend pipe");
assert_ne!(sole_address(&first), sole_address(&second));
}
}