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 {
78 runtime_dir().join(format!("room-{room_id}.sock"))
79}
80
81pub fn room_meta_path(room_id: &str) -> PathBuf {
83 runtime_dir().join(format!("room-{room_id}.meta"))
84}
85
86pub fn token_path(room_id: &str, username: &str) -> PathBuf {
90 room_state_dir().join(format!("room-{room_id}-{username}.token"))
91}
92
93pub fn global_token_path(username: &str) -> PathBuf {
98 room_state_dir().join(format!("room-{username}.token"))
99}
100
101pub fn cursor_path(room_id: &str, username: &str) -> PathBuf {
105 room_state_dir().join(format!("room-{room_id}-{username}.cursor"))
106}
107
108pub fn broker_tokens_path(state_dir: &Path, room_id: &str) -> PathBuf {
112 state_dir.join(format!("{room_id}.tokens"))
113}
114
115pub fn room_pid_path() -> PathBuf {
120 room_home().join("roomd.pid")
121}
122
123pub fn system_tokens_path() -> PathBuf {
129 room_state_dir().join("tokens.json")
130}
131
132pub fn legacy_token_dir() -> PathBuf {
140 runtime_dir()
141}
142
143pub fn broker_subscriptions_path(state_dir: &Path, room_id: &str) -> PathBuf {
148 state_dir.join(format!("{room_id}.subscriptions"))
149}
150
151pub fn broker_event_filters_path(state_dir: &Path, room_id: &str) -> PathBuf {
156 state_dir.join(format!("{room_id}.event_filters"))
157}
158
159pub fn ensure_room_dirs() -> std::io::Result<()> {
167 create_dir_0700(&room_state_dir())?;
168 create_dir_0700(&room_data_dir())?;
169 let rt = runtime_dir();
173 if rt != std::path::Path::new("/tmp") {
174 create_dir_0700(&rt)?;
175 }
176 Ok(())
177}
178
179fn home_dir() -> PathBuf {
182 std::env::var("HOME")
183 .map(PathBuf::from)
184 .unwrap_or_else(|_| PathBuf::from("/tmp"))
185}
186
187fn runtime_dir() -> PathBuf {
188 #[cfg(target_os = "macos")]
191 {
192 std::env::var("TMPDIR")
193 .map(PathBuf::from)
194 .unwrap_or_else(|_| PathBuf::from("/tmp"))
195 }
196 #[cfg(not(target_os = "macos"))]
197 {
198 std::env::var("XDG_RUNTIME_DIR")
199 .map(|d| PathBuf::from(d).join("room"))
200 .unwrap_or_else(|_| PathBuf::from("/tmp"))
201 }
202}
203
204fn create_dir_0700(path: &Path) -> std::io::Result<()> {
205 #[cfg(unix)]
206 {
207 std::fs::DirBuilder::new()
208 .recursive(true)
209 .mode(0o700)
210 .create(path)
211 }
212 #[cfg(not(unix))]
213 {
214 std::fs::create_dir_all(path)
215 }
216}
217
218#[cfg(test)]
221mod tests {
222 use super::*;
223 use std::sync::Mutex;
224
225 static ENV_LOCK: Mutex<()> = Mutex::new(());
229
230 #[test]
231 fn room_home_ends_with_dot_room() {
232 let h = room_home();
233 assert!(
234 h.ends_with(".room"),
235 "expected path ending in .room, got: {h:?}"
236 );
237 }
238
239 #[test]
240 fn room_state_dir_under_room_home() {
241 assert!(room_state_dir().starts_with(room_home()));
242 assert!(room_state_dir().ends_with("state"));
243 }
244
245 #[test]
246 fn room_data_dir_under_room_home() {
247 assert!(room_data_dir().starts_with(room_home()));
248 assert!(room_data_dir().ends_with("data"));
249 }
250
251 #[test]
252 fn token_path_is_per_room_and_user() {
253 let alice_r1 = token_path("room1", "alice");
254 let bob_r1 = token_path("room1", "bob");
255 let alice_r2 = token_path("room2", "alice");
256 assert_ne!(alice_r1, bob_r1);
257 assert_ne!(alice_r1, alice_r2);
258 assert!(alice_r1.to_str().unwrap().contains("alice"));
259 assert!(alice_r1.to_str().unwrap().contains("room1"));
260 }
261
262 #[test]
263 fn cursor_path_is_per_room_and_user() {
264 let p = cursor_path("myroom", "bob");
265 assert!(p.to_str().unwrap().contains("bob"));
266 assert!(p.to_str().unwrap().contains("myroom"));
267 assert!(p.to_str().unwrap().ends_with(".cursor"));
268 }
269
270 #[test]
271 fn broker_tokens_path_contains_room_id() {
272 let base = PathBuf::from("/tmp/state");
273 let p = broker_tokens_path(&base, "test-room");
274 assert_eq!(p, base.join("test-room.tokens"));
275 }
276
277 #[test]
278 fn broker_subscriptions_path_contains_room_id() {
279 let base = PathBuf::from("/tmp/state");
280 let p = broker_subscriptions_path(&base, "test-room");
281 assert_eq!(p, base.join("test-room.subscriptions"));
282 }
283
284 #[test]
285 fn broker_event_filters_path_contains_room_id() {
286 let base = PathBuf::from("/tmp/state");
287 let p = broker_event_filters_path(&base, "test-room");
288 assert_eq!(p, base.join("test-room.event_filters"));
289 }
290
291 #[test]
292 fn create_dir_0700_is_idempotent() {
293 let dir = tempfile::TempDir::new().unwrap();
294 let target = dir.path().join("nested").join("deep");
295 create_dir_0700(&target).unwrap();
296 create_dir_0700(&target).unwrap();
298 assert!(target.exists());
299 }
300
301 #[cfg(unix)]
302 #[test]
303 fn create_dir_0700_sets_correct_permissions() {
304 use std::os::unix::fs::PermissionsExt;
305 let dir = tempfile::TempDir::new().unwrap();
306 let target = dir.path().join("secret");
307 create_dir_0700(&target).unwrap();
308 let perms = std::fs::metadata(&target).unwrap().permissions();
309 assert_eq!(
310 perms.mode() & 0o777,
311 0o700,
312 "expected 0700, got {:o}",
313 perms.mode() & 0o777
314 );
315 }
316
317 #[test]
320 fn effective_socket_path_uses_env_var() {
321 let _lock = ENV_LOCK.lock().unwrap();
322 let key = "ROOM_SOCKET";
323 let prev = std::env::var(key).ok();
324 std::env::set_var(key, "/tmp/test-roomd.sock");
325 let result = effective_socket_path(None);
326 match prev {
327 Some(v) => std::env::set_var(key, v),
328 None => std::env::remove_var(key),
329 }
330 assert_eq!(result, PathBuf::from("/tmp/test-roomd.sock"));
331 }
332
333 #[test]
334 fn effective_socket_path_explicit_overrides_env() {
335 let _lock = ENV_LOCK.lock().unwrap();
336 let key = "ROOM_SOCKET";
337 let prev = std::env::var(key).ok();
338 std::env::set_var(key, "/tmp/env-roomd.sock");
339 let explicit = PathBuf::from("/tmp/explicit.sock");
340 let result = effective_socket_path(Some(&explicit));
341 match prev {
342 Some(v) => std::env::set_var(key, v),
343 None => std::env::remove_var(key),
344 }
345 assert_eq!(result, explicit);
346 }
347
348 #[test]
349 fn effective_socket_path_default_without_env() {
350 let _lock = ENV_LOCK.lock().unwrap();
351 let key = "ROOM_SOCKET";
352 let prev = std::env::var(key).ok();
353 std::env::remove_var(key);
354 let result = effective_socket_path(None);
355 match prev {
356 Some(v) => std::env::set_var(key, v),
357 None => std::env::remove_var(key),
358 }
359 assert_eq!(result, room_socket_path());
360 }
361
362 #[test]
363 fn room_runtime_dir_returns_absolute_path() {
364 let p = room_runtime_dir();
365 assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
366 }
367
368 #[test]
369 fn legacy_token_dir_returns_valid_path() {
370 let p = legacy_token_dir();
371 assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
373 }
374
375 #[test]
376 fn ensure_room_dirs_creates_state_and_data() {
377 let dir = tempfile::TempDir::new().unwrap();
380 let state = dir.path().join("state");
381 let data = dir.path().join("data");
382 create_dir_0700(&state).unwrap();
383 create_dir_0700(&data).unwrap();
384 assert!(state.exists());
385 assert!(data.exists());
386 }
387}