Skip to main content

mountos_native/
socket.rs

1use std::ffi::OsString;
2use std::io;
3use std::os::unix::ffi::{OsStrExt, OsStringExt};
4use std::path::{Path, PathBuf};
5
6use crate::error::{Error, Result};
7
8/// Longest usable AF_UNIX path. macOS allows 104 bytes with the NUL, Linux 108.
9const MAX_UNIX_SOCKET_PATH: usize = 103;
10
11extern "C" {
12  fn geteuid() -> u32;
13}
14
15/// Computes the native API socket path of the mount at `mount_path`. `mount_path` is the path the
16/// mount was started with. The result is the same path the mount listens on. Nothing is created
17/// or checked on disk.
18///
19/// The name is `mountos_api_` plus the first 8 bytes of the SHA-256 of the mount path in hex plus
20/// `.sock`. It sits in `~/.mountOS/sockets`, or in `/tmp/mountos-<euid>` when that path is longer
21/// than the AF_UNIX limit. The home directory comes from `$HOME`, then the passwd entry, then the
22/// temporary directory.
23pub fn socket_path(mount_path: impl AsRef<Path>) -> Result<PathBuf> {
24  let mount_path = mount_path.as_ref();
25  if mount_path.as_os_str().is_empty() {
26    return Err(Error::Io(io::Error::new(
27      io::ErrorKind::InvalidInput,
28      "socket_path: empty mount path",
29    )));
30  }
31  // Hash the symlink-resolved path, as the mount does. A path that does not exist keeps its spelling.
32  let resolved = std::fs::canonicalize(mount_path).ok();
33  let mount_path = resolved.as_deref().unwrap_or(mount_path);
34  let name = socket_name(mount_path.as_os_str().as_bytes());
35  let preferred = clean(&sockets_dir().join(&name));
36  if preferred.as_os_str().len() <= MAX_UNIX_SOCKET_PATH {
37    return Ok(preferred);
38  }
39  // SAFETY: geteuid has no arguments and cannot fail.
40  let euid = unsafe { geteuid() };
41  Ok(Path::new("/tmp").join(format!("mountos-{euid}")).join(name))
42}
43
44fn socket_name(mount_path: &[u8]) -> String {
45  use std::fmt::Write;
46  let sum = sha256(mount_path);
47  let mut name = String::from("mountos_api_");
48  for b in &sum[..8] {
49    let _ = write!(name, "{b:02x}");
50  }
51  name.push_str(".sock");
52  name
53}
54
55fn sockets_dir() -> PathBuf {
56  let home = std::env::var_os("HOME")
57    .filter(|h| !h.is_empty())
58    .or_else(passwd_home)
59    .map(PathBuf::from)
60    .unwrap_or_else(std::env::temp_dir);
61  home.join(".mountOS").join("sockets")
62}
63
64/// Home directory of the real user from /etc/passwd.
65fn passwd_home() -> Option<OsString> {
66  // SAFETY: getuid has no arguments and cannot fail.
67  let uid = unsafe { getuid() }.to_string();
68  let data = std::fs::read(Path::new("/etc/passwd")).ok()?;
69  for line in data.split(|&b| b == b'\n') {
70    let f: Vec<&[u8]> = line.split(|&b| b == b':').collect();
71    if f.len() >= 6 && f[2] == uid.as_bytes() && !f[5].is_empty() {
72      return Some(OsString::from_vec(f[5].to_vec()));
73    }
74  }
75  None
76}
77
78extern "C" {
79  fn getuid() -> u32;
80}
81
82/// Lexical clean: drops repeated separators and `.` components.
83fn clean(p: &Path) -> PathBuf {
84  p.components().collect()
85}
86
87fn sha256(data: &[u8]) -> [u8; 32] {
88  const K: [u32; 64] = [
89    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
90    0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
91    0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
92    0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
93    0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
94    0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
95    0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
96    0xc67178f2,
97  ];
98  let mut h: [u32; 8] = [
99    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
100  ];
101  let mut msg = data.to_vec();
102  msg.push(0x80);
103  while msg.len() % 64 != 56 {
104    msg.push(0);
105  }
106  msg.extend_from_slice(&((data.len() as u64) * 8).to_be_bytes());
107  for block in msg.chunks_exact(64) {
108    let mut w = [0u32; 64];
109    for i in 0..16 {
110      w[i] = u32::from_be_bytes(block[i * 4..i * 4 + 4].try_into().unwrap());
111    }
112    for i in 16..64 {
113      let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
114      let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
115      w[i] = w[i - 16].wrapping_add(s0).wrapping_add(w[i - 7]).wrapping_add(s1);
116    }
117    let mut v = h;
118    for i in 0..64 {
119      let s1 = v[4].rotate_right(6) ^ v[4].rotate_right(11) ^ v[4].rotate_right(25);
120      let ch = (v[4] & v[5]) ^ (!v[4] & v[6]);
121      let t1 = v[7]
122        .wrapping_add(s1)
123        .wrapping_add(ch)
124        .wrapping_add(K[i])
125        .wrapping_add(w[i]);
126      let s0 = v[0].rotate_right(2) ^ v[0].rotate_right(13) ^ v[0].rotate_right(22);
127      let maj = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]);
128      let t2 = s0.wrapping_add(maj);
129      v = [
130        t1.wrapping_add(t2),
131        v[0],
132        v[1],
133        v[2],
134        v[3].wrapping_add(t1),
135        v[4],
136        v[5],
137        v[6],
138      ];
139    }
140    for i in 0..8 {
141      h[i] = h[i].wrapping_add(v[i]);
142    }
143  }
144  let mut out = [0u8; 32];
145  for (i, x) in h.iter().enumerate() {
146    out[i * 4..i * 4 + 4].copy_from_slice(&x.to_be_bytes());
147  }
148  out
149}
150
151#[cfg(test)]
152mod tests {
153  use super::*;
154  use std::sync::Mutex;
155
156  static ENV: Mutex<()> = Mutex::new(());
157
158  fn with_home<T>(home: &str, f: impl FnOnce() -> T) -> T {
159    let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
160    let saved = std::env::var_os("HOME");
161    std::env::set_var("HOME", home);
162    let out = f();
163    match saved {
164      Some(v) => std::env::set_var("HOME", v),
165      None => std::env::remove_var("HOME"),
166    }
167    out
168  }
169
170  fn hex(b: &[u8]) -> String {
171    b.iter().map(|x| format!("{x:02x}")).collect()
172  }
173
174  #[test]
175  fn sha256_known_vectors() {
176    assert_eq!(
177      hex(&sha256(b"abc")),
178      "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
179    );
180    assert_eq!(
181      hex(&sha256(b"")),
182      "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
183    );
184    let long = vec![b'a'; 1000];
185    assert_eq!(
186      hex(&sha256(&long)),
187      "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3"
188    );
189  }
190
191  #[test]
192  fn golden_names() {
193    let cases = [
194      ("/mnt/vol", "mountos_api_9bbffa0eba37045f.sock"),
195      ("/Volumes/data one", "mountos_api_87774d7d94a7fe60.sock"),
196      ("/mnt/\u{fc}/x", "mountos_api_bc8db74b980a9567.sock"),
197    ];
198    for (mount, name) in cases {
199      let got = with_home("/home/u", || socket_path(mount)).unwrap();
200      assert_eq!(got, Path::new("/home/u/.mountOS/sockets").join(name), "{mount}");
201    }
202  }
203
204  #[test]
205  fn home_is_cleaned() {
206    let got = with_home("/home//u/", || socket_path("/mnt/vol")).unwrap();
207    assert_eq!(
208      got,
209      Path::new("/home/u/.mountOS/sockets/mountos_api_9bbffa0eba37045f.sock")
210    );
211  }
212
213  #[test]
214  fn long_home_falls_back_to_tmp() {
215    let home = format!("/home/{}", "x".repeat(80));
216    let got = with_home(&home, || socket_path("/mnt/vol")).unwrap();
217    let euid = unsafe { geteuid() };
218    assert_eq!(
219      got,
220      PathBuf::from(format!("/tmp/mountos-{euid}/mountos_api_9bbffa0eba37045f.sock"))
221    );
222  }
223
224  #[test]
225  fn limit_is_inclusive_at_103() {
226    let name_len = "mountos_api_9bbffa0eba37045f.sock".len();
227    let suffix = "/.mountOS/sockets/".len();
228    let fit = format!("/{}", "h".repeat(MAX_UNIX_SOCKET_PATH - name_len - suffix - 1));
229    let got = with_home(&fit, || socket_path("/mnt/vol")).unwrap();
230    assert_eq!(got.as_os_str().len(), MAX_UNIX_SOCKET_PATH);
231    assert!(got.starts_with(&fit));
232    let over = format!("{fit}h");
233    let got = with_home(&over, || socket_path("/mnt/vol")).unwrap();
234    assert!(got.starts_with("/tmp"));
235  }
236
237  #[test]
238  fn symlink_resolves_before_hashing() {
239    let root = std::env::temp_dir().join(format!("mos-sock-{}", std::process::id()));
240    let target = root.join("target");
241    let link = root.join("link");
242    std::fs::create_dir_all(&target).unwrap();
243    std::os::unix::fs::symlink(&target, &link).unwrap();
244    let want = with_home("/home/u", || socket_path(&target)).unwrap();
245    let got = with_home("/home/u", || socket_path(&link)).unwrap();
246    let _ = std::fs::remove_dir_all(&root);
247    assert_eq!(got, want);
248  }
249
250  #[test]
251  fn missing_path_keeps_spelling() {
252    let missing = "/nonexistent-mountos-sock/vol";
253    let got = with_home("/home/u", || socket_path(missing)).unwrap();
254    let name = socket_name(missing.as_bytes());
255    assert_eq!(got, Path::new("/home/u/.mountOS/sockets").join(name));
256  }
257
258  #[test]
259  fn empty_mount_path_is_rejected() {
260    assert!(socket_path("").is_err());
261  }
262}