use std::collections::HashSet;
use crate::broker::lifecycle::names::{backend_pipe, PipePath, PipePathError};
use crate::broker::protocol::Endpoint;
pub const DEFAULT_BACKEND_ENDPOINT_ATTEMPTS: usize = 16;
#[derive(Debug)]
pub struct BackendEndpointAllocator {
user_sid_hash: String,
namespace_id: String,
max_attempts: usize,
reserved_paths: HashSet<String>,
}
impl BackendEndpointAllocator {
pub fn new(user_sid_hash: impl Into<String>, namespace_id: impl Into<String>) -> Self {
Self {
user_sid_hash: user_sid_hash.into(),
namespace_id: namespace_id.into(),
max_attempts: DEFAULT_BACKEND_ENDPOINT_ATTEMPTS,
reserved_paths: HashSet::new(),
}
}
pub fn with_max_attempts(mut self, max_attempts: usize) -> Self {
self.max_attempts = max_attempts.max(1);
self
}
pub fn reserve_path(&mut self, path: impl Into<String>) {
self.reserved_paths.insert(path.into());
}
pub fn allocate(&mut self) -> Result<Endpoint, BackendEndpointAllocatorError> {
self.allocate_with_random128(|| {
let mut bytes = [0_u8; 16];
getrandom::fill(&mut bytes)?;
Ok(bytes)
})
}
pub fn allocate_with_random128<F>(
&mut self,
mut next_random128: F,
) -> Result<Endpoint, BackendEndpointAllocatorError>
where
F: FnMut() -> Result<[u8; 16], BackendEndpointAllocatorError>,
{
for _ in 0..self.max_attempts {
let random128 = next_random128()?;
let path = endpoint_path(backend_pipe(&self.user_sid_hash, &random128)?)?;
if self.reserved_paths.insert(path.clone()) {
return Ok(Endpoint {
namespace_id: self.namespace_id.clone(),
path,
});
}
}
Err(BackendEndpointAllocatorError::CollisionExhausted {
attempts: self.max_attempts,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum BackendEndpointAllocatorError {
#[error("backend endpoint random generation failed: {0}")]
Random(String),
#[error(transparent)]
PipePath(#[from] PipePathError),
#[error("backend pipe path did not contain the current platform variant")]
MissingPlatformPath,
#[error("backend endpoint allocation exhausted after {attempts} collision attempts")]
CollisionExhausted {
attempts: usize,
},
}
impl From<getrandom::Error> for BackendEndpointAllocatorError {
fn from(value: getrandom::Error) -> Self {
Self::Random(value.to_string())
}
}
fn endpoint_path(pipe_path: PipePath) -> Result<String, BackendEndpointAllocatorError> {
#[cfg(windows)]
{
pipe_path
.windows
.ok_or(BackendEndpointAllocatorError::MissingPlatformPath)
}
#[cfg(unix)]
{
pipe_path
.unix
.map(|path| path.to_string_lossy().into_owned())
.ok_or(BackendEndpointAllocatorError::MissingPlatformPath)
}
}