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
8const MAX_UNIX_SOCKET_PATH: usize = 103;
10
11extern "C" {
12 fn geteuid() -> u32;
13}
14
15pub 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 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 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
64fn passwd_home() -> Option<OsString> {
66 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
82fn 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}