use crate::{ServiceIdentity, socket_path::SocketName};
use std::io;
pub const MAX_PIPE_NAME_UNITS: usize = 247;
#[cfg(windows)]
pub fn for_current_user(identity: &ServiceIdentity, name: SocketName) -> io::Result<String> {
user_pipe_name(&super::windows::current_user_sid()?, identity, name)
}
fn user_pipe_name(sid: &str, identity: &ServiceIdentity, name: SocketName) -> io::Result<String> {
let pipe = format!(
"{sid}_service-{:032x}_profile-{}_instance-{}_{}",
u128::from_be_bytes(*identity.id()),
identity.profile(),
identity.instance(),
name.as_str()
);
let units = pipe.encode_utf16().count();
if units > MAX_PIPE_NAME_UNITS {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"pipe name is {units} characters, but a named pipe name can be at most \
{MAX_PIPE_NAME_UNITS}: {pipe}"
),
));
}
Ok(pipe)
}
#[cfg(test)]
mod tests {
use super::*;
const DAEMON: SocketName = SocketName::new("daemon.sock");
const SID: &str = "S-1-5-21-3623811015-3361044348-30300820-1013";
fn identity(id: u8, profile: &str, instance: &str) -> ServiceIdentity {
ServiceIdentity::new([id; 16], "example.service", profile, instance).unwrap()
}
#[test]
fn a_name_carries_the_user_the_service_and_the_endpoint() {
assert_eq!(
user_pipe_name(SID, &identity(0xab, "stable", "default"), DAEMON).unwrap(),
format!(
"{SID}_service-{}_profile-stable_instance-default_daemon.sock",
"ab".repeat(16)
)
);
}
#[test]
fn users_services_profiles_instances_and_endpoints_get_disjoint_names() {
let other = SocketName::new("daemon.ctl");
let names = [
user_pipe_name(SID, &identity(1, "stable", "default"), DAEMON),
user_pipe_name(
"S-1-5-21-1-2-3-1001",
&identity(1, "stable", "default"),
DAEMON,
),
user_pipe_name(SID, &identity(2, "stable", "default"), DAEMON),
user_pipe_name(SID, &identity(1, "dev", "default"), DAEMON),
user_pipe_name(SID, &identity(1, "stable", "second"), DAEMON),
user_pipe_name(SID, &identity(1, "stable", "default"), other),
user_pipe_name(SID, &identity(1, "a-instance-b", "c"), DAEMON),
user_pipe_name(SID, &identity(1, "a", "b-instance-c"), DAEMON),
]
.map(Result::unwrap);
let distinct: std::collections::HashSet<_> = names.iter().collect();
assert_eq!(distinct.len(), names.len(), "{names:#?}");
}
#[test]
fn a_name_at_the_limit_fits_and_one_unit_more_does_not() {
let identity = identity(1, "stable", "default");
let rest = user_pipe_name("", &identity, DAEMON).unwrap().len();
let at_limit = "S".repeat(MAX_PIPE_NAME_UNITS - rest);
let fits = user_pipe_name(&at_limit, &identity, DAEMON).unwrap();
assert_eq!(fits.encode_utf16().count(), MAX_PIPE_NAME_UNITS);
let error = user_pipe_name(&format!("{at_limit}S"), &identity, DAEMON).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
let text = error.to_string();
assert!(
text.contains(&format!("{} characters", MAX_PIPE_NAME_UNITS + 1)),
"{text}"
);
assert!(
text.contains(&format!("at most {MAX_PIPE_NAME_UNITS}")),
"{text}"
);
}
#[test]
fn the_limit_counts_utf16_units_not_bytes() {
let identity = identity(1, "stable", "default");
let wide = SocketName::new("é");
let rest = user_pipe_name("", &identity, wide).unwrap();
let sid = "S".repeat(MAX_PIPE_NAME_UNITS - rest.encode_utf16().count());
let name = user_pipe_name(&sid, &identity, wide).unwrap();
assert!(name.len() > MAX_PIPE_NAME_UNITS);
}
#[cfg(windows)]
fn whoami_sid() -> String {
let output = std::process::Command::new("whoami")
.args(["/user", "/fo", "csv", "/nh"])
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
let line = output.stdout.trim_ascii();
let field = line.rsplit(|&byte| byte == b',').next().unwrap();
let sid = std::str::from_utf8(field.trim_ascii())
.unwrap()
.trim_matches('"');
assert!(
sid.starts_with("S-1-"),
"{}",
String::from_utf8_lossy(&output.stdout)
);
sid.to_owned()
}
#[cfg(windows)]
#[test]
fn the_current_users_name_starts_with_the_sid_of_this_process() {
let identity = identity(1, "stable", "default");
let name = for_current_user(&identity, DAEMON).unwrap();
assert!(name.starts_with(&format!("{}_", whoami_sid())), "{name}");
assert_eq!(
name,
user_pipe_name(&whoami_sid(), &identity, DAEMON).unwrap()
);
}
#[cfg(windows)]
#[test]
fn windows_creates_a_pipe_whose_name_is_at_the_limit() {
use crate::local::{Bound, socket::acquire};
let unique = format!("{}-", std::process::id());
let at_limit = format!("{unique}{}", "a".repeat(MAX_PIPE_NAME_UNITS - unique.len()));
assert!(matches!(
acquire(std::path::Path::new(&at_limit)).unwrap(),
Bound::Won(_)
));
}
#[cfg(windows)]
#[test]
fn a_service_binds_and_reaches_its_user_scoped_pipe() {
use crate::local::{Bound, Duplex, PlatformDuplex, peer::PeerCredentials, socket::acquire};
let unique = format!("test{}", std::process::id());
let name = for_current_user(&identity(7, "stable", &unique), DAEMON).unwrap();
let Bound::Won(_listener) = acquire(std::path::Path::new(&name)).unwrap() else {
panic!("first owner of {name}")
};
let client = PlatformDuplex::connect(&name).unwrap();
assert!(client.credentials().unwrap().same_user);
}
}