1use std::fmt;
2use std::ops::Range;
3
4use crate::configobj::{ConfigDocument, ConfigError, Value};
5use crate::reference::keys::{interface as interface_key, section as section_key};
6use crate::{parse_and_plan_named, ConfigDiagnostic, ConfigErrors, InterfaceKind, SecretDisplay};
7
8use super::catalog::ConfiguredInterfaceSetting;
9use super::interface::{
10 render_bool, render_value, InterfaceConfigKey, InterfaceDefinition, InterfaceName,
11 InterfaceSetting, InterfaceSettingKey, RNodeMultiRadioDefinition,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ConfiguredInterface {
16 name: InterfaceName,
17 configured_type: Option<String>,
18 kind: Option<InterfaceKind>,
19 enabled: Option<bool>,
20 settings: Vec<ConfiguredInterfaceSetting>,
21 rnode_multi_radios: Vec<RNodeMultiRadioDefinition>,
22}
23
24impl ConfiguredInterface {
25 pub fn name(&self) -> &InterfaceName {
26 &self.name
27 }
28
29 pub fn configured_type(&self) -> Option<&str> {
30 self.configured_type.as_deref()
31 }
32
33 pub const fn kind(&self) -> Option<InterfaceKind> {
34 self.kind
35 }
36
37 pub const fn enabled(&self) -> Option<bool> {
38 self.enabled
39 }
40
41 pub fn settings(&self) -> &[ConfiguredInterfaceSetting] {
42 &self.settings
43 }
44
45 pub fn rnode_multi_radios(&self) -> &[RNodeMultiRadioDefinition] {
46 &self.rnode_multi_radios
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub enum InterfaceSettingChange {
52 Set(InterfaceSetting),
53 Remove(InterfaceSettingKey),
54}
55
56#[derive(Debug, Clone, PartialEq)]
57pub enum ConfigEdit {
58 Add(InterfaceDefinition),
59 Replace {
60 current: InterfaceName,
61 replacement: InterfaceDefinition,
62 },
63 Rename {
64 current: InterfaceName,
65 replacement: InterfaceName,
66 },
67 SetEnabled {
68 name: InterfaceName,
69 enabled: bool,
70 },
71 SetType {
72 name: InterfaceName,
73 kind: InterfaceKind,
74 },
75 ChangeSettings {
76 name: InterfaceName,
77 changes: Vec<InterfaceSettingChange>,
78 },
79 ReplaceRNodeMultiRadios {
80 name: InterfaceName,
81 radios: Vec<RNodeMultiRadioDefinition>,
82 },
83 Remove(InterfaceName),
84 RemoveInterfaceValue {
85 name: InterfaceName,
86 key: InterfaceConfigKey,
87 },
88 Batch(Vec<ConfigEdit>),
89}
90
91#[derive(Debug, Clone, PartialEq)]
92pub struct EditedConfig {
93 original: String,
94 candidate: String,
95 warnings: Vec<ConfigDiagnostic>,
96}
97
98impl EditedConfig {
99 pub fn original(&self) -> &str {
100 &self.original
101 }
102
103 pub fn candidate(&self) -> &str {
104 &self.candidate
105 }
106
107 pub fn warnings(&self) -> &[ConfigDiagnostic] {
108 &self.warnings
109 }
110
111 pub fn diff(&self, secrets: SecretDisplay) -> String {
112 render_diff(&self.original, &self.candidate, secrets)
113 }
114}
115
116impl ConfigDocument {
117 pub fn interfaces(&self) -> Vec<ConfiguredInterface> {
118 self.child_section_names(&[section_key::INTERFACES])
119 .into_iter()
120 .filter_map(|name| {
121 let name = InterfaceName::new(name).ok()?;
122 let path = [section_key::INTERFACES, name.as_str()];
123 let configured_type = self
124 .section_value(&path, interface_key::TYPE)
125 .and_then(Value::as_scalar)
126 .map(str::to_string);
127 let kind = configured_type.as_deref().and_then(InterfaceKind::parse);
128 let enabled = interface_enabled(self, &path);
129 let settings = kind
130 .map(|kind| configured_settings(self, name.as_str(), kind))
131 .unwrap_or_default();
132 let rnode_multi_radios = if kind == Some(InterfaceKind::RnodeMulti) {
133 configured_radios(self, name.as_str())
134 } else {
135 Vec::new()
136 };
137 Some(ConfiguredInterface {
138 name,
139 configured_type,
140 kind,
141 enabled,
142 settings,
143 rnode_multi_radios,
144 })
145 })
146 .collect()
147 }
148
149 pub fn edit(&self, edit: &ConfigEdit) -> Result<EditedConfig, ConfigEditError> {
150 self.edit_named("<edited config>", edit)
151 }
152
153 pub fn edit_named(
154 &self,
155 source_name: impl Into<String>,
156 edit: &ConfigEdit,
157 ) -> Result<EditedConfig, ConfigEditError> {
158 let candidate = mutate(self.source(), edit)?;
159 let report =
160 parse_and_plan_named(source_name, &candidate).map_err(ConfigEditError::Invalid)?;
161 Ok(EditedConfig {
162 original: self.source().to_string(),
163 candidate,
164 warnings: report.warnings,
165 })
166 }
167}
168
169#[derive(Debug)]
170pub enum ConfigEditError {
171 Syntax(ConfigError),
172 InterfaceNotFound(InterfaceName),
173 DuplicateInterface(InterfaceName),
174 Invalid(ConfigErrors),
175}
176
177impl fmt::Display for ConfigEditError {
178 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match self {
180 Self::Syntax(error) => error.fmt(formatter),
181 Self::InterfaceNotFound(name) => write!(formatter, "interface {name} was not found"),
182 Self::DuplicateInterface(name) => {
183 write!(formatter, "interface {name} already exists")
184 }
185 Self::Invalid(errors) => errors.fmt(formatter),
186 }
187 }
188}
189
190impl std::error::Error for ConfigEditError {}
191
192fn mutate(source: &str, edit: &ConfigEdit) -> Result<String, ConfigEditError> {
193 match edit {
194 ConfigEdit::Batch(edits) => {
195 let mut candidate = source.to_string();
196 for edit in edits {
197 candidate = mutate(&candidate, edit)?;
198 }
199 Ok(candidate)
200 }
201 _ => mutate_one(
202 ConfigDocument::parse(source).map_err(ConfigEditError::Syntax)?,
203 edit,
204 ),
205 }
206}
207
208fn mutate_one(document: ConfigDocument, edit: &ConfigEdit) -> Result<String, ConfigEditError> {
209 match edit {
210 ConfigEdit::Add(definition) => add_interface(&document, definition),
211 ConfigEdit::Replace {
212 current,
213 replacement,
214 } => replace_interface(&document, current, replacement),
215 ConfigEdit::Rename {
216 current,
217 replacement,
218 } => rename_interface(&document, current, replacement),
219 ConfigEdit::SetEnabled { name, enabled } => set_enabled(&document, name, *enabled),
220 ConfigEdit::SetType { name, kind } => set_type(&document, name, *kind),
221 ConfigEdit::ChangeSettings { name, changes } => change_settings(&document, name, changes),
222 ConfigEdit::ReplaceRNodeMultiRadios { name, radios } => {
223 replace_rnode_multi_radios(&document, name, radios)
224 }
225 ConfigEdit::Remove(name) => remove_interface(&document, name),
226 ConfigEdit::RemoveInterfaceValue { name, key } => {
227 remove_interface_value(&document, name, key)
228 }
229 ConfigEdit::Batch(edits) => {
230 let mut candidate = document.source().to_string();
231 for edit in edits {
232 candidate = mutate(&candidate, edit)?;
233 }
234 Ok(candidate)
235 }
236 }
237}
238
239fn add_interface(
240 document: &ConfigDocument,
241 definition: &InterfaceDefinition,
242) -> Result<String, ConfigEditError> {
243 ensure_interface_absent(document, definition.name())?;
244 let newline = document.newline();
245 let rendered = definition.render(newline);
246 let Some(interfaces) = document.section_range(&[section_key::INTERFACES]) else {
247 let mut candidate = document.source().to_string();
248 ensure_trailing_newline(&mut candidate, newline);
249 candidate.push_str(&format!("[{}]{newline}{rendered}", section_key::INTERFACES));
250 return Ok(candidate);
251 };
252 let insertion = interfaces.end;
253 let mut block = String::new();
254 if insertion > 0 && !document.source()[..insertion].ends_with(['\n', '\r']) {
255 block.push_str(newline);
256 }
257 block.push_str(&rendered);
258 Ok(replace_range(
259 document.source(),
260 insertion..insertion,
261 &block,
262 ))
263}
264
265fn replace_interface(
266 document: &ConfigDocument,
267 current: &InterfaceName,
268 replacement: &InterfaceDefinition,
269) -> Result<String, ConfigEditError> {
270 let range = interface_range(document, current)?;
271 if current != replacement.name() {
272 ensure_interface_absent(document, replacement.name())?;
273 }
274 Ok(replace_range(
275 document.source(),
276 range,
277 &replacement.render(document.newline()),
278 ))
279}
280
281fn rename_interface(
282 document: &ConfigDocument,
283 current: &InterfaceName,
284 replacement: &InterfaceName,
285) -> Result<String, ConfigEditError> {
286 ensure_interface_absent(document, replacement)?;
287 let header = document
288 .section_header_range(&[section_key::INTERFACES, current.as_str()])
289 .ok_or_else(|| ConfigEditError::InterfaceNotFound(current.clone()))?;
290 let original = &document.source()[header.clone()];
291 let indent = original.len() - original.trim_start().len();
292 let suffix = section_header_suffix(original, 2);
293 let rendered = format!("{}[[{}]]{}", &original[..indent], replacement, suffix,);
294 Ok(replace_range(document.source(), header, &rendered))
295}
296
297fn set_enabled(
298 document: &ConfigDocument,
299 name: &InterfaceName,
300 enabled: bool,
301) -> Result<String, ConfigEditError> {
302 interface_range(document, name)?;
303 let explicit_path = [
304 section_key::INTERFACES,
305 name.as_str(),
306 interface_key::INTERFACE_ENABLED,
307 ];
308 let shorthand_path = [
309 section_key::INTERFACES,
310 name.as_str(),
311 interface_key::ENABLED,
312 ];
313 let mut replacements = Vec::new();
314 match document.key_range(&explicit_path) {
315 Some(range) => replacements.push((
316 range.clone(),
317 render_key_line(
318 document.source(),
319 range,
320 interface_key::INTERFACE_ENABLED,
321 render_bool(enabled),
322 document.newline(),
323 ),
324 )),
325 None => {
326 let insertion = key_insertion(document, name)?;
327 replacements.push((
328 insertion..insertion,
329 format!(
330 " {} = {}{}",
331 interface_key::INTERFACE_ENABLED,
332 render_bool(enabled),
333 document.newline(),
334 ),
335 ));
336 }
337 }
338 if let Some(range) = document.key_range(&shorthand_path) {
339 replacements.push((range, String::new()));
340 }
341 Ok(apply_replacements(document.source(), replacements))
342}
343
344fn set_type(
345 document: &ConfigDocument,
346 name: &InterfaceName,
347 kind: InterfaceKind,
348) -> Result<String, ConfigEditError> {
349 interface_range(document, name)?;
350 let path = [section_key::INTERFACES, name.as_str(), interface_key::TYPE];
351 let replacement = match document.key_range(&path) {
352 Some(range) => {
353 let line = render_key_line(
354 document.source(),
355 range.clone(),
356 interface_key::TYPE,
357 kind.canonical_name(),
358 document.newline(),
359 );
360 replace_range(document.source(), range, &line)
361 }
362 None => {
363 let insertion = key_insertion(document, name)?;
364 replace_range(
365 document.source(),
366 insertion..insertion,
367 &format!(
368 " {} = {}{}",
369 interface_key::TYPE,
370 kind.canonical_name(),
371 document.newline()
372 ),
373 )
374 }
375 };
376 Ok(replacement)
377}
378
379fn change_settings(
380 document: &ConfigDocument,
381 name: &InterfaceName,
382 changes: &[InterfaceSettingChange],
383) -> Result<String, ConfigEditError> {
384 interface_range(document, name)?;
385 let mut candidate = document.source().to_string();
386 for change in changes {
387 let parsed = ConfigDocument::parse(&candidate).map_err(ConfigEditError::Syntax)?;
388 match change {
389 InterfaceSettingChange::Set(setting) => {
390 let key = setting.key().canonical();
391 candidate = remove_aliases(&parsed, name, key, false);
392 let parsed = ConfigDocument::parse(&candidate).map_err(ConfigEditError::Syntax)?;
393 let path = [section_key::INTERFACES, name.as_str(), key.as_str()];
394 let value = render_value(setting.value());
395 candidate = match parsed.key_range(&path) {
396 Some(range) => {
397 let line = render_key_line(
398 parsed.source(),
399 range.clone(),
400 key.as_str(),
401 &value,
402 parsed.newline(),
403 );
404 replace_range(parsed.source(), range, &line)
405 }
406 None => {
407 let insertion = key_insertion(&parsed, name)?;
408 replace_range(
409 parsed.source(),
410 insertion..insertion,
411 &format!(" {} = {}{}", key.as_str(), value, parsed.newline()),
412 )
413 }
414 };
415 }
416 InterfaceSettingChange::Remove(key) => {
417 candidate = remove_aliases(&parsed, name, key.canonical(), true);
418 }
419 }
420 }
421 Ok(candidate)
422}
423
424fn remove_aliases(
425 document: &ConfigDocument,
426 name: &InterfaceName,
427 key: InterfaceSettingKey,
428 include_canonical: bool,
429) -> String {
430 let aliases = key.aliases();
431 let keys = if aliases.is_empty() {
432 vec![key.as_str()]
433 } else {
434 aliases.to_vec()
435 };
436 let replacements = keys
437 .into_iter()
438 .filter(|candidate| include_canonical || *candidate != key.as_str())
439 .filter_map(|candidate| {
440 document
441 .key_range(&[section_key::INTERFACES, name.as_str(), candidate])
442 .map(|range| (range, String::new()))
443 })
444 .collect();
445 apply_replacements(document.source(), replacements)
446}
447
448fn remove_interface_value(
449 document: &ConfigDocument,
450 name: &InterfaceName,
451 key: &InterfaceConfigKey,
452) -> Result<String, ConfigEditError> {
453 interface_range(document, name)?;
454 let path = [section_key::INTERFACES, name.as_str(), key.as_str()];
455 Ok(match document.key_range(&path) {
456 Some(range) => replace_range(document.source(), range, ""),
457 None => document.source().to_string(),
458 })
459}
460
461fn configured_settings(
462 document: &ConfigDocument,
463 name: &str,
464 kind: InterfaceKind,
465) -> Vec<ConfiguredInterfaceSetting> {
466 let Some(section) = document
467 .root()
468 .section(section_key::INTERFACES)
469 .and_then(|interfaces| interfaces.section(name))
470 else {
471 return Vec::new();
472 };
473 kind.setting_specs()
474 .into_iter()
475 .filter_map(|spec| {
476 let key = spec.key();
477 let aliases = key.aliases();
478 let keys = if aliases.is_empty() {
479 vec![key.as_str()]
480 } else {
481 aliases.to_vec()
482 };
483 keys.into_iter().find_map(|source_key| {
484 let value = section.get(source_key)?;
485 let value = match value {
486 Value::Scalar(value) => value.clone(),
487 Value::List(values) => values.join(", "),
488 };
489 Some(ConfiguredInterfaceSetting::new(
490 spec,
491 source_key.to_string(),
492 value,
493 ))
494 })
495 })
496 .collect()
497}
498
499fn configured_radios(document: &ConfigDocument, name: &str) -> Vec<RNodeMultiRadioDefinition> {
500 let Some(section) = document
501 .root()
502 .section(section_key::INTERFACES)
503 .and_then(|interfaces| interfaces.section(name))
504 else {
505 return Vec::new();
506 };
507 section
508 .sections
509 .iter()
510 .filter_map(|(name, radio)| {
511 let scalar = |key: &str| radio.get(key).and_then(Value::as_scalar);
512 let enabled = scalar(interface_key::INTERFACE_ENABLED)
513 .or_else(|| scalar(interface_key::ENABLED))
514 .and_then(|value| match value.trim().to_ascii_lowercase().as_str() {
515 "yes" | "true" | "on" | "1" => Some(true),
516 "no" | "false" | "off" | "0" => Some(false),
517 _ => None,
518 })
519 .unwrap_or(true);
520 if !enabled {
521 return None;
522 }
523 RNodeMultiRadioDefinition::new(
524 InterfaceName::new(name.clone()).ok()?,
525 scalar(interface_key::VPORT)?.parse().ok()?,
526 scalar(interface_key::FREQUENCY)?.parse().ok()?,
527 scalar(interface_key::BANDWIDTH)?.parse().ok()?,
528 scalar(interface_key::TXPOWER)?.parse().ok()?,
529 scalar(interface_key::SPREADINGFACTOR)?.parse().ok()?,
530 scalar(interface_key::CODINGRATE)?.parse().ok()?,
531 )
532 .ok()
533 })
534 .collect()
535}
536
537fn remove_interface(
538 document: &ConfigDocument,
539 name: &InterfaceName,
540) -> Result<String, ConfigEditError> {
541 let range = interface_range(document, name)?;
542 Ok(replace_range(document.source(), range, ""))
543}
544
545fn replace_rnode_multi_radios(
546 document: &ConfigDocument,
547 name: &InterfaceName,
548 radios: &[RNodeMultiRadioDefinition],
549) -> Result<String, ConfigEditError> {
550 let parent_path = [section_key::INTERFACES, name.as_str()];
551 let parent = document
552 .section_range(&parent_path)
553 .ok_or_else(|| ConfigEditError::InterfaceNotFound(name.clone()))?;
554 let child_ranges = document
555 .child_section_names(&parent_path)
556 .into_iter()
557 .filter_map(|child| {
558 document.section_range(&[section_key::INTERFACES, name.as_str(), child])
559 })
560 .collect::<Vec<_>>();
561 let replacement = child_ranges
562 .first()
563 .zip(child_ranges.last())
564 .map_or(parent.end..parent.end, |(first, last)| {
565 first.start..last.end
566 });
567 let rendered = radios
568 .iter()
569 .map(|radio| radio.render(document.newline()))
570 .collect::<String>();
571 Ok(replace_range(document.source(), replacement, &rendered))
572}
573
574fn interface_range(
575 document: &ConfigDocument,
576 name: &InterfaceName,
577) -> Result<Range<usize>, ConfigEditError> {
578 document
579 .section_range(&[section_key::INTERFACES, name.as_str()])
580 .ok_or_else(|| ConfigEditError::InterfaceNotFound(name.clone()))
581}
582
583fn ensure_interface_absent(
584 document: &ConfigDocument,
585 name: &InterfaceName,
586) -> Result<(), ConfigEditError> {
587 if document
588 .section_range(&[section_key::INTERFACES, name.as_str()])
589 .is_some()
590 {
591 return Err(ConfigEditError::DuplicateInterface(name.clone()));
592 }
593 Ok(())
594}
595
596fn key_insertion(
597 document: &ConfigDocument,
598 name: &InterfaceName,
599) -> Result<usize, ConfigEditError> {
600 let path = [section_key::INTERFACES, name.as_str()];
601 let section = document
602 .section_range(&path)
603 .ok_or_else(|| ConfigEditError::InterfaceNotFound(name.clone()))?;
604 Ok(document
605 .first_child_section_start(&path)
606 .unwrap_or(section.end))
607}
608
609fn replace_range(source: &str, range: Range<usize>, replacement: &str) -> String {
610 let mut candidate = String::with_capacity(source.len() + replacement.len());
611 candidate.push_str(&source[..range.start]);
612 candidate.push_str(replacement);
613 candidate.push_str(&source[range.end..]);
614 candidate
615}
616
617fn apply_replacements(source: &str, mut replacements: Vec<(Range<usize>, String)>) -> String {
618 replacements.sort_by_key(|replacement| std::cmp::Reverse(replacement.0.start));
619 let mut candidate = source.to_string();
620 for (range, replacement) in replacements {
621 candidate.replace_range(range, &replacement);
622 }
623 candidate
624}
625
626fn render_key_line(
627 source: &str,
628 range: Range<usize>,
629 key: &str,
630 value: &str,
631 newline: &str,
632) -> String {
633 let line = &source[range];
634 let indent_end = line.len() - line.trim_start().len();
635 let suffix = inline_comment(line).unwrap_or_default();
636 format!(
637 "{}{} = {}{}{}",
638 &line[..indent_end],
639 key,
640 value,
641 suffix,
642 newline
643 )
644}
645
646fn inline_comment(line: &str) -> Option<&str> {
647 let mut quote = None;
648 for (index, character) in line.char_indices() {
649 match (quote, character) {
650 (None, '\'' | '"') => quote = Some(character),
651 (Some(open), close) if open == close => quote = None,
652 (None, '#') => {
653 let start = line[..index]
654 .char_indices()
655 .rev()
656 .take_while(|(_, character)| character.is_whitespace())
657 .last()
658 .map_or(index, |(position, _)| position);
659 return Some(line[start..].trim_end_matches(['\r', '\n']));
660 }
661 _ => {}
662 }
663 }
664 None
665}
666
667fn section_header_suffix(header: &str, depth: usize) -> &str {
668 let close = "]".repeat(depth);
669 header
670 .find(&close)
671 .map(|position| &header[position + depth..])
672 .unwrap_or_default()
673}
674
675fn interface_enabled(document: &ConfigDocument, path: &[&str]) -> Option<bool> {
676 [interface_key::INTERFACE_ENABLED, interface_key::ENABLED]
677 .into_iter()
678 .filter_map(|key| document.section_value(path, key))
679 .filter_map(Value::as_scalar)
680 .filter_map(parse_bool)
681 .reduce(|left, right| left || right)
682}
683
684fn parse_bool(value: &str) -> Option<bool> {
685 match value.trim().to_ascii_lowercase().as_str() {
686 "yes" | "true" | "on" | "1" => Some(true),
687 "no" | "false" | "off" | "0" => Some(false),
688 _ => None,
689 }
690}
691
692fn ensure_trailing_newline(source: &mut String, newline: &str) {
693 if !source.is_empty() && !source.ends_with(['\n', '\r']) {
694 source.push_str(newline);
695 }
696}
697
698fn render_diff(original: &str, candidate: &str, secrets: SecretDisplay) -> String {
699 if original == candidate {
700 return String::new();
701 }
702 let before = original.lines().collect::<Vec<_>>();
703 let after = candidate.lines().collect::<Vec<_>>();
704 let prefix = before
705 .iter()
706 .zip(&after)
707 .take_while(|(left, right)| left == right)
708 .count();
709 let suffix = before[prefix..]
710 .iter()
711 .rev()
712 .zip(after[prefix..].iter().rev())
713 .take_while(|(left, right)| left == right)
714 .count();
715 let before_end = before.len().saturating_sub(suffix);
716 let after_end = after.len().saturating_sub(suffix);
717 let mut rendered = String::from("--- config\n+++ config\n");
718 for line in display_lines(&before[prefix..before_end], secrets) {
719 rendered.push('-');
720 rendered.push_str(&line);
721 rendered.push('\n');
722 }
723 for line in display_lines(&after[prefix..after_end], secrets) {
724 rendered.push('+');
725 rendered.push_str(&line);
726 rendered.push('\n');
727 }
728 rendered
729}
730
731fn display_lines(lines: &[&str], secrets: SecretDisplay) -> Vec<String> {
732 if secrets == SecretDisplay::Revealed {
733 return lines.iter().map(|line| (*line).to_string()).collect();
734 }
735 let mut multiline_secret = None;
736 let mut displayed = Vec::with_capacity(lines.len());
737 for line in lines {
738 if let Some(delimiter) = multiline_secret {
739 displayed.push("<redacted>".to_string());
740 if line.contains(delimiter) {
741 multiline_secret = None;
742 }
743 continue;
744 }
745 let Some((key, value)) = line.split_once('=') else {
746 displayed.push((*line).to_string());
747 continue;
748 };
749 if !matches!(key.trim(), "pass_phrase" | "passphrase" | "rpc_key") {
750 displayed.push((*line).to_string());
751 continue;
752 }
753 let value = value.trim_start();
754 multiline_secret = ["\"\"\"", "'''"].into_iter().find(|delimiter| {
755 value.starts_with(delimiter) && value.matches(delimiter).count() == 1
756 });
757 displayed.push(format!("{} = <redacted>", key.trim_end()));
758 }
759 displayed
760}