use std::path::PathBuf;
use purecrypto::hash::sha256;
const SUN_PATH_MAX: usize = 100;
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
s.push(char::from_digit((b & 0xf) as u32, 16).unwrap());
}
s
}
pub fn connection_hash(localhost: &str, host: &str, port: u16, user: &str) -> String {
let material = format!("{localhost}:{host}:{port}:{user}");
let digest = sha256(material.as_bytes());
hex(&digest[..8])
}
pub fn expand_tokens_with_hash(
template: &str,
localhost: &str,
host: &str,
port: u16,
user: &str,
) -> String {
let mut pre = String::with_capacity(template.len());
let mut chars = template.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
pre.push(c);
continue;
}
match chars.peek() {
Some('C') => {
chars.next();
pre.push_str(&connection_hash(localhost, host, port, user));
}
Some('l') => {
chars.next();
pre.push_str(localhost);
}
Some(&other) => {
chars.next();
pre.push('%');
pre.push(other);
}
None => pre.push('%'),
}
}
crate::proc_transport::expand_tokens(&pre, host, port, user)
}
pub fn local_hostname() -> String {
if let Ok(h) = std::env::var("HOSTNAME")
&& !h.is_empty()
{
return h;
}
if let Ok(h) = std::fs::read_to_string("/proc/sys/kernel/hostname") {
let h = h.trim();
if !h.is_empty() {
return h.to_string();
}
}
String::new()
}
pub fn expand_control_path(
template: &str,
localhost: &str,
host: &str,
port: u16,
user: &str,
tilde: impl Fn(&str) -> String,
) -> PathBuf {
let expanded = tilde(&expand_tokens_with_hash(
template, localhost, host, port, user,
));
socket_path_for(&expanded)
}
pub fn socket_path_for(expanded: &str) -> PathBuf {
if expanded.len() <= SUN_PATH_MAX {
return PathBuf::from(expanded);
}
let digest = sha256(expanded.as_bytes());
let short = format!("ssh-mux-{}", hex(&digest[..16]));
let parent = PathBuf::from(expanded)
.parent()
.map(|p| p.to_path_buf())
.filter(|p| !p.as_os_str().is_empty());
match parent {
Some(dir) if dir.as_os_str().len() + 1 + short.len() <= SUN_PATH_MAX => dir.join(short),
_ => std::env::temp_dir().join(short),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn connection_hash_is_stable_and_distinct() {
let a = connection_hash("box", "example.com", 22, "alice");
let b = connection_hash("box", "example.com", 22, "alice");
assert_eq!(a, b, "same tuple ⇒ same hash");
assert_eq!(a.len(), 16);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
let c = connection_hash("box", "example.com", 2222, "alice");
assert_ne!(a, c, "different port ⇒ different hash");
let d = connection_hash("box", "example.com", 22, "bob");
assert_ne!(a, d, "different user ⇒ different hash");
}
#[test]
fn expand_basic_tokens() {
let out = expand_tokens_with_hash("/tmp/cm-%r@%h:%p", "lh", "example.com", 2222, "alice");
assert_eq!(out, "/tmp/cm-alice@example.com:2222");
}
#[test]
fn expand_percent_c_and_l() {
let h = connection_hash("myhost", "srv", 22, "u");
let out = expand_tokens_with_hash("/tmp/%l-%C", "myhost", "srv", 22, "u");
assert_eq!(out, format!("/tmp/myhost-{h}"));
}
#[test]
fn expand_literal_percent() {
let out = expand_tokens_with_hash("100%%-%h", "lh", "h", 22, "u");
assert_eq!(out, "100%-h");
}
#[test]
fn unknown_token_passthrough() {
let out = expand_tokens_with_hash("%z-%h", "lh", "h", 22, "u");
assert_eq!(out, "%z-h");
}
#[test]
fn short_path_unchanged() {
let p = socket_path_for("/tmp/ssh-mux-abc");
assert_eq!(p, PathBuf::from("/tmp/ssh-mux-abc"));
}
#[test]
fn overlong_path_is_hashed_in_same_dir() {
let long_name: String = "x".repeat(200);
let full = format!("/tmp/{long_name}");
let p = socket_path_for(&full);
assert!(p.as_os_str().len() <= SUN_PATH_MAX, "result fits sun_path");
assert_eq!(p.parent().unwrap(), std::path::Path::new("/tmp"));
assert!(
p.file_name()
.unwrap()
.to_str()
.unwrap()
.starts_with("ssh-mux-"),
"hashed name uses ssh-mux- prefix"
);
let other = format!("/tmp/{}", "y".repeat(200));
assert_ne!(p, socket_path_for(&other));
}
#[test]
fn overlong_parent_falls_back_to_tempdir() {
let deep = format!("/{}/sock", "d".repeat(200));
let p = socket_path_for(&deep);
assert!(p.as_os_str().len() <= SUN_PATH_MAX + 64);
assert!(
p.file_name()
.unwrap()
.to_str()
.unwrap()
.starts_with("ssh-mux-")
);
}
}