1use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{ConfigError, Error, Result};
11
12pub fn default_config_path() -> PathBuf {
14 if let Some(home) = dirs::config_dir() {
15 home.join("ntfs-mac").join("config.toml")
16 } else {
17 PathBuf::from("/tmp/ntfs-mac/config.toml")
20 }
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Config {
25 #[serde(default = "default_mount_base")]
28 pub mount_base: String,
29
30 #[serde(default)]
33 pub mount_options: Vec<String>,
34
35 #[serde(default = "default_fuse_driver")]
39 pub fuse_driver: String,
40
41 #[serde(default = "default_true")]
45 pub require_confirmation: bool,
46
47 #[serde(default = "default_true")]
49 pub color: bool,
50}
51
52impl Default for Config {
53 fn default() -> Self {
54 Self {
55 mount_base: default_mount_base(),
56 mount_options: Vec::new(),
57 fuse_driver: default_fuse_driver(),
58 require_confirmation: default_true(),
59 color: default_true(),
60 }
61 }
62}
63
64fn default_mount_base() -> String {
65 "/Volumes".to_string()
66}
67
68fn default_fuse_driver() -> String {
69 "auto".to_string()
70}
71
72fn default_true() -> bool {
73 true
74}
75
76impl Config {
77 pub fn load() -> Result<Self> {
79 load_config(&default_config_path())
80 }
81
82 pub fn save(&self) -> Result<()> {
84 save_config(self, &default_config_path())
85 }
86
87 #[must_use]
89 pub fn with_mount_base(mut self, base: String) -> Self {
90 self.mount_base = base;
91 self
92 }
93}
94
95pub fn load_config(path: &std::path::Path) -> Result<Config> {
97 let Ok(bytes) = std::fs::read(path) else {
98 return Ok(Config::default());
99 };
100 match toml::from_str(&String::from_utf8_lossy(&bytes)) {
101 Ok(c) => Ok(c),
102 Err(e) => Err(Error::Serde(e.to_string())),
103 }
104}
105
106pub fn save_config(cfg: &Config, path: &std::path::Path) -> Result<()> {
108 if let Some(parent) = path.parent() {
109 std::fs::create_dir_all(parent).map_err(|e| ConfigError::Write {
110 path: path.to_path_buf(),
111 reason: e.to_string(),
112 })?;
113 }
114 let text = toml::to_string_pretty(cfg).map_err(|e| ConfigError::Write {
115 path: path.to_path_buf(),
116 reason: e.to_string(),
117 })?;
118 std::fs::write(path, text).map_err(|e| ConfigError::Write {
119 path: path.to_path_buf(),
120 reason: e.to_string(),
121 })?;
122 Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn defaults_are_sensible() {
131 let c = Config::default();
132 assert_eq!(c.mount_base, "/Volumes");
133 assert_eq!(c.fuse_driver, "auto");
134 assert!(c.require_confirmation);
135 assert!(c.color);
136 assert!(c.mount_options.is_empty());
137 }
138
139 #[test]
140 fn round_trip_persists_fields() {
141 let tmp = tempfile::tempdir().unwrap();
142 let path = tmp.path().join("config.toml");
143 let mut cfg = Config::default();
144 cfg.mount_base = "/tmp/vol".into();
145 cfg.mount_options.push("noowners".into());
146 cfg.require_confirmation = false;
147 save_config(&cfg, &path).unwrap();
148 let loaded = load_config(&path).unwrap();
149 assert_eq!(loaded.mount_base, "/tmp/vol");
150 assert_eq!(loaded.mount_options, vec!["noowners".to_string()]);
151 assert!(!loaded.require_confirmation);
152 }
153
154 #[test]
155 fn missing_file_returns_defaults() {
156 let cfg = load_config(std::path::Path::new("/nonexistent/path/config.toml")).unwrap();
157 assert_eq!(cfg.mount_base, "/Volumes");
158 }
159
160 #[test]
161 fn unknown_fields_are_ignored() {
162 let tmp = tempfile::tempdir().unwrap();
163 let path = tmp.path().join("config.toml");
164 std::fs::write(
165 &path,
166 r#"
167 mount_base = "/Volumes"
168 totally_unknown_field = 42
169 "#,
170 )
171 .unwrap();
172 let cfg = load_config(&path).unwrap();
173 assert_eq!(cfg.mount_base, "/Volumes");
174 }
175}