Skip to main content

room_daemon/broker/daemon/
config.rs

1//! Daemon configuration and room ID validation.
2
3use std::path::PathBuf;
4
5/// Characters that are unsafe in filesystem paths or shell contexts.
6const UNSAFE_CHARS: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
7
8/// Maximum allowed length for a room ID.
9pub(crate) const MAX_ROOM_ID_LEN: usize = 64;
10
11/// Validate a room ID for filesystem safety.
12///
13/// Rejects IDs that are empty, too long, contain path traversal sequences
14/// (`..`), whitespace, or filesystem-unsafe characters.
15pub fn validate_room_id(room_id: &str) -> Result<(), String> {
16    if room_id.is_empty() {
17        return Err("room ID cannot be empty".into());
18    }
19    if room_id.len() > MAX_ROOM_ID_LEN {
20        return Err(format!(
21            "room ID too long ({} chars, max {MAX_ROOM_ID_LEN})",
22            room_id.len()
23        ));
24    }
25    if room_id == "." || room_id == ".." || room_id.contains("..") {
26        return Err("room ID cannot contain '..'".into());
27    }
28    if room_id.chars().any(|c| c.is_whitespace()) {
29        return Err("room ID cannot contain whitespace".into());
30    }
31    if let Some(bad) = room_id.chars().find(|c| UNSAFE_CHARS.contains(c)) {
32        return Err(format!("room ID contains unsafe character: {bad:?}"));
33    }
34    Ok(())
35}
36
37/// Configuration for the daemon.
38#[derive(Debug, Clone)]
39pub struct DaemonConfig {
40    /// Path to the daemon UDS socket (ephemeral, platform-native temp dir).
41    pub socket_path: PathBuf,
42    /// Directory for chat files. Each room gets `<data_dir>/<room_id>.chat`.
43    /// Defaults to `~/.room/data/`; overridable with `--data-dir`.
44    pub data_dir: PathBuf,
45    /// Directory for state files (token maps, cursors, subscriptions).
46    /// Defaults to `~/.room/state/`.
47    pub state_dir: PathBuf,
48    /// Optional WebSocket/REST port.
49    pub ws_port: Option<u16>,
50    /// Seconds to wait after the last connection closes before shutting down.
51    ///
52    /// Default is 30 seconds. Set to 0 for immediate shutdown when the last
53    /// client disconnects. Has no effect if there are always active connections.
54    pub grace_period_secs: u64,
55}
56
57impl DaemonConfig {
58    /// Resolve the chat file path for a given room.
59    pub fn chat_path(&self, room_id: &str) -> PathBuf {
60        self.data_dir.join(format!("{room_id}.chat"))
61    }
62
63    /// Resolve the token-map persistence path for a given room.
64    pub fn token_map_path(&self, room_id: &str) -> PathBuf {
65        crate::paths::broker_tokens_path(&self.state_dir, room_id)
66    }
67
68    /// System-level token persistence path: `<state_dir>/tokens.json`.
69    ///
70    /// Used by the daemon to share a single token store across all rooms.
71    /// Production default is `~/.room/state/tokens.json`; tests override
72    /// `state_dir` with a temp directory.
73    pub fn system_tokens_path(&self) -> PathBuf {
74        self.state_dir.join("tokens.json")
75    }
76
77    /// Resolve the subscription-map persistence path for a given room.
78    pub fn subscription_map_path(&self, room_id: &str) -> PathBuf {
79        crate::paths::broker_subscriptions_path(&self.state_dir, room_id)
80    }
81
82    /// Resolve the event-filter-map persistence path for a given room.
83    pub fn event_filter_map_path(&self, room_id: &str) -> PathBuf {
84        crate::paths::broker_event_filters_path(&self.state_dir, room_id)
85    }
86}
87
88impl Default for DaemonConfig {
89    fn default() -> Self {
90        Self {
91            socket_path: crate::paths::room_socket_path(),
92            data_dir: crate::paths::room_data_dir(),
93            state_dir: crate::paths::room_state_dir(),
94            ws_port: None,
95            grace_period_secs: 30,
96        }
97    }
98}