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 broker_event_filters_path(state_dir: &Path, room_id: &str) -> PathBuf {
153 state_dir.join(format!("{room_id}.event_filters"))
154}
155
156pub fn ensure_room_dirs() -> std::io::Result<()> {
164 create_dir_0700(&room_state_dir())?;
165 create_dir_0700(&room_data_dir())?;
166 let rt = runtime_dir();
170 if rt != std::path::Path::new("/tmp") {
171 create_dir_0700(&rt)?;
172 }
173 Ok(())
174}
175
176fn home_dir() -> PathBuf {
179 std::env::var("HOME")
180 .map(PathBuf::from)
181 .unwrap_or_else(|_| PathBuf::from("/tmp"))
182}
183
184fn runtime_dir() -> PathBuf {
185 #[cfg(target_os = "macos")]
188 {
189 std::env::var("TMPDIR")
190 .map(PathBuf::from)
191 .unwrap_or_else(|_| PathBuf::from("/tmp"))
192 }
193 #[cfg(not(target_os = "macos"))]
194 {
195 std::env::var("XDG_RUNTIME_DIR")
196 .map(|d| PathBuf::from(d).join("room"))
197 .unwrap_or_else(|_| PathBuf::from("/tmp"))
198 }
199}
200
201fn create_dir_0700(path: &Path) -> std::io::Result<()> {
202 #[cfg(unix)]
203 {
204 std::fs::DirBuilder::new()
205 .recursive(true)
206 .mode(0o700)
207 .create(path)
208 }
209 #[cfg(not(unix))]
210 {
211 std::fs::create_dir_all(path)
212 }
213}
214
215#[cfg(test)]
218mod tests {
219 use super::*;
220 use std::sync::Mutex;
221
222 static ENV_LOCK: Mutex<()> = Mutex::new(());
226
227 #[test]
228 fn room_home_ends_with_dot_room() {
229 let h = room_home();
230 assert!(
231 h.ends_with(".room"),
232 "expected path ending in .room, got: {h:?}"
233 );
234 }
235
236 #[test]
237 fn room_state_dir_under_room_home() {
238 assert!(room_state_dir().starts_with(room_home()));
239 assert!(room_state_dir().ends_with("state"));
240 }
241
242 #[test]
243 fn room_data_dir_under_room_home() {
244 assert!(room_data_dir().starts_with(room_home()));
245 assert!(room_data_dir().ends_with("data"));
246 }
247
248 #[test]
249 fn token_path_is_per_room_and_user() {
250 let alice_r1 = token_path("room1", "alice");
251 let bob_r1 = token_path("room1", "bob");
252 let alice_r2 = token_path("room2", "alice");
253 assert_ne!(alice_r1, bob_r1);
254 assert_ne!(alice_r1, alice_r2);
255 assert!(alice_r1.to_str().unwrap().contains("alice"));
256 assert!(alice_r1.to_str().unwrap().contains("room1"));
257 }
258
259 #[test]
260 fn cursor_path_is_per_room_and_user() {
261 let p = cursor_path("myroom", "bob");
262 assert!(p.to_str().unwrap().contains("bob"));
263 assert!(p.to_str().unwrap().contains("myroom"));
264 assert!(p.to_str().unwrap().ends_with(".cursor"));
265 }
266
267 #[test]
268 fn broker_tokens_path_contains_room_id() {
269 let base = PathBuf::from("/tmp/state");
270 let p = broker_tokens_path(&base, "test-room");
271 assert_eq!(p, base.join("test-room.tokens"));
272 }
273
274 #[test]
275 fn broker_subscriptions_path_contains_room_id() {
276 let base = PathBuf::from("/tmp/state");
277 let p = broker_subscriptions_path(&base, "test-room");
278 assert_eq!(p, base.join("test-room.subscriptions"));
279 }
280
281 #[test]
282 fn broker_event_filters_path_contains_room_id() {
283 let base = PathBuf::from("/tmp/state");
284 let p = broker_event_filters_path(&base, "test-room");
285 assert_eq!(p, base.join("test-room.event_filters"));
286 }
287
288 #[test]
289 fn create_dir_0700_is_idempotent() {
290 let dir = tempfile::TempDir::new().unwrap();
291 let target = dir.path().join("nested").join("deep");
292 create_dir_0700(&target).unwrap();
293 create_dir_0700(&target).unwrap();
295 assert!(target.exists());
296 }
297
298 #[cfg(unix)]
299 #[test]
300 fn create_dir_0700_sets_correct_permissions() {
301 use std::os::unix::fs::PermissionsExt;
302 let dir = tempfile::TempDir::new().unwrap();
303 let target = dir.path().join("secret");
304 create_dir_0700(&target).unwrap();
305 let perms = std::fs::metadata(&target).unwrap().permissions();
306 assert_eq!(
307 perms.mode() & 0o777,
308 0o700,
309 "expected 0700, got {:o}",
310 perms.mode() & 0o777
311 );
312 }
313
314 #[test]
317 fn effective_socket_path_uses_env_var() {
318 let _lock = ENV_LOCK.lock().unwrap();
319 let key = "ROOM_SOCKET";
320 let prev = std::env::var(key).ok();
321 std::env::set_var(key, "/tmp/test-roomd.sock");
322 let result = effective_socket_path(None);
323 match prev {
324 Some(v) => std::env::set_var(key, v),
325 None => std::env::remove_var(key),
326 }
327 assert_eq!(result, PathBuf::from("/tmp/test-roomd.sock"));
328 }
329
330 #[test]
331 fn effective_socket_path_explicit_overrides_env() {
332 let _lock = ENV_LOCK.lock().unwrap();
333 let key = "ROOM_SOCKET";
334 let prev = std::env::var(key).ok();
335 std::env::set_var(key, "/tmp/env-roomd.sock");
336 let explicit = PathBuf::from("/tmp/explicit.sock");
337 let result = effective_socket_path(Some(&explicit));
338 match prev {
339 Some(v) => std::env::set_var(key, v),
340 None => std::env::remove_var(key),
341 }
342 assert_eq!(result, explicit);
343 }
344
345 #[test]
346 fn effective_socket_path_default_without_env() {
347 let _lock = ENV_LOCK.lock().unwrap();
348 let key = "ROOM_SOCKET";
349 let prev = std::env::var(key).ok();
350 std::env::remove_var(key);
351 let result = effective_socket_path(None);
352 match prev {
353 Some(v) => std::env::set_var(key, v),
354 None => std::env::remove_var(key),
355 }
356 assert_eq!(result, room_socket_path());
357 }
358
359 #[test]
360 fn room_runtime_dir_returns_absolute_path() {
361 let p = room_runtime_dir();
362 assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
363 }
364
365 #[test]
366 fn legacy_token_dir_returns_valid_path() {
367 let p = legacy_token_dir();
368 assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
370 }
371
372 #[test]
373 fn ensure_room_dirs_creates_state_and_data() {
374 let dir = tempfile::TempDir::new().unwrap();
377 let state = dir.path().join("state");
378 let data = dir.path().join("data");
379 create_dir_0700(&state).unwrap();
380 create_dir_0700(&data).unwrap();
381 assert!(state.exists());
382 assert!(data.exists());
383 }
384}