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(
111 block[i * 4..i * 4 + 4]
112 .try_into()
113 .expect("4-byte chunk of a 64-byte block"),
114 );
115 }
116 for i in 16..64 {
117 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
118 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
119 w[i] = w[i - 16].wrapping_add(s0).wrapping_add(w[i - 7]).wrapping_add(s1);
120 }
121 let mut v = h;
122 for i in 0..64 {
123 let s1 = v[4].rotate_right(6) ^ v[4].rotate_right(11) ^ v[4].rotate_right(25);
124 let ch = (v[4] & v[5]) ^ (!v[4] & v[6]);
125 let t1 = v[7]
126 .wrapping_add(s1)
127 .wrapping_add(ch)
128 .wrapping_add(K[i])
129 .wrapping_add(w[i]);
130 let s0 = v[0].rotate_right(2) ^ v[0].rotate_right(13) ^ v[0].rotate_right(22);
131 let maj = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]);
132 let t2 = s0.wrapping_add(maj);
133 v = [
134 t1.wrapping_add(t2),
135 v[0],
136 v[1],
137 v[2],
138 v[3].wrapping_add(t1),
139 v[4],
140 v[5],
141 v[6],
142 ];
143 }
144 for i in 0..8 {
145 h[i] = h[i].wrapping_add(v[i]);
146 }
147 }
148 let mut out = [0u8; 32];
149 for (i, x) in h.iter().enumerate() {
150 out[i * 4..i * 4 + 4].copy_from_slice(&x.to_be_bytes());
151 }
152 out
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use std::sync::Mutex;
159
160 static ENV: Mutex<()> = Mutex::new(());
161
162 fn with_home<T>(home: &str, f: impl FnOnce() -> T) -> T {
163 let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
164 let saved = std::env::var_os("HOME");
165 std::env::set_var("HOME", home);
166 let out = f();
167 match saved {
168 Some(v) => std::env::set_var("HOME", v),
169 None => std::env::remove_var("HOME"),
170 }
171 out
172 }
173
174 fn hex(b: &[u8]) -> String {
175 b.iter().map(|x| format!("{x:02x}")).collect()
176 }
177
178 #[test]
179 fn sha256_known_vectors() {
180 assert_eq!(
181 hex(&sha256(b"abc")),
182 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
183 );
184 assert_eq!(
185 hex(&sha256(b"")),
186 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
187 );
188 let long = vec![b'a'; 1000];
189 assert_eq!(
190 hex(&sha256(&long)),
191 "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3"
192 );
193 }
194
195 #[test]
196 fn golden_names() {
197 let cases = [
198 ("/mnt/vol", "mountos_api_9bbffa0eba37045f.sock"),
199 ("/Volumes/data one", "mountos_api_87774d7d94a7fe60.sock"),
200 ("/mnt/\u{fc}/x", "mountos_api_bc8db74b980a9567.sock"),
201 ];
202 for (mount, name) in cases {
203 let got = with_home("/home/u", || socket_path(mount)).unwrap();
204 assert_eq!(got, Path::new("/home/u/.mountOS/sockets").join(name), "{mount}");
205 }
206 }
207
208 #[test]
209 fn home_is_cleaned() {
210 let got = with_home("/home//u/", || socket_path("/mnt/vol")).unwrap();
211 assert_eq!(
212 got,
213 Path::new("/home/u/.mountOS/sockets/mountos_api_9bbffa0eba37045f.sock")
214 );
215 }
216
217 #[test]
218 fn long_home_falls_back_to_tmp() {
219 let home = format!("/home/{}", "x".repeat(80));
220 let got = with_home(&home, || socket_path("/mnt/vol")).unwrap();
221 let euid = unsafe { geteuid() };
222 assert_eq!(
223 got,
224 PathBuf::from(format!("/tmp/mountos-{euid}/mountos_api_9bbffa0eba37045f.sock"))
225 );
226 }
227
228 #[test]
229 fn limit_is_inclusive_at_103() {
230 let name_len = "mountos_api_9bbffa0eba37045f.sock".len();
231 let suffix = "/.mountOS/sockets/".len();
232 let fit = format!("/{}", "h".repeat(MAX_UNIX_SOCKET_PATH - name_len - suffix - 1));
233 let got = with_home(&fit, || socket_path("/mnt/vol")).unwrap();
234 assert_eq!(got.as_os_str().len(), MAX_UNIX_SOCKET_PATH);
235 assert!(got.starts_with(&fit));
236 let over = format!("{fit}h");
237 let got = with_home(&over, || socket_path("/mnt/vol")).unwrap();
238 assert!(got.starts_with("/tmp"));
239 }
240
241 #[test]
242 fn symlink_resolves_before_hashing() {
243 let root = std::env::temp_dir().join(format!("mos-sock-{}", std::process::id()));
244 let target = root.join("target");
245 let link = root.join("link");
246 std::fs::create_dir_all(&target).unwrap();
247 std::os::unix::fs::symlink(&target, &link).unwrap();
248 let want = with_home("/home/u", || socket_path(&target)).unwrap();
249 let got = with_home("/home/u", || socket_path(&link)).unwrap();
250 let _ = std::fs::remove_dir_all(&root);
251 assert_eq!(got, want);
252 }
253
254 #[test]
255 fn missing_path_keeps_spelling() {
256 let missing = "/nonexistent-mountos-sock/vol";
257 let got = with_home("/home/u", || socket_path(missing)).unwrap();
258 let name = socket_name(missing.as_bytes());
259 assert_eq!(got, Path::new("/home/u/.mountOS/sockets").join(name));
260 }
261
262 #[test]
263 fn empty_mount_path_is_rejected() {
264 assert!(socket_path("").is_err());
265 }
266}