purple_ssh/ssh_config/
repair.rs1use super::model::{ConfigElement, SshConfigFile};
14
15pub(super) fn provider_group_display_name(name: &str) -> &str {
19 match name {
20 "digitalocean" => "DigitalOcean",
21 "vultr" => "Vultr",
22 "linode" => "Linode",
23 "hetzner" => "Hetzner",
24 "upcloud" => "UpCloud",
25 "proxmox" => "Proxmox VE",
26 "aws" => "AWS EC2",
27 "scaleway" => "Scaleway",
28 "gcp" => "GCP",
29 "azure" => "Azure",
30 "tailscale" => "Tailscale",
31 "oracle" => "Oracle Cloud",
32 other => other,
33 }
34}
35
36impl SshConfigFile {
37 pub fn remove_all_orphaned_group_headers(&mut self) -> usize {
40 let active_providers: std::collections::HashSet<String> = self
41 .elements
42 .iter()
43 .filter_map(|e| {
44 if let ConfigElement::HostBlock(block) = e {
45 block
46 .provider()
47 .map(|(name, _)| provider_group_display_name(&name).to_string())
48 } else {
49 None
50 }
51 })
52 .collect();
53
54 let mut removed = 0;
55 self.elements.retain(|e| {
56 if let ConfigElement::GlobalLine(line) = e {
57 if let Some(rest) = line.trim().strip_prefix("# purple:group ") {
58 if !active_providers.contains(rest.trim()) {
59 removed += 1;
60 return false;
61 }
62 }
63 }
64 true
65 });
66 removed
67 }
68
69 pub fn repair_absorbed_group_comments(&mut self) -> usize {
80 let known_providers: std::collections::HashSet<&str> = [
84 "DigitalOcean",
85 "Vultr",
86 "Linode",
87 "Hetzner",
88 "UpCloud",
89 "Proxmox VE",
90 "AWS EC2",
91 "Scaleway",
92 "GCP",
93 "Azure",
94 "Tailscale",
95 "Oracle Cloud",
96 ]
97 .iter()
98 .copied()
99 .collect();
100
101 let is_known_group = |raw: &str| -> bool {
102 raw.trim()
103 .strip_prefix("# purple:group ")
104 .map(|suffix| known_providers.contains(suffix.trim()))
105 .unwrap_or(false)
106 };
107
108 let mut repaired = 0;
109 let mut idx = 0;
110 while idx < self.elements.len() {
111 let needs_repair = if let ConfigElement::HostBlock(block) = &self.elements[idx] {
112 block
113 .directives
114 .iter()
115 .any(|d| d.is_non_directive && is_known_group(&d.raw_line))
116 } else {
117 false
118 };
119
120 if !needs_repair {
121 idx += 1;
122 continue;
123 }
124
125 let block = if let ConfigElement::HostBlock(block) = &mut self.elements[idx] {
126 block
127 } else {
128 unreachable!()
129 };
130
131 let group_idx = block
132 .directives
133 .iter()
134 .position(|d| d.is_non_directive && is_known_group(&d.raw_line))
135 .unwrap();
136
137 let mut keep_end = group_idx;
138 while keep_end > 0
139 && block.directives[keep_end - 1].is_non_directive
140 && block.directives[keep_end - 1].raw_line.trim().is_empty()
141 {
142 keep_end -= 1;
143 }
144
145 let extracted: Vec<ConfigElement> = block
146 .directives
147 .drain(keep_end..)
148 .map(|d| ConfigElement::GlobalLine(d.raw_line))
149 .collect();
150
151 let insert_at = idx + 1;
152 for (i, elem) in extracted.into_iter().enumerate() {
153 self.elements.insert(insert_at + i, elem);
154 }
155
156 repaired += 1;
157 idx = insert_at;
158 while idx < self.elements.len() {
159 if let ConfigElement::HostBlock(_) = &self.elements[idx] {
160 break;
161 }
162 idx += 1;
163 }
164 }
165 repaired
166 }
167
168 pub(super) fn remove_orphaned_group_header(&mut self, provider_name: &str) {
171 if self.find_hosts_by_provider(provider_name).is_empty() {
172 let display = provider_group_display_name(provider_name);
173 let header = format!("# purple:group {}", display);
174 self.elements
175 .retain(|e| !matches!(e, ConfigElement::GlobalLine(line) if *line == header));
176 }
177 }
178}