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