use std::path::{Path, PathBuf};
use crate::broker::lifecycle::names::{validate_service_name, PipePathError};
const PIPE_PREFIX_V2: &str = "rpb-v2";
#[derive(Debug, thiserror::Error)]
pub enum BrokerPathIdentityError {
#[error("canonicalize installed broker path {path:?}: {source}")]
Canonicalize {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
pub fn broker_path_scope_hash(
broker_path: impl AsRef<Path>,
) -> Result<String, BrokerPathIdentityError> {
let supplied = broker_path.as_ref();
let canonical = std::fs::canonicalize(supplied).map_err(|source| {
BrokerPathIdentityError::Canonicalize {
path: supplied.to_path_buf(),
source,
}
})?;
let mut hasher = blake3::Hasher::new();
hasher.update(b"running-process:broker-install-path:v1\0");
hasher.update(&crate::platform::ipc::endpoint_scope_bytes(&canonical));
let digest = hasher.finalize();
let mut scope = String::with_capacity(16);
for byte in digest.as_bytes().iter().take(8) {
use std::fmt::Write as _;
let _ = write!(scope, "{byte:02x}");
}
Ok(scope)
}
pub fn v2_broker_path_pipe(
program: &str,
broker_path: impl AsRef<Path>,
pipe_idx: u32,
) -> Result<String, BrokerPathPipeError> {
let scope = broker_path_scope_hash(broker_path)?;
Ok(v2_program_pipe(program, &scope, pipe_idx)?)
}
#[derive(Debug, thiserror::Error)]
pub enum BrokerPathPipeError {
#[error(transparent)]
Identity(#[from] BrokerPathIdentityError),
#[error(transparent)]
Pipe(#[from] PipePathError),
}
pub fn v2_program_pipe(
program: &str,
sid_hash: &str,
pipe_idx: u32,
) -> Result<String, PipePathError> {
validate_service_name(program)?;
validate_sid_hash(sid_hash)?;
Ok(format!("{PIPE_PREFIX_V2}-{program}-{sid_hash}-{pipe_idx}"))
}
fn validate_sid_hash(sid_hash: &str) -> Result<(), PipePathError> {
if sid_hash.is_empty() {
return Err(PipePathError::InvalidName {
name: sid_hash.into(),
reason: "sid_hash must be at least 1 character",
});
}
if sid_hash.len() != 16 {
return Err(PipePathError::InvalidName {
name: sid_hash.into(),
reason: "sid_hash must be exactly 16 hex characters",
});
}
for c in sid_hash.chars() {
if !c.is_ascii_hexdigit() || c.is_ascii_uppercase() {
return Err(PipePathError::InvalidName {
name: sid_hash.into(),
reason: "sid_hash must be lowercase hex digits",
});
}
}
Ok(())
}
pub fn broker_v2_runtime_dir() -> std::path::PathBuf {
crate::platform::ipc::broker_v2_runtime_dir()
}
pub fn daemon_identity_path(service: &str) -> std::path::PathBuf {
broker_v2_runtime_dir().join(format!("daemon-{service}.json"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_runtime_dir_is_absolute() {
let dir = broker_v2_runtime_dir();
assert!(dir.is_absolute(), "{} is not absolute", dir.display());
}
#[test]
fn the_runtime_dir_is_stable_across_calls() {
assert_eq!(broker_v2_runtime_dir(), broker_v2_runtime_dir());
}
#[test]
fn the_runtime_dir_is_specific_to_broker_v2() {
let dir = broker_v2_runtime_dir();
let text = dir.to_string_lossy();
assert!(
text.contains("broker-v2"),
"{text} does not name broker-v2, so it may be shared with other components"
);
}
const VALID_SID: &str = "deadbeefcafef00d";
#[test]
fn the_identity_path_is_stable_and_service_specific() {
let a = daemon_identity_path("zccache");
assert_eq!(a, daemon_identity_path("zccache"));
assert_ne!(a, daemon_identity_path("fbuild"));
assert!(a.is_absolute(), "{} is not absolute", a.display());
assert!(a.starts_with(broker_v2_runtime_dir()));
assert!(
a.to_string_lossy().contains("zccache"),
"{} does not name the service",
a.display()
);
}
#[test]
fn v2_program_pipe_happy_path() {
let name =
v2_program_pipe("zccache", VALID_SID, 0).expect("valid inputs produce a v2 pipe name");
assert_eq!(name, "rpb-v2-zccache-deadbeefcafef00d-0");
}
#[test]
fn v2_program_pipe_distinct_pipe_idx_distinct_names() {
let name_0 = v2_program_pipe("zccache", VALID_SID, 0).expect("idx=0 valid");
let name_7 = v2_program_pipe("zccache", VALID_SID, 7).expect("idx=7 valid");
assert_ne!(name_0, name_7);
assert!(name_7.ends_with("-7"));
}
#[test]
fn v2_program_pipe_rejects_invalid_program() {
assert!(matches!(
v2_program_pipe("", VALID_SID, 0),
Err(PipePathError::InvalidName { .. })
));
assert!(matches!(
v2_program_pipe("Zccache", VALID_SID, 0),
Err(PipePathError::InvalidName { .. })
));
let too_long = "a".repeat(65);
assert!(matches!(
v2_program_pipe(&too_long, VALID_SID, 0),
Err(PipePathError::InvalidName { .. })
));
}
#[test]
fn v2_program_pipe_rejects_invalid_sid_hash() {
assert!(matches!(
v2_program_pipe("zccache", "", 0),
Err(PipePathError::InvalidName { .. })
));
assert!(matches!(
v2_program_pipe("zccache", "deadbeefcafef00", 0),
Err(PipePathError::InvalidName { .. })
));
assert!(matches!(
v2_program_pipe("zccache", "deadbeefcafef00g", 0),
Err(PipePathError::InvalidName { .. })
));
assert!(matches!(
v2_program_pipe("zccache", "DEADBEEFCAFEF00D", 0),
Err(PipePathError::InvalidName { .. })
));
}
#[test]
fn broker_path_scope_is_stable_and_path_specific() {
let temp = tempfile::tempdir().expect("tempdir");
let first = temp.path().join("broker-a");
let second = temp.path().join("broker-b");
std::fs::write(&first, b"a").expect("first broker fixture");
std::fs::write(&second, b"b").expect("second broker fixture");
let a = broker_path_scope_hash(&first).expect("first scope");
assert_eq!(a, broker_path_scope_hash(&first).expect("stable scope"));
assert_ne!(a, broker_path_scope_hash(&second).expect("second scope"));
assert_eq!(a.len(), 16);
assert!(a
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn path_scoped_pipe_contains_no_user_identity() {
let temp = tempfile::tempdir().expect("tempdir");
let broker = temp.path().join("soldr");
std::fs::write(&broker, b"broker").expect("broker fixture");
let scope = broker_path_scope_hash(&broker).expect("scope");
let name = v2_broker_path_pipe("soldr-daemon", &broker, 1).expect("pipe");
assert_eq!(name, format!("rpb-v2-soldr-daemon-{scope}-1"));
}
#[test]
fn missing_broker_path_has_no_lexical_fallback() {
let missing = std::env::temp_dir().join(format!(
"running-process-missing-broker-{}",
std::process::id()
));
let err = broker_path_scope_hash(&missing).expect_err("missing path must fail");
assert!(matches!(err, BrokerPathIdentityError::Canonicalize { .. }));
}
}