use std::ffi::OsString;
use std::io;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
const MAX_UNIX_SOCKET_PATH: usize = 103;
extern "C" {
fn geteuid() -> u32;
}
pub fn socket_path(mount_path: impl AsRef<Path>) -> Result<PathBuf> {
let mount_path = mount_path.as_ref();
if mount_path.as_os_str().is_empty() {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidInput,
"socket_path: empty mount path",
)));
}
let resolved = std::fs::canonicalize(mount_path).ok();
let mount_path = resolved.as_deref().unwrap_or(mount_path);
let name = socket_name(mount_path.as_os_str().as_bytes());
let preferred = clean(&sockets_dir().join(&name));
if preferred.as_os_str().len() <= MAX_UNIX_SOCKET_PATH {
return Ok(preferred);
}
let euid = unsafe { geteuid() };
Ok(Path::new("/tmp").join(format!("mountos-{euid}")).join(name))
}
fn socket_name(mount_path: &[u8]) -> String {
use std::fmt::Write;
let sum = sha256(mount_path);
let mut name = String::from("mountos_api_");
for b in &sum[..8] {
let _ = write!(name, "{b:02x}");
}
name.push_str(".sock");
name
}
fn sockets_dir() -> PathBuf {
let home = std::env::var_os("HOME")
.filter(|h| !h.is_empty())
.or_else(passwd_home)
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
home.join(".mountOS").join("sockets")
}
fn passwd_home() -> Option<OsString> {
let uid = unsafe { getuid() }.to_string();
let data = std::fs::read(Path::new("/etc/passwd")).ok()?;
for line in data.split(|&b| b == b'\n') {
let f: Vec<&[u8]> = line.split(|&b| b == b':').collect();
if f.len() >= 6 && f[2] == uid.as_bytes() && !f[5].is_empty() {
return Some(OsString::from_vec(f[5].to_vec()));
}
}
None
}
extern "C" {
fn getuid() -> u32;
}
fn clean(p: &Path) -> PathBuf {
p.components().collect()
}
fn sha256(data: &[u8]) -> [u8; 32] {
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
];
let mut h: [u32; 8] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
];
let mut msg = data.to_vec();
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&((data.len() as u64) * 8).to_be_bytes());
for block in msg.chunks_exact(64) {
let mut w = [0u32; 64];
for i in 0..16 {
w[i] = u32::from_be_bytes(block[i * 4..i * 4 + 4].try_into().unwrap());
}
for i in 16..64 {
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16].wrapping_add(s0).wrapping_add(w[i - 7]).wrapping_add(s1);
}
let mut v = h;
for i in 0..64 {
let s1 = v[4].rotate_right(6) ^ v[4].rotate_right(11) ^ v[4].rotate_right(25);
let ch = (v[4] & v[5]) ^ (!v[4] & v[6]);
let t1 = v[7]
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(K[i])
.wrapping_add(w[i]);
let s0 = v[0].rotate_right(2) ^ v[0].rotate_right(13) ^ v[0].rotate_right(22);
let maj = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]);
let t2 = s0.wrapping_add(maj);
v = [
t1.wrapping_add(t2),
v[0],
v[1],
v[2],
v[3].wrapping_add(t1),
v[4],
v[5],
v[6],
];
}
for i in 0..8 {
h[i] = h[i].wrapping_add(v[i]);
}
}
let mut out = [0u8; 32];
for (i, x) in h.iter().enumerate() {
out[i * 4..i * 4 + 4].copy_from_slice(&x.to_be_bytes());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV: Mutex<()> = Mutex::new(());
fn with_home<T>(home: &str, f: impl FnOnce() -> T) -> T {
let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
let saved = std::env::var_os("HOME");
std::env::set_var("HOME", home);
let out = f();
match saved {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
out
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
#[test]
fn sha256_known_vectors() {
assert_eq!(
hex(&sha256(b"abc")),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(
hex(&sha256(b"")),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
let long = vec![b'a'; 1000];
assert_eq!(
hex(&sha256(&long)),
"41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3"
);
}
#[test]
fn golden_names() {
let cases = [
("/mnt/vol", "mountos_api_9bbffa0eba37045f.sock"),
("/Volumes/data one", "mountos_api_87774d7d94a7fe60.sock"),
("/mnt/\u{fc}/x", "mountos_api_bc8db74b980a9567.sock"),
];
for (mount, name) in cases {
let got = with_home("/home/u", || socket_path(mount)).unwrap();
assert_eq!(got, Path::new("/home/u/.mountOS/sockets").join(name), "{mount}");
}
}
#[test]
fn home_is_cleaned() {
let got = with_home("/home//u/", || socket_path("/mnt/vol")).unwrap();
assert_eq!(
got,
Path::new("/home/u/.mountOS/sockets/mountos_api_9bbffa0eba37045f.sock")
);
}
#[test]
fn long_home_falls_back_to_tmp() {
let home = format!("/home/{}", "x".repeat(80));
let got = with_home(&home, || socket_path("/mnt/vol")).unwrap();
let euid = unsafe { geteuid() };
assert_eq!(
got,
PathBuf::from(format!("/tmp/mountos-{euid}/mountos_api_9bbffa0eba37045f.sock"))
);
}
#[test]
fn limit_is_inclusive_at_103() {
let name_len = "mountos_api_9bbffa0eba37045f.sock".len();
let suffix = "/.mountOS/sockets/".len();
let fit = format!("/{}", "h".repeat(MAX_UNIX_SOCKET_PATH - name_len - suffix - 1));
let got = with_home(&fit, || socket_path("/mnt/vol")).unwrap();
assert_eq!(got.as_os_str().len(), MAX_UNIX_SOCKET_PATH);
assert!(got.starts_with(&fit));
let over = format!("{fit}h");
let got = with_home(&over, || socket_path("/mnt/vol")).unwrap();
assert!(got.starts_with("/tmp"));
}
#[test]
fn symlink_resolves_before_hashing() {
let root = std::env::temp_dir().join(format!("mos-sock-{}", std::process::id()));
let target = root.join("target");
let link = root.join("link");
std::fs::create_dir_all(&target).unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let want = with_home("/home/u", || socket_path(&target)).unwrap();
let got = with_home("/home/u", || socket_path(&link)).unwrap();
let _ = std::fs::remove_dir_all(&root);
assert_eq!(got, want);
}
#[test]
fn missing_path_keeps_spelling() {
let missing = "/nonexistent-mountos-sock/vol";
let got = with_home("/home/u", || socket_path(missing)).unwrap();
let name = socket_name(missing.as_bytes());
assert_eq!(got, Path::new("/home/u/.mountOS/sockets").join(name));
}
#[test]
fn empty_mount_path_is_rejected() {
assert!(socket_path("").is_err());
}
}