1use anyhow::{bail, Context, Result};
18use lazy_static::lazy_static;
19use regex::Regex;
20use std::fs::read_dir;
21use std::path::{Path, PathBuf};
22
23pub fn visit_bls_entry(
31 mountpoint: &Path,
32 mut f: impl FnMut(&str) -> Result<Option<String>>,
33) -> Result<bool> {
34 let mut config_path = mountpoint.to_path_buf();
36 config_path.push("loader/entries");
37
38 let mut entries: Vec<PathBuf> = Vec::new();
45 for entry in read_dir(&config_path)
46 .with_context(|| format!("reading directory {}", config_path.display()))?
47 {
48 let path = entry
49 .with_context(|| format!("reading directory {}", config_path.display()))?
50 .path();
51 if path.extension().unwrap_or_default() != "conf" {
52 continue;
53 }
54 entries.push(path);
55 }
56 entries.sort();
57
58 let mut changed = false;
59 if let Some(path) = entries.pop() {
60 let orig_contents = std::fs::read_to_string(&path)
61 .with_context(|| format!("reading {}", path.display()))?;
62 let r = f(&orig_contents).with_context(|| format!("visiting {}", path.display()))?;
63
64 if let Some(new_contents) = r {
65 std::fs::write(&path, new_contents.as_bytes())
67 .with_context(|| format!("writing {}", path.display()))?;
68 changed = true;
69 }
70 } else {
71 bail!("Found no BLS entries in {}", config_path.display());
72 }
73
74 Ok(changed)
75}
76
77pub fn visit_bls_entry_options(
81 mountpoint: &Path,
82 f: impl Fn(&str) -> Result<Option<String>>,
83) -> Result<bool> {
84 visit_bls_entry(mountpoint, |orig_contents: &str| {
85 let mut new_contents = String::with_capacity(orig_contents.len());
86 let mut found_options = false;
87 let mut modified = false;
88 for line in orig_contents.lines() {
89 if !line.starts_with("options ") {
90 new_contents.push_str(line.trim_end());
91 } else if found_options {
92 bail!("Multiple 'options' lines found");
93 } else {
94 let r = f(line["options ".len()..].trim()).context("visiting options")?;
95 if let Some(new_options) = r {
96 new_contents.push_str("options ");
97 new_contents.push_str(new_options.trim());
98 modified = true;
99 }
100 found_options = true;
101 }
102 new_contents.push('\n');
103 }
104 if !found_options {
105 bail!("Couldn't locate 'options' line");
106 }
107 if !modified {
108 Ok(None)
109 } else {
110 Ok(Some(new_contents))
111 }
112 })
113}
114
115#[derive(Default, PartialEq, Eq)]
116pub struct KargsEditor {
117 append: Vec<String>,
118 append_if_missing: Vec<String>,
119 replace: Vec<String>,
120 delete: Vec<String>,
121}
122
123impl KargsEditor {
124 pub fn new() -> Self {
125 Default::default()
126 }
127
128 pub fn append(&mut self, args: &[String]) -> &mut Self {
129 self.append.extend_from_slice(args);
130 self
131 }
132
133 pub fn append_if_missing(&mut self, args: &[String]) -> &mut Self {
134 self.append_if_missing.extend_from_slice(args);
135 self
136 }
137
138 pub fn replace(&mut self, args: &[String]) -> &mut Self {
139 self.replace.extend_from_slice(args);
140 self
141 }
142
143 pub fn delete(&mut self, args: &[String]) -> &mut Self {
144 self.delete.extend_from_slice(args);
145 self
146 }
147
148 pub fn apply_to(&self, current_kargs: &str) -> Result<String> {
153 lazy_static! {
154 static ref RE: Regex = Regex::new(r"^([^=]+)=([^=]+)=([^=]+)$").unwrap();
155 }
156 let mut new_kargs: String = format!(" {current_kargs} ");
157 for karg in &self.delete {
158 let s = format!(" {} ", karg.trim());
159 new_kargs = new_kargs.replace(&s, " ");
160 }
161 for karg in &self.append {
162 new_kargs.push_str(karg.trim());
163 new_kargs.push(' ');
164 }
165 for karg in &self.append_if_missing {
166 let karg = karg.trim();
167 let s = format!(" {karg} ");
168 if !new_kargs.contains(&s) {
169 new_kargs.push_str(karg);
170 new_kargs.push(' ');
171 }
172 }
173 for karg in &self.replace {
174 let caps = match RE.captures(karg) {
175 Some(caps) => caps,
176 None => bail!("Wrong input, format should be: KEY=OLD=NEW"),
177 };
178 let old = format!(" {}={} ", &caps[1], &caps[2]);
179 let new = format!(" {}={} ", &caps[1], &caps[3]);
180 new_kargs = new_kargs.replace(&old, &new);
181 }
182 Ok(new_kargs.trim().into())
183 }
184
185 pub fn maybe_apply_to(&self, current_kargs: &str) -> Result<Option<String>> {
189 if self == &Self::new() {
190 Ok(None)
191 } else {
192 Ok(Some(self.apply_to(current_kargs)?))
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_apply_to() {
203 let orig_kargs = "foo bar foobar";
204
205 let delete_kargs = vec!["foo".into()];
206 let new_kargs = KargsEditor::new()
207 .delete(&delete_kargs)
208 .apply_to(orig_kargs)
209 .unwrap();
210 assert_eq!(new_kargs, "bar foobar");
211
212 let delete_kargs = vec!["bar".into()];
213 let new_kargs = KargsEditor::new()
214 .delete(&delete_kargs)
215 .apply_to(orig_kargs)
216 .unwrap();
217 assert_eq!(new_kargs, "foo foobar");
218
219 let delete_kargs = vec!["foobar".into()];
220 let new_kargs = KargsEditor::new()
221 .delete(&delete_kargs)
222 .apply_to(orig_kargs)
223 .unwrap();
224 assert_eq!(new_kargs, "foo bar");
225
226 let delete_kargs = vec!["foo bar".into()];
227 let new_kargs = KargsEditor::new()
228 .delete(&delete_kargs)
229 .apply_to(orig_kargs)
230 .unwrap();
231 assert_eq!(new_kargs, "foobar");
232
233 let delete_kargs = vec!["bar".into(), "foo".into()];
234 let new_kargs = KargsEditor::new()
235 .delete(&delete_kargs)
236 .apply_to(orig_kargs)
237 .unwrap();
238 assert_eq!(new_kargs, "foobar");
239
240 let orig_kargs = "foo=val bar baz=val";
241
242 let delete_kargs = vec![" foo=val".into()];
243 let new_kargs = KargsEditor::new()
244 .delete(&delete_kargs)
245 .apply_to(orig_kargs)
246 .unwrap();
247 assert_eq!(new_kargs, "bar baz=val");
248
249 let delete_kargs = vec!["baz=val ".into()];
250 let new_kargs = KargsEditor::new()
251 .delete(&delete_kargs)
252 .apply_to(orig_kargs)
253 .unwrap();
254 assert_eq!(new_kargs, "foo=val bar");
255
256 let orig_kargs = "foo mitigations=auto,nosmt console=tty0 bar console=ttyS0,115200n8 baz";
257
258 let delete_kargs = vec![
259 "mitigations=auto,nosmt".into(),
260 "console=ttyS0,115200n8".into(),
261 ];
262 let append_kargs = vec!["console=ttyS1,115200n8 ".into()];
263 let append_kargs_if_missing =
264 vec!["bar".into(), "console=ttyS1,115200n8".into(), "boo".into()];
266 let new_kargs = KargsEditor::new()
267 .delete(&delete_kargs)
268 .append(&append_kargs)
269 .append_if_missing(&append_kargs_if_missing)
270 .apply_to(orig_kargs)
271 .unwrap();
272 assert_eq!(
273 new_kargs,
274 "foo console=tty0 bar baz console=ttyS1,115200n8 boo"
275 );
276
277 let orig_kargs = "foo mitigations=auto,nosmt console=tty0 bar console=ttyS0,115200n8 baz";
278
279 let append_kargs = vec!["console=ttyS1,115200n8".into()];
280 let replace_kargs = vec!["mitigations=auto,nosmt=auto".into()];
281 let delete_kargs = vec!["console=ttyS0,115200n8".into()];
282 let new_kargs = KargsEditor::new()
283 .append(&append_kargs)
284 .replace(&replace_kargs)
285 .delete(&delete_kargs)
286 .apply_to(orig_kargs)
287 .unwrap();
288 assert_eq!(
289 new_kargs,
290 "foo mitigations=auto console=tty0 bar baz console=ttyS1,115200n8"
291 );
292 }
293
294 #[test]
295 fn test_maybe_apply_to() {
296 assert!(KargsEditor::new()
298 .maybe_apply_to("foo bar foobar")
299 .unwrap()
300 .is_none());
301
302 assert!(KargsEditor::new()
304 .append(&[])
305 .delete(&[])
306 .maybe_apply_to("foo bar foobar")
307 .unwrap()
308 .is_none());
309
310 let new_kargs = KargsEditor::new()
312 .delete(&["baz".into()])
313 .maybe_apply_to("foo bar foobar")
314 .unwrap()
315 .unwrap();
316 assert_eq!(new_kargs, "foo bar foobar");
317 }
318}