1use std::fmt;
2
3use crate::configobj::SourceLocations;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ConfigSeverity {
7 Warning,
8 Error,
9}
10
11impl fmt::Display for ConfigSeverity {
12 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
13 match self {
14 ConfigSeverity::Warning => formatter.write_str("warning"),
15 ConfigSeverity::Error => formatter.write_str("error"),
16 }
17 }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ConfigDiagnosticCode {
22 Syntax,
23 MisplacedKey,
24 UnknownKey,
25 PersistedRuntimeMetadata,
26 UnknownSection,
27 MissingRequiredKey,
28 InvalidValue,
29 ConflictingAliases,
30 RedundantAliases,
31 UnsupportedInterface,
32 UnsupportedTransport,
33 UnsupportedSetting,
34 IneffectiveSetting,
35 ImplicitOff,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ConfigFix {
40 DisableInterface {
41 name: String,
42 },
43 InsertValue {
44 path: String,
45 accepted: String,
46 },
47 ReplaceValue {
48 path: String,
49 accepted: String,
50 },
51 RemoveValue {
52 path: String,
53 safety: ConfigFixSafety,
54 },
55 ResolveAliases {
56 path: String,
57 aliases: Vec<String>,
58 },
59 ChooseInterfaceType {
60 name: String,
61 },
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ConfigFixSafety {
66 Safe,
67 Guided,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SecretDisplay {
72 Redacted,
73 Revealed,
74}
75
76impl ConfigFix {
77 pub const fn is_safe(&self) -> bool {
78 matches!(
79 self,
80 Self::DisableInterface { .. }
81 | Self::RemoveValue {
82 safety: ConfigFixSafety::Safe,
83 ..
84 }
85 )
86 }
87}
88
89impl ConfigDiagnosticCode {
90 pub const fn as_str(self) -> &'static str {
91 match self {
92 ConfigDiagnosticCode::Syntax => "syntax",
93 ConfigDiagnosticCode::MisplacedKey => "misplaced_key",
94 ConfigDiagnosticCode::UnknownKey => "unknown_key",
95 ConfigDiagnosticCode::PersistedRuntimeMetadata => "persisted_runtime_metadata",
96 ConfigDiagnosticCode::UnknownSection => "unknown_section",
97 ConfigDiagnosticCode::MissingRequiredKey => "missing_required_key",
98 ConfigDiagnosticCode::InvalidValue => "invalid_value",
99 ConfigDiagnosticCode::ConflictingAliases => "conflicting_aliases",
100 ConfigDiagnosticCode::RedundantAliases => "redundant_aliases",
101 ConfigDiagnosticCode::UnsupportedInterface => "unsupported_interface",
102 ConfigDiagnosticCode::UnsupportedTransport => "unsupported_transport",
103 ConfigDiagnosticCode::UnsupportedSetting => "unsupported_setting",
104 ConfigDiagnosticCode::IneffectiveSetting => "ineffective_setting",
105 ConfigDiagnosticCode::ImplicitOff => "implicit_off",
106 }
107 }
108
109 pub const fn severity(self) -> ConfigSeverity {
110 match self {
111 ConfigDiagnosticCode::UnknownKey
112 | ConfigDiagnosticCode::PersistedRuntimeMetadata
113 | ConfigDiagnosticCode::UnknownSection
114 | ConfigDiagnosticCode::RedundantAliases
115 | ConfigDiagnosticCode::UnsupportedSetting
116 | ConfigDiagnosticCode::IneffectiveSetting
117 | ConfigDiagnosticCode::ImplicitOff => ConfigSeverity::Warning,
118 ConfigDiagnosticCode::Syntax
119 | ConfigDiagnosticCode::MisplacedKey
120 | ConfigDiagnosticCode::MissingRequiredKey
121 | ConfigDiagnosticCode::InvalidValue
122 | ConfigDiagnosticCode::ConflictingAliases
123 | ConfigDiagnosticCode::UnsupportedInterface
124 | ConfigDiagnosticCode::UnsupportedTransport => ConfigSeverity::Error,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct ConfigDiagnostic {
131 code: ConfigDiagnosticCode,
132 source: String,
133 line: usize,
134 path: String,
135 value: Option<String>,
136 message: String,
137 accepted: Option<String>,
138 correction: String,
139 fixes: Vec<ConfigFix>,
140}
141
142impl ConfigDiagnostic {
143 #[allow(clippy::too_many_arguments)]
144 pub(crate) fn new(
145 code: ConfigDiagnosticCode,
146 source: impl Into<String>,
147 line: usize,
148 path: impl Into<String>,
149 value: Option<String>,
150 message: impl Into<String>,
151 accepted: Option<String>,
152 correction: impl Into<String>,
153 ) -> Self {
154 let path = path.into();
155 let fixes = default_fixes(code, &path, accepted.as_deref());
156 Self {
157 code,
158 source: source.into(),
159 line,
160 path,
161 value,
162 message: message.into(),
163 accepted,
164 correction: correction.into(),
165 fixes,
166 }
167 }
168
169 pub const fn severity(&self) -> ConfigSeverity {
170 self.code.severity()
171 }
172
173 pub const fn code(&self) -> ConfigDiagnosticCode {
174 self.code
175 }
176
177 pub fn source(&self) -> &str {
178 &self.source
179 }
180
181 pub const fn line(&self) -> usize {
182 self.line
183 }
184
185 pub fn path(&self) -> &str {
186 &self.path
187 }
188
189 pub fn value(&self) -> Option<&str> {
190 self.value.as_deref()
191 }
192
193 pub fn message(&self) -> &str {
194 &self.message
195 }
196
197 pub fn accepted(&self) -> Option<&str> {
198 self.accepted.as_deref()
199 }
200
201 pub fn correction(&self) -> &str {
202 &self.correction
203 }
204
205 pub fn fixes(&self) -> &[ConfigFix] {
206 &self.fixes
207 }
208
209 pub(crate) fn with_fixes(mut self, fixes: Vec<ConfigFix>) -> Self {
210 self.fixes = fixes;
211 self
212 }
213
214 pub fn display_with(&self, secrets: SecretDisplay) -> DisplayedConfigDiagnostic<'_> {
215 DisplayedConfigDiagnostic {
216 diagnostic: self,
217 secrets,
218 }
219 }
220}
221
222fn default_fixes(code: ConfigDiagnosticCode, path: &str, accepted: Option<&str>) -> Vec<ConfigFix> {
223 let interface = interface_name(path);
224 match code {
225 ConfigDiagnosticCode::MissingRequiredKey if path.ends_with(" > type") => interface
226 .into_iter()
227 .flat_map(|name| {
228 [
229 ConfigFix::ChooseInterfaceType { name: name.clone() },
230 ConfigFix::DisableInterface { name },
231 ]
232 })
233 .collect(),
234 ConfigDiagnosticCode::MissingRequiredKey => accepted
235 .map(|accepted| ConfigFix::InsertValue {
236 path: path.to_string(),
237 accepted: accepted.to_string(),
238 })
239 .into_iter()
240 .chain(interface.map(|name| ConfigFix::DisableInterface { name }))
241 .collect(),
242 ConfigDiagnosticCode::InvalidValue => accepted
243 .map(|accepted| ConfigFix::ReplaceValue {
244 path: path.to_string(),
245 accepted: accepted.to_string(),
246 })
247 .into_iter()
248 .chain(interface.map(|name| ConfigFix::DisableInterface { name }))
249 .collect(),
250 ConfigDiagnosticCode::ConflictingAliases => vec![ConfigFix::ResolveAliases {
251 path: path.to_string(),
252 aliases: Vec::new(),
253 }],
254 ConfigDiagnosticCode::RedundantAliases => {
255 vec![ConfigFix::RemoveValue {
256 path: path.to_string(),
257 safety: ConfigFixSafety::Safe,
258 }]
259 }
260 ConfigDiagnosticCode::UnknownKey => vec![ConfigFix::RemoveValue {
261 path: path.to_string(),
262 safety: ConfigFixSafety::Guided,
263 }],
264 ConfigDiagnosticCode::PersistedRuntimeMetadata => vec![ConfigFix::RemoveValue {
265 path: path.to_string(),
266 safety: ConfigFixSafety::Safe,
267 }],
268 ConfigDiagnosticCode::UnsupportedInterface => interface
269 .into_iter()
270 .flat_map(|name| {
271 [
272 ConfigFix::ChooseInterfaceType { name: name.clone() },
273 ConfigFix::DisableInterface { name },
274 ]
275 })
276 .collect(),
277 ConfigDiagnosticCode::UnsupportedTransport => interface
278 .map(|name| vec![ConfigFix::DisableInterface { name }])
279 .unwrap_or_default(),
280 ConfigDiagnosticCode::ImplicitOff => vec![ConfigFix::ReplaceValue {
281 path: path.to_string(),
282 accepted: "off".to_string(),
283 }],
284 ConfigDiagnosticCode::Syntax
285 | ConfigDiagnosticCode::MisplacedKey
286 | ConfigDiagnosticCode::UnknownSection
287 | ConfigDiagnosticCode::UnsupportedSetting
288 | ConfigDiagnosticCode::IneffectiveSetting => Vec::new(),
289 }
290}
291
292fn interface_name(path: &str) -> Option<String> {
293 let start = path.find("[[")? + 2;
294 let rest = &path[start..];
295 let end = rest.find("]]")?;
296 let name = rest[..end].trim();
297 (!name.is_empty()).then(|| name.to_string())
298}
299
300impl fmt::Display for ConfigDiagnostic {
301 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302 self.display_with(SecretDisplay::Redacted).fmt(formatter)
303 }
304}
305
306pub struct DisplayedConfigDiagnostic<'a> {
307 diagnostic: &'a ConfigDiagnostic,
308 secrets: SecretDisplay,
309}
310
311impl fmt::Display for DisplayedConfigDiagnostic<'_> {
312 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
313 let diagnostic = self.diagnostic;
314 write!(
315 formatter,
316 "{}:{}: {}[{}] {}: {}",
317 diagnostic.source,
318 diagnostic.line,
319 diagnostic.severity(),
320 diagnostic.code.as_str(),
321 diagnostic.path,
322 diagnostic.message,
323 )?;
324 if let Some(value) = &diagnostic.value {
325 if self.secrets == SecretDisplay::Revealed || !secret_path(&diagnostic.path) {
326 write!(formatter, "; found {value:?}")?;
327 } else {
328 formatter.write_str("; found <redacted>")?;
329 }
330 }
331 if let Some(accepted) = &diagnostic.accepted {
332 write!(formatter, "; accepted: {accepted}")?;
333 }
334 if self.secrets == SecretDisplay::Redacted && secret_path(&diagnostic.path) {
335 formatter.write_str("; fix: correct the secret-bearing setting")
336 } else {
337 write!(formatter, "; fix: {}", diagnostic.correction)
338 }
339 }
340}
341
342fn secret_path(path: &str) -> bool {
343 matches!(
344 path.rsplit(" > ").next().map(str::trim),
345 Some("pass_phrase" | "passphrase" | "rpc_key")
346 )
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct ConfigErrors {
351 diagnostics: Vec<ConfigDiagnostic>,
352}
353
354impl ConfigErrors {
355 pub(crate) fn new(diagnostics: Vec<ConfigDiagnostic>) -> Self {
356 Self { diagnostics }
357 }
358
359 pub fn diagnostics(&self) -> &[ConfigDiagnostic] {
360 &self.diagnostics
361 }
362
363 pub fn len(&self) -> usize {
364 self.diagnostics.len()
365 }
366
367 pub fn is_empty(&self) -> bool {
368 self.diagnostics.is_empty()
369 }
370}
371
372impl fmt::Display for ConfigErrors {
373 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374 for (index, diagnostic) in self.diagnostics.iter().enumerate() {
375 if index != 0 {
376 formatter.write_str("\n")?;
377 }
378 write!(formatter, "{diagnostic}")?;
379 }
380 Ok(())
381 }
382}
383
384impl std::error::Error for ConfigErrors {}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct ConfigReport<T> {
388 pub value: T,
389 pub warnings: Vec<ConfigDiagnostic>,
390 pub source: String,
391 pub locations: SourceLocations,
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn diagnostics_redact_secret_values_unless_revealed() {
400 let diagnostic = ConfigDiagnostic::new(
401 ConfigDiagnosticCode::InvalidValue,
402 "config",
403 4,
404 "[interfaces] > [[WiFi]] > pass_phrase",
405 Some("private value".to_string()),
406 "invalid passphrase",
407 Some("a valid passphrase".to_string()),
408 "replace the value",
409 );
410
411 assert!(!diagnostic.to_string().contains("private value"));
412 assert!(diagnostic
413 .display_with(SecretDisplay::Revealed)
414 .to_string()
415 .contains("private value"));
416 }
417}