1use std::net::SocketAddr;
2use std::path::PathBuf;
3
4use crate::error::{Error, Result};
5
6pub 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
26pub 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
46pub 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
59pub 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 let _result = load_bootstrap_peers();
124 }
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}