Skip to main content

huddle_core/
config.rs

1use std::path::PathBuf;
2
3pub fn data_dir() -> PathBuf {
4    let base = dirs::data_dir().unwrap_or_else(|| PathBuf::from("."));
5    base.join("huddle")
6}
7
8/// Phase D: location of the user's optional config file. We use
9/// `dirs::config_dir()` rather than `data_dir()` so this lives in the
10/// platform-appropriate "preferences" directory (macOS
11/// `~/Library/Application Support`, Linux `~/.config`, Windows
12/// `%APPDATA%`). Doesn't have to exist — `load_relays` returns an
13/// empty list if absent.
14pub fn config_path() -> PathBuf {
15    let base = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
16    base.join("huddle").join("config.toml")
17}
18
19/// Phase D: parse the `[network] relays = [...]` list from the config
20/// file. Tiny hand-rolled parser — we don't want a `toml` crate dep
21/// just for this. Lines starting with `#` are comments; whitespace is
22/// trimmed. Returns an empty Vec if the file doesn't exist or has no
23/// relays entry.
24pub fn load_relays() -> Option<Vec<String>> {
25    let path = config_path();
26    let body = std::fs::read_to_string(&path).ok()?;
27    let mut in_network = false;
28    let mut out: Vec<String> = Vec::new();
29    for line in body.lines() {
30        let line = line.trim();
31        if line.is_empty() || line.starts_with('#') {
32            continue;
33        }
34        if line.starts_with('[') {
35            in_network = line == "[network]";
36            continue;
37        }
38        if !in_network {
39            continue;
40        }
41        if let Some(rest) = line.strip_prefix("relays") {
42            let rest = rest.trim_start().trim_start_matches('=').trim();
43            // Support both `relays = ["a", "b"]` and a multi-line form.
44            // Strip the leading `[` and trailing `]` if present, split
45            // on `,`, then unquote each piece.
46            let payload = rest.trim_start_matches('[').trim_end_matches(']');
47            for item in payload.split(',') {
48                let item = item.trim().trim_matches('"').trim_matches('\'');
49                if !item.is_empty() {
50                    out.push(item.to_string());
51                }
52            }
53        }
54    }
55    Some(out)
56}
57
58pub fn db_path() -> PathBuf {
59    data_dir().join("huddle.db")
60}
61
62pub fn identity_key_path() -> PathBuf {
63    data_dir().join("identity.key")
64}
65
66pub fn log_path() -> PathBuf {
67    data_dir().join("huddle.log")
68}
69
70pub fn ensure_data_dir() -> std::io::Result<()> {
71    std::fs::create_dir_all(data_dir())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn data_dir_is_inside_huddle_directory() {
80        let dir = data_dir();
81        assert!(dir.ends_with("huddle") || dir.to_string_lossy().contains("huddle"));
82    }
83
84    #[test]
85    fn db_path_ends_with_huddle_db() {
86        let path = db_path();
87        assert_eq!(path.file_name().unwrap(), "huddle.db");
88    }
89
90    #[test]
91    fn identity_path_ends_with_identity_key() {
92        let path = identity_key_path();
93        assert_eq!(path.file_name().unwrap(), "identity.key");
94    }
95}