Skip to main content

ant_core/
config.rs

1use std::net::SocketAddr;
2use std::path::PathBuf;
3
4use crate::error::{Error, Result};
5
6/// Returns the platform-appropriate data directory for ant.
7///
8/// - Linux: `~/.local/share/ant`
9/// - macOS: `~/Library/Application Support/ant`
10/// - Windows: `%APPDATA%\ant`
11pub fn data_dir() -> Result<PathBuf> {
12    let base = if cfg!(target_os = "macos") {
13        home_dir()?.join("Library").join("Application Support")
14    } else if cfg!(target_os = "windows") {
15        std::env::var("APPDATA")
16            .map(PathBuf::from)
17            .unwrap_or_else(|_| home_dir().unwrap().join("AppData").join("Roaming"))
18    } else {
19        std::env::var("XDG_DATA_HOME")
20            .map(PathBuf::from)
21            .unwrap_or_else(|_| home_dir().unwrap().join(".local").join("share"))
22    };
23    Ok(base.join("ant"))
24}
25
26/// Returns the platform-appropriate configuration directory for ant.
27///
28/// - Linux: `~/.config/ant`
29/// - macOS: `~/Library/Application Support/ant`
30/// - Windows: `%APPDATA%\ant`
31pub fn config_dir() -> Result<PathBuf> {
32    let base = if cfg!(target_os = "macos") {
33        home_dir()?.join("Library").join("Application Support")
34    } else if cfg!(target_os = "windows") {
35        std::env::var("APPDATA")
36            .map(PathBuf::from)
37            .unwrap_or_else(|_| home_dir().unwrap().join("AppData").join("Roaming"))
38    } else {
39        std::env::var("XDG_CONFIG_HOME")
40            .map(PathBuf::from)
41            .unwrap_or_else(|_| home_dir().unwrap().join(".config"))
42    };
43    Ok(base.join("ant"))
44}
45
46/// Returns the platform-appropriate log directory for ant.
47///
48/// - Linux: `~/.local/share/ant/logs`
49/// - macOS: `~/Library/Logs/ant`
50/// - Windows: `%APPDATA%\ant\logs`
51pub fn log_dir() -> Result<PathBuf> {
52    if cfg!(target_os = "macos") {
53        Ok(home_dir()?.join("Library").join("Logs").join("ant"))
54    } else {
55        Ok(data_dir()?.join("logs"))
56    }
57}
58
59/// Loads bootstrap peers from the platform-appropriate `bootstrap_peers.toml` file.
60///
61/// Returns `Ok(Some(peers))` if the file exists and parses successfully,
62/// `Ok(None)` if the file does not exist, or `Err` on parse/IO failures.
63pub fn load_bootstrap_peers() -> Result<Option<Vec<SocketAddr>>> {
64    let path = config_dir()?.join("bootstrap_peers.toml");
65    if !path.exists() {
66        return Ok(None);
67    }
68
69    let contents = std::fs::read_to_string(&path)?;
70    let config: BootstrapConfig =
71        toml::from_str(&contents).map_err(|e| Error::BootstrapConfigParse(e.to_string()))?;
72
73    let addrs: Vec<SocketAddr> = config.peers.iter().filter_map(|s| s.parse().ok()).collect();
74
75    if addrs.is_empty() {
76        return Ok(None);
77    }
78
79    Ok(Some(addrs))
80}
81
82#[derive(serde::Deserialize)]
83struct BootstrapConfig {
84    peers: Vec<String>,
85}
86
87fn home_dir() -> Result<PathBuf> {
88    std::env::var("HOME")
89        .or_else(|_| std::env::var("USERPROFILE"))
90        .map(PathBuf::from)
91        .map_err(|_| Error::HomeDirNotFound)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn data_dir_ends_with_ant() {
100        let dir = data_dir().unwrap();
101        assert_eq!(dir.file_name().unwrap(), "ant");
102    }
103
104    #[test]
105    fn config_dir_ends_with_ant() {
106        let dir = config_dir().unwrap();
107        assert_eq!(dir.file_name().unwrap(), "ant");
108    }
109
110    #[test]
111    fn log_dir_contains_ant() {
112        let dir = log_dir().unwrap();
113        assert!(
114            dir.components().any(|c| c.as_os_str() == "ant"),
115            "log_dir should contain 'ant' component: {:?}",
116            dir
117        );
118    }
119
120    #[test]
121    fn load_bootstrap_peers_returns_none_when_no_file() {
122        // Set config dir to a temp location where no file exists
123        let _result = load_bootstrap_peers();
124        // Just verify it doesn't panic — actual None depends on whether the file exists
125    }
126
127    #[test]
128    fn parse_bootstrap_config() {
129        let toml_str = r#"
130peers = [
131    "129.212.138.135:10000",
132    "134.199.138.183:10000",
133]
134"#;
135        let config: BootstrapConfig = toml::from_str(toml_str).unwrap();
136        assert_eq!(config.peers.len(), 2);
137        let addr: SocketAddr = config.peers[0].parse().unwrap();
138        assert_eq!(addr.port(), 10000);
139    }
140}