Skip to main content

prns_config/editing/
repair.rs

1use std::collections::BTreeSet;
2use std::fmt;
3
4use crate::configobj::{ConfigDocument, ConfigError};
5use crate::{parse_and_plan_named, ConfigDiagnostic, ConfigFix};
6
7use super::{ConfigEdit, InterfaceConfigKey, InterfaceName};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ConfigRepairReport {
11    diagnostics: Vec<ConfigDiagnostic>,
12}
13
14impl ConfigRepairReport {
15    pub fn analyze(source: &str) -> Result<Self, ConfigRepairError> {
16        Self::analyze_named("<config>", source)
17    }
18
19    pub fn analyze_named(
20        source_name: impl Into<String>,
21        source: &str,
22    ) -> Result<Self, ConfigRepairError> {
23        ConfigDocument::parse(source).map_err(ConfigRepairError::Syntax)?;
24        let diagnostics = match parse_and_plan_named(source_name, source) {
25            Ok(report) => report.warnings,
26            Err(errors) => errors.diagnostics().to_vec(),
27        };
28        Ok(Self { diagnostics })
29    }
30
31    pub fn diagnostics(&self) -> &[ConfigDiagnostic] {
32        &self.diagnostics
33    }
34
35    pub fn safe_edit(&self) -> Option<ConfigEdit> {
36        let mut disabled = BTreeSet::new();
37        let mut removed = BTreeSet::new();
38        for fix in self
39            .diagnostics
40            .iter()
41            .flat_map(ConfigDiagnostic::fixes)
42            .filter(|fix| fix.is_safe())
43        {
44            match fix {
45                ConfigFix::DisableInterface { name } => {
46                    if let Ok(name) = InterfaceName::new(name.clone()) {
47                        disabled.insert(name);
48                    }
49                }
50                ConfigFix::RemoveValue { path, .. } => {
51                    if let Some(target) = interface_value(path) {
52                        removed.insert(target);
53                    }
54                }
55                ConfigFix::InsertValue { .. }
56                | ConfigFix::ReplaceValue { .. }
57                | ConfigFix::ResolveAliases { .. }
58                | ConfigFix::ChooseInterfaceType { .. } => {}
59            }
60        }
61        if disabled.is_empty() && removed.is_empty() {
62            return None;
63        }
64        let mut edits = disabled
65            .into_iter()
66            .map(|name| ConfigEdit::SetEnabled {
67                name,
68                enabled: false,
69            })
70            .collect::<Vec<_>>();
71        edits.extend(
72            removed
73                .into_iter()
74                .map(|(name, key)| ConfigEdit::RemoveInterfaceValue { name, key }),
75        );
76        Some(ConfigEdit::Batch(edits))
77    }
78}
79
80fn interface_value(path: &str) -> Option<(InterfaceName, InterfaceConfigKey)> {
81    let start = path.find("[[")? + 2;
82    let rest = &path[start..];
83    let end = rest.find("]]")?;
84    if rest[end + 2..].contains("[[[") {
85        return None;
86    }
87    let name = InterfaceName::new(rest[..end].trim()).ok()?;
88    let key = InterfaceConfigKey::new(path.rsplit(" > ").next()?.trim()).ok()?;
89    Some((name, key))
90}
91
92#[derive(Debug)]
93pub enum ConfigRepairError {
94    Syntax(ConfigError),
95}
96
97impl fmt::Display for ConfigRepairError {
98    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::Syntax(error) => write!(
101                formatter,
102                "{error}; malformed ConfigObj syntax is preserved for manual correction"
103            ),
104        }
105    }
106}
107
108impl std::error::Error for ConfigRepairError {}