1use std::path::{Path, PathBuf};
13
14#[cfg(unix)]
15use std::os::unix::fs::DirBuilderExt;
16
17pub fn room_home() -> PathBuf {
21 home_dir().join(".room")
22}
23
24pub fn room_state_dir() -> PathBuf {
28 room_home().join("state")
29}
30
31pub fn room_data_dir() -> PathBuf {
35 room_home().join("data")
36}
37
38pub fn room_runtime_dir() -> PathBuf {
44 runtime_dir()
45}
46
47pub fn room_socket_path() -> PathBuf {
52 runtime_dir().join("roomd.sock")
53}
54
55pub fn effective_socket_path(explicit: Option<&std::path::Path>) -> PathBuf {
62 if let Some(p) = explicit {
63 return p.to_owned();
64 }
65 if let Ok(p) = std::env::var("ROOM_SOCKET") {
66 if !p.is_empty() {
67 return PathBuf::from(p);
68 }
69 }
70 room_socket_path()
71}
72
73pub fn room_single_socket_path(room_id: &str) -> PathBuf {
75 runtime_dir().join(format!("room-{room_id}.sock"))
76}
77
78pub fn room_meta_path(room_id: &str) -> PathBuf {
80 runtime_dir().join(format!("room-{room_id}.meta"))
81}
82
83pub fn token_path(room_id: &str, username: &str) -> PathBuf {
87 room_state_dir().join(format!("room-{room_id}-{username}.token"))
88}
89
90pub fn global_token_path(username: &str) -> PathBuf {
95 room_state_dir().join(format!("room-{username}.token"))
96}
97
98pub fn cursor_path(room_id: &str, username: &str) -> PathBuf {
102 room_state_dir().join(format!("room-{room_id}-{username}.cursor"))
103}
104
105pub fn broker_tokens_path(state_dir: &Path, room_id: &str) -> PathBuf {
109 state_dir.join(format!("{room_id}.tokens"))
110}
111
112pub fn room_pid_path() -> PathBuf {
117 room_home().join("roomd.pid")
118}
119
120pub fn system_tokens_path() -> PathBuf {
126 room_state_dir().join("tokens.json")
127}
128
129pub fn legacy_token_dir() -> PathBuf {
137 runtime_dir()
138}
139
140pub fn broker_subscriptions_path(state_dir: &Path, room_id: &str) -> PathBuf {
145 state_dir.join(format!("{room_id}.subscriptions"))
146}
147
148pub fn ensure_room_dirs() -> std::io::Result<()> {
156 create_dir_0700(&room_state_dir())?;
157 create_dir_0700(&room_data_dir())?;
158 Ok(())
159}
160
161fn home_dir() -> PathBuf {
164 std::env::var("HOME")
165 .map(PathBuf::from)
166 .unwrap_or_else(|_| PathBuf::from("/tmp"))
167}
168
169fn runtime_dir() -> PathBuf {
170 #[cfg(target_os = "macos")]
173 {
174 std::env::var("TMPDIR")
175 .map(PathBuf::from)
176 .unwrap_or_else(|_| PathBuf::from("/tmp"))
177 }
178 #[cfg(not(target_os = "macos"))]
179 {
180 std::env::var("XDG_RUNTIME_DIR")
181 .map(|d| PathBuf::from(d).join("room"))
182 .unwrap_or_else(|_| PathBuf::from("/tmp"))
183 }
184}
185
186fn create_dir_0700(path: &Path) -> std::io::Result<()> {
187 #[cfg(unix)]
188 {
189 std::fs::DirBuilder::new()
190 .recursive(true)
191 .mode(0o700)
192 .create(path)
193 }
194 #[cfg(not(unix))]
195 {
196 std::fs::create_dir_all(path)
197 }
198}
199
200#[cfg(test)]
203mod tests {
204 use super::*;
205 use std::sync::Mutex;
206
207 static ENV_LOCK: Mutex<()> = Mutex::new(());
211
212 #[test]
213 fn room_home_ends_with_dot_room() {
214 let h = room_home();
215 assert!(
216 h.ends_with(".room"),
217 "expected path ending in .room, got: {h:?}"
218 );
219 }
220
221 #[test]
222 fn room_state_dir_under_room_home() {
223 assert!(room_state_dir().starts_with(room_home()));
224 assert!(room_state_dir().ends_with("state"));
225 }
226
227 #[test]
228 fn room_data_dir_under_room_home() {
229 assert!(room_data_dir().starts_with(room_home()));
230 assert!(room_data_dir().ends_with("data"));
231 }
232
233 #[test]
234 fn token_path_is_per_room_and_user() {
235 let alice_r1 = token_path("room1", "alice");
236 let bob_r1 = token_path("room1", "bob");
237 let alice_r2 = token_path("room2", "alice");
238 assert_ne!(alice_r1, bob_r1);
239 assert_ne!(alice_r1, alice_r2);
240 assert!(alice_r1.to_str().unwrap().contains("alice"));
241 assert!(alice_r1.to_str().unwrap().contains("room1"));
242 }
243
244 #[test]
245 fn cursor_path_is_per_room_and_user() {
246 let p = cursor_path("myroom", "bob");
247 assert!(p.to_str().unwrap().contains("bob"));
248 assert!(p.to_str().unwrap().contains("myroom"));
249 assert!(p.to_str().unwrap().ends_with(".cursor"));
250 }
251
252 #[test]
253 fn broker_tokens_path_contains_room_id() {
254 let base = PathBuf::from("/tmp/state");
255 let p = broker_tokens_path(&base, "test-room");
256 assert_eq!(p, base.join("test-room.tokens"));
257 }
258
259 #[test]
260 fn broker_subscriptions_path_contains_room_id() {
261 let base = PathBuf::from("/tmp/state");
262 let p = broker_subscriptions_path(&base, "test-room");
263 assert_eq!(p, base.join("test-room.subscriptions"));
264 }
265
266 #[test]
267 fn create_dir_0700_is_idempotent() {
268 let dir = tempfile::TempDir::new().unwrap();
269 let target = dir.path().join("nested").join("deep");
270 create_dir_0700(&target).unwrap();
271 create_dir_0700(&target).unwrap();
273 assert!(target.exists());
274 }
275
276 #[cfg(unix)]
277 #[test]
278 fn create_dir_0700_sets_correct_permissions() {
279 use std::os::unix::fs::PermissionsExt;
280 let dir = tempfile::TempDir::new().unwrap();
281 let target = dir.path().join("secret");
282 create_dir_0700(&target).unwrap();
283 let perms = std::fs::metadata(&target).unwrap().permissions();
284 assert_eq!(
285 perms.mode() & 0o777,
286 0o700,
287 "expected 0700, got {:o}",
288 perms.mode() & 0o777
289 );
290 }
291
292 #[test]
295 fn effective_socket_path_uses_env_var() {
296 let _lock = ENV_LOCK.lock().unwrap();
297 let key = "ROOM_SOCKET";
298 let prev = std::env::var(key).ok();
299 std::env::set_var(key, "/tmp/test-roomd.sock");
300 let result = effective_socket_path(None);
301 match prev {
302 Some(v) => std::env::set_var(key, v),
303 None => std::env::remove_var(key),
304 }
305 assert_eq!(result, PathBuf::from("/tmp/test-roomd.sock"));
306 }
307
308 #[test]
309 fn effective_socket_path_explicit_overrides_env() {
310 let _lock = ENV_LOCK.lock().unwrap();
311 let key = "ROOM_SOCKET";
312 let prev = std::env::var(key).ok();
313 std::env::set_var(key, "/tmp/env-roomd.sock");
314 let explicit = PathBuf::from("/tmp/explicit.sock");
315 let result = effective_socket_path(Some(&explicit));
316 match prev {
317 Some(v) => std::env::set_var(key, v),
318 None => std::env::remove_var(key),
319 }
320 assert_eq!(result, explicit);
321 }
322
323 #[test]
324 fn effective_socket_path_default_without_env() {
325 let _lock = ENV_LOCK.lock().unwrap();
326 let key = "ROOM_SOCKET";
327 let prev = std::env::var(key).ok();
328 std::env::remove_var(key);
329 let result = effective_socket_path(None);
330 match prev {
331 Some(v) => std::env::set_var(key, v),
332 None => std::env::remove_var(key),
333 }
334 assert_eq!(result, room_socket_path());
335 }
336
337 #[test]
338 fn room_runtime_dir_returns_absolute_path() {
339 let p = room_runtime_dir();
340 assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
341 }
342
343 #[test]
344 fn legacy_token_dir_returns_valid_path() {
345 let p = legacy_token_dir();
346 assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
348 }
349
350 #[test]
351 fn ensure_room_dirs_creates_state_and_data() {
352 let dir = tempfile::TempDir::new().unwrap();
355 let state = dir.path().join("state");
356 let data = dir.path().join("data");
357 create_dir_0700(&state).unwrap();
358 create_dir_0700(&data).unwrap();
359 assert!(state.exists());
360 assert!(data.exists());
361 }
362}