1use std::path::{Path, PathBuf};
2
3use agent_first_data::document::{
4 CreateOptions, Document, DocumentError, DocumentFile, Format, Value,
5};
6
7use super::HyphaConfig;
8
9const CONFIG_MAX_BYTES: u64 = 1024 * 1024;
10
11const CONFIG_KEYS: &[&str] = &[
12 "cache.path",
13 "cache.cmn_ttl_s",
14 "cache.key_trust_ttl_s",
15 "cache.key_trust_refresh_mode",
16 "cache.key_trust_synapse_witness_mode",
17 "cache.spore_max_download_bytes",
18 "cache.spore_max_extract_bytes",
19 "cache.spore_max_extract_files",
20 "cache.spore_max_extract_file_bytes",
21 "cache.spore_reject_path_components",
22 "cache.clock_skew_tolerance_s",
23 "cache.require_domain_first_key",
24 "defaults.synapse_domain",
25 "defaults.domain",
26 "defaults.taste.synapse_domain",
27 "defaults.taste.domain",
28];
29
30impl HyphaConfig {
31 pub fn load() -> Result<Self, crate::sink::HyphaError> {
32 use crate::sink::HyphaError;
33
34 let path = config_path();
35 match std::fs::symlink_metadata(&path) {
36 Ok(_) => {}
37 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
38 return Ok(Self::default());
39 }
40 Err(error) => {
41 return Err(HyphaError::with_hint(
42 "config_read_failed",
43 format!("Failed to inspect {}: {}", path.display(), error),
44 "fix the file permissions and retry",
45 ));
46 }
47 }
48
49 let doc = DocumentFile::open_capped(&path, Some(Format::Toml), CONFIG_MAX_BYTES)
50 .map_err(|error| config_load_error(&path, &error))?;
51 agent_first_data::document::from_value(doc.value(), "")
52 .map_err(|error| config_load_error(&path, &error))
53 }
54
55 pub fn save(&self) -> Result<(), crate::sink::HyphaError> {
56 use crate::sink::HyphaError;
57
58 let path = config_path();
59 if let Some(parent) = path.parent() {
60 std::fs::create_dir_all(parent).map_err(|e| {
61 HyphaError::new(
62 "config_save_failed",
63 format!("Failed to create config directory: {}", e),
64 )
65 })?;
66 }
67
68 let desired_source = toml::to_string_pretty(self).map_err(|e| {
69 HyphaError::new(
70 "config_save_failed",
71 format!("Failed to serialize config: {}", e),
72 )
73 })?;
74 let desired = Document::parse(&desired_source, Format::Toml).map_err(|error| {
75 config_document_error("config_save_failed", &path, "prepare configuration", &error)
76 })?;
77 let default_source = toml::to_string_pretty(&Self::default()).map_err(|error| {
78 HyphaError::new(
79 "config_save_failed",
80 format!("Failed to serialize default config: {error}"),
81 )
82 })?;
83 let defaults = Document::parse(&default_source, Format::Toml).map_err(|error| {
84 config_document_error(
85 "config_save_failed",
86 &path,
87 "prepare default configuration",
88 &error,
89 )
90 })?;
91
92 match std::fs::symlink_metadata(&path) {
93 Ok(_) => save_existing_config(&path, &desired, &defaults),
94 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
95 let options = CreateOptions::new().unix_mode(0o600);
96 DocumentFile::create_atomic(&path, desired, options)
97 .map(|_| ())
98 .map_err(|error| {
99 config_document_error(
100 "config_save_failed",
101 &path,
102 "create configuration",
103 &error,
104 )
105 })
106 }
107 Err(error) => Err(HyphaError::with_hint(
108 "config_save_failed",
109 format!("Failed to inspect {}: {}", path.display(), error),
110 "fix the file permissions and retry",
111 )),
112 }
113 }
114}
115
116fn save_existing_config(
117 path: &Path,
118 desired: &Document,
119 defaults: &Document,
120) -> Result<(), crate::sink::HyphaError> {
121 let mut current = DocumentFile::open_capped(path, Some(Format::Toml), CONFIG_MAX_BYTES)
122 .map_err(|error| config_load_error(path, &error))?;
123 let mut changed = false;
124
125 for key in CONFIG_KEYS {
126 let desired_value = optional_value(desired, key).map_err(|error| {
127 config_document_error(
128 "config_save_failed",
129 path,
130 "read prepared configuration",
131 &error,
132 )
133 })?;
134 let default_value = optional_value(defaults, key).map_err(|error| {
135 config_document_error(
136 "config_save_failed",
137 path,
138 "read default configuration",
139 &error,
140 )
141 })?;
142 let current_value = optional_value(¤t, key).map_err(|error| {
143 config_document_error(
144 "config_save_failed",
145 path,
146 "read existing configuration",
147 &error,
148 )
149 })?;
150
151 match (desired_value, current_value) {
152 (Some(desired_value), Some(current_value)) if desired_value == current_value => {}
153 (Some(desired_value), None) if Some(&desired_value) == default_value.as_ref() => {}
154 (Some(desired_value), _) => {
155 current.set(key, desired_value).map_err(|error| {
156 config_document_error(
157 "config_save_failed",
158 path,
159 "stage configuration update",
160 &error,
161 )
162 })?;
163 changed = true;
164 }
165 (None, Some(_)) => {
166 current.unset(key).map_err(|error| {
167 config_document_error(
168 "config_save_failed",
169 path,
170 "stage configuration removal",
171 &error,
172 )
173 })?;
174 changed = true;
175 }
176 (None, None) => {}
177 }
178 }
179
180 if !changed {
181 return Ok(());
182 }
183
184 current.decode::<HyphaConfig>().map_err(|error| {
185 config_document_error(
186 "config_save_failed",
187 path,
188 "validate updated configuration",
189 &error,
190 )
191 })?;
192 current.save().map_err(|error| {
193 config_document_error("config_save_failed", path, "commit configuration", &error)
194 })
195}
196
197fn optional_value(doc: &Document, key: &str) -> Result<Option<Value>, DocumentError> {
198 match doc.value_at(key) {
199 Ok(value) => Ok(Some(value)),
200 Err(error) if error.code() == "document_path_not_found" => Ok(None),
201 Err(error) => Err(error),
202 }
203}
204
205fn config_load_error(path: &Path, error: &DocumentError) -> crate::sink::HyphaError {
206 let code = match error.code() {
207 "document_parse_failed" | "document_type_mismatch" => "config_parse_failed",
208 _ => "config_read_failed",
209 };
210 config_document_error(code, path, "load configuration", error)
211}
212
213fn config_document_error(
214 code: &str,
215 path: &Path,
216 operation: &str,
217 error: &DocumentError,
218) -> crate::sink::HyphaError {
219 crate::sink::HyphaError::with_hint(
220 code,
221 format!(
222 "Failed to {} at {}: {} ({})",
223 operation,
224 path.display(),
225 error.redacted_message(),
226 error.code()
227 ),
228 "fix the configuration file or its permissions and retry",
229 )
230}
231
232pub(super) fn write_text_file_atomic(
233 path: &Path,
234 content: &str,
235 mode: u32,
236 error_code: &str,
237 file_label: &str,
238) -> Result<(), crate::sink::HyphaError> {
239 use crate::sink::HyphaError;
240
241 let parent = path.parent().ok_or_else(|| {
242 HyphaError::new(
243 error_code,
244 format!("Failed to determine parent directory for {}", file_label),
245 )
246 })?;
247 crate::cache::fs::atomic_write_file(path, content.as_bytes(), parent, Some(mode))
248 .map_err(|e| HyphaError::new(error_code, format!("Failed to write {}: {}", file_label, e)))
249}
250
251pub fn config_path() -> PathBuf {
252 hypha_dir().join("config.toml")
253}
254
255pub fn hypha_dir() -> PathBuf {
257 crate::site::get_cmn_home().join("hypha")
258}