use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::os::unix::fs::DirBuilderExt;
pub fn room_home() -> PathBuf {
home_dir().join(".room")
}
pub fn room_state_dir() -> PathBuf {
room_home().join("state")
}
pub fn room_data_dir() -> PathBuf {
room_home().join("data")
}
pub fn room_runtime_dir() -> PathBuf {
runtime_dir()
}
pub fn room_socket_path() -> PathBuf {
runtime_dir().join("roomd.sock")
}
pub fn effective_socket_path(explicit: Option<&std::path::Path>) -> PathBuf {
if let Some(p) = explicit {
return p.to_owned();
}
if let Ok(p) = std::env::var("ROOM_SOCKET") {
if !p.is_empty() {
return PathBuf::from(p);
}
}
room_socket_path()
}
pub fn room_single_socket_path(room_id: &str) -> PathBuf {
runtime_dir().join(format!("room-{room_id}.sock"))
}
pub fn room_meta_path(room_id: &str) -> PathBuf {
runtime_dir().join(format!("room-{room_id}.meta"))
}
pub fn token_path(room_id: &str, username: &str) -> PathBuf {
room_state_dir().join(format!("room-{room_id}-{username}.token"))
}
pub fn global_token_path(username: &str) -> PathBuf {
room_state_dir().join(format!("room-{username}.token"))
}
pub fn cursor_path(room_id: &str, username: &str) -> PathBuf {
room_state_dir().join(format!("room-{room_id}-{username}.cursor"))
}
pub fn broker_tokens_path(state_dir: &Path, room_id: &str) -> PathBuf {
state_dir.join(format!("{room_id}.tokens"))
}
pub fn room_pid_path() -> PathBuf {
room_home().join("roomd.pid")
}
pub fn system_tokens_path() -> PathBuf {
room_state_dir().join("tokens.json")
}
pub fn legacy_token_dir() -> PathBuf {
runtime_dir()
}
pub fn broker_subscriptions_path(state_dir: &Path, room_id: &str) -> PathBuf {
state_dir.join(format!("{room_id}.subscriptions"))
}
pub fn ensure_room_dirs() -> std::io::Result<()> {
create_dir_0700(&room_state_dir())?;
create_dir_0700(&room_data_dir())?;
Ok(())
}
fn home_dir() -> PathBuf {
std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"))
}
fn runtime_dir() -> PathBuf {
#[cfg(target_os = "macos")]
{
std::env::var("TMPDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"))
}
#[cfg(not(target_os = "macos"))]
{
std::env::var("XDG_RUNTIME_DIR")
.map(|d| PathBuf::from(d).join("room"))
.unwrap_or_else(|_| PathBuf::from("/tmp"))
}
}
fn create_dir_0700(path: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(path)
}
#[cfg(not(unix))]
{
std::fs::create_dir_all(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn room_home_ends_with_dot_room() {
let h = room_home();
assert!(
h.ends_with(".room"),
"expected path ending in .room, got: {h:?}"
);
}
#[test]
fn room_state_dir_under_room_home() {
assert!(room_state_dir().starts_with(room_home()));
assert!(room_state_dir().ends_with("state"));
}
#[test]
fn room_data_dir_under_room_home() {
assert!(room_data_dir().starts_with(room_home()));
assert!(room_data_dir().ends_with("data"));
}
#[test]
fn token_path_is_per_room_and_user() {
let alice_r1 = token_path("room1", "alice");
let bob_r1 = token_path("room1", "bob");
let alice_r2 = token_path("room2", "alice");
assert_ne!(alice_r1, bob_r1);
assert_ne!(alice_r1, alice_r2);
assert!(alice_r1.to_str().unwrap().contains("alice"));
assert!(alice_r1.to_str().unwrap().contains("room1"));
}
#[test]
fn cursor_path_is_per_room_and_user() {
let p = cursor_path("myroom", "bob");
assert!(p.to_str().unwrap().contains("bob"));
assert!(p.to_str().unwrap().contains("myroom"));
assert!(p.to_str().unwrap().ends_with(".cursor"));
}
#[test]
fn broker_tokens_path_contains_room_id() {
let base = PathBuf::from("/tmp/state");
let p = broker_tokens_path(&base, "test-room");
assert_eq!(p, base.join("test-room.tokens"));
}
#[test]
fn broker_subscriptions_path_contains_room_id() {
let base = PathBuf::from("/tmp/state");
let p = broker_subscriptions_path(&base, "test-room");
assert_eq!(p, base.join("test-room.subscriptions"));
}
#[test]
fn create_dir_0700_is_idempotent() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("nested").join("deep");
create_dir_0700(&target).unwrap();
create_dir_0700(&target).unwrap();
assert!(target.exists());
}
#[cfg(unix)]
#[test]
fn create_dir_0700_sets_correct_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("secret");
create_dir_0700(&target).unwrap();
let perms = std::fs::metadata(&target).unwrap().permissions();
assert_eq!(
perms.mode() & 0o777,
0o700,
"expected 0700, got {:o}",
perms.mode() & 0o777
);
}
#[test]
fn effective_socket_path_uses_env_var() {
let _lock = ENV_LOCK.lock().unwrap();
let key = "ROOM_SOCKET";
let prev = std::env::var(key).ok();
std::env::set_var(key, "/tmp/test-roomd.sock");
let result = effective_socket_path(None);
match prev {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
assert_eq!(result, PathBuf::from("/tmp/test-roomd.sock"));
}
#[test]
fn effective_socket_path_explicit_overrides_env() {
let _lock = ENV_LOCK.lock().unwrap();
let key = "ROOM_SOCKET";
let prev = std::env::var(key).ok();
std::env::set_var(key, "/tmp/env-roomd.sock");
let explicit = PathBuf::from("/tmp/explicit.sock");
let result = effective_socket_path(Some(&explicit));
match prev {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
assert_eq!(result, explicit);
}
#[test]
fn effective_socket_path_default_without_env() {
let _lock = ENV_LOCK.lock().unwrap();
let key = "ROOM_SOCKET";
let prev = std::env::var(key).ok();
std::env::remove_var(key);
let result = effective_socket_path(None);
match prev {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
assert_eq!(result, room_socket_path());
}
#[test]
fn room_runtime_dir_returns_absolute_path() {
let p = room_runtime_dir();
assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
}
#[test]
fn legacy_token_dir_returns_valid_path() {
let p = legacy_token_dir();
assert!(p.is_absolute(), "expected absolute path, got: {p:?}");
}
#[test]
fn ensure_room_dirs_creates_state_and_data() {
let dir = tempfile::TempDir::new().unwrap();
let state = dir.path().join("state");
let data = dir.path().join("data");
create_dir_0700(&state).unwrap();
create_dir_0700(&data).unwrap();
assert!(state.exists());
assert!(data.exists());
}
}