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