Skip to content

Commit 324c322

Browse files
Johan-Liebert1cgwalters
authored andcommitted
Write nom parser for Grub menuentries
Signed-off-by: Johan-Liebert1 <[email protected]> Signed-off-by: Colin Walters <[email protected]>
1 parent 92409e9 commit 324c322

File tree

5 files changed

+272
-2
lines changed

5 files changed

+272
-2
lines changed

Cargo.lock

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/lib/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,20 @@ tini = "1.3.0"
5959
comfy-table = "7.1.1"
6060
thiserror = { workspace = true }
6161
canon-json = { workspace = true }
62+
nom = "8.0.0"
6263

6364
[dev-dependencies]
6465
similar-asserts = { workspace = true }
6566
static_assertions = { workspace = true }
6667

6768
[features]
68-
default = ["install-to-disk"]
69+
default = ["install-to-disk", "grub"]
6970
# This feature enables `bootc install to-disk`, which is considered just a "demo"
7071
# or reference installer; we expect most nontrivial use cases to be using
7172
# `bootc install to-filesystem`.
7273
install-to-disk = []
74+
# Enable direct support for the GRUB bootloader
75+
grub = []
7376
# This featuares enables `bootc internals publish-rhsm-facts` to integrate with
7477
# Red Hat Subscription Manager
7578
rhsm = []

crates/lib/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ mod kernel;
3838

3939
#[cfg(feature = "rhsm")]
4040
mod rhsm;
41+
#[cfg(feature = "grub")]
42+
pub(crate) mod parsers;
4143

4244
// Re-export blockdev crate for internal use
4345
pub(crate) use bootc_blockdev as blockdev;
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use std::fmt::Display;
2+
3+
use nom::{
4+
bytes::complete::{tag, take_until},
5+
character::complete::multispace0,
6+
error::{Error, ErrorKind, ParseError},
7+
multi::many0,
8+
sequence::{delimited, preceded},
9+
Err, IResult, Parser,
10+
};
11+
12+
#[derive(Debug, PartialEq, Eq)]
13+
pub(crate) struct MenuentryBody<'a> {
14+
insmod: Vec<&'a str>,
15+
chainloader: &'a str,
16+
search: &'a str,
17+
version: u8,
18+
extra: Vec<(&'a str, &'a str)>,
19+
}
20+
21+
impl<'a> Display for MenuentryBody<'a> {
22+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23+
for insmod in &self.insmod {
24+
writeln!(f, "insmod {}", insmod)?;
25+
}
26+
27+
writeln!(f, "search {}", self.search)?;
28+
// writeln!(f, "version {}", self.version)?;
29+
writeln!(f, "chainloader {}", self.chainloader)?;
30+
31+
for (k, v) in &self.extra {
32+
writeln!(f, "{k} {v}")?;
33+
}
34+
35+
Ok(())
36+
}
37+
}
38+
39+
impl<'a> From<Vec<(&'a str, &'a str)>> for MenuentryBody<'a> {
40+
fn from(vec: Vec<(&'a str, &'a str)>) -> Self {
41+
let mut entry = Self {
42+
insmod: vec![],
43+
chainloader: "",
44+
search: "",
45+
version: 0,
46+
extra: vec![],
47+
};
48+
49+
for (key, value) in vec {
50+
match key {
51+
"insmod" => entry.insmod.push(value),
52+
"chainloader" => entry.chainloader = value,
53+
"search" => entry.search = value,
54+
"set" => {}
55+
_ => entry.extra.push((key, value)),
56+
}
57+
}
58+
59+
return entry;
60+
}
61+
}
62+
63+
#[derive(Debug, PartialEq, Eq)]
64+
pub(crate) struct MenuEntry<'a> {
65+
pub(crate) title: &'a str,
66+
pub(crate) body: MenuentryBody<'a>,
67+
}
68+
69+
impl<'a> Display for MenuEntry<'a> {
70+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71+
writeln!(f, "menuentry \"{}\" {{", self.title)?;
72+
write!(f, "{}", self.body)?;
73+
writeln!(f, "}}")
74+
}
75+
}
76+
77+
pub fn take_until_balanced_allow_nested(
78+
opening_bracket: char,
79+
closing_bracket: char,
80+
) -> impl Fn(&str) -> IResult<&str, &str> {
81+
move |i: &str| {
82+
let mut index = 0;
83+
let mut bracket_counter = 0;
84+
85+
while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket, '\\'][..]) {
86+
index += n;
87+
let mut characters = i[index..].chars();
88+
89+
match characters.next().unwrap_or_default() {
90+
c if c == '\\' => {
91+
// Skip '\'
92+
index += '\\'.len_utf8();
93+
// Skip char following '\'
94+
let c = characters.next().unwrap_or_default();
95+
index += c.len_utf8();
96+
}
97+
98+
c if c == opening_bracket => {
99+
bracket_counter += 1;
100+
index += opening_bracket.len_utf8();
101+
}
102+
103+
c if c == closing_bracket => {
104+
bracket_counter -= 1;
105+
index += closing_bracket.len_utf8();
106+
}
107+
108+
// Should not happen
109+
_ => unreachable!(),
110+
};
111+
112+
// We found the unmatched closing bracket.
113+
if bracket_counter == -1 {
114+
// Don't consume it as we'll "tag" it afterwards
115+
index -= closing_bracket.len_utf8();
116+
return Ok((&i[index..], &i[0..index]));
117+
};
118+
}
119+
120+
if bracket_counter == 0 {
121+
Ok(("", i))
122+
} else {
123+
Err(Err::Error(Error::from_error_kind(i, ErrorKind::TakeUntil)))
124+
}
125+
}
126+
}
127+
128+
fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry> {
129+
let (input, _) = take_until("menuentry")(input)?; // skip irrelevant prefix
130+
let (input, _) = tag("menuentry").parse(input)?;
131+
132+
// Skip the whitespace after "menuentry"
133+
let (input, _) = multispace0.parse(input)?;
134+
// Eat up the title
135+
let (input, title) = delimited(tag("\""), take_until("\""), tag("\"")).parse(input)?;
136+
137+
// Skip any whitespace after title
138+
let (input, _) = multispace0.parse(input)?;
139+
140+
// Eat up everything insde { .. }
141+
let (input, body) = delimited(
142+
tag("{"),
143+
take_until_balanced_allow_nested('{', '}'),
144+
tag("}"),
145+
)
146+
.parse(input)?;
147+
148+
let mut map = vec![];
149+
150+
for line in body.lines() {
151+
let line = line.trim();
152+
153+
if line.is_empty() || line.starts_with('#') {
154+
continue;
155+
}
156+
157+
if let Some((key, value)) = line.split_once(' ') {
158+
map.push((key, value.trim()));
159+
}
160+
}
161+
162+
Ok((
163+
input,
164+
MenuEntry {
165+
title,
166+
body: MenuentryBody::from(map),
167+
},
168+
))
169+
}
170+
171+
#[rustfmt::skip]
172+
fn parse_all(input: &str) -> IResult<&str, Vec<MenuEntry>> {
173+
many0(
174+
preceded(
175+
multispace0,
176+
parse_menuentry,
177+
)
178+
)
179+
.parse(input)
180+
}
181+
182+
pub(crate) fn parse_grub_menuentry_file(contents: &str) -> anyhow::Result<Vec<MenuEntry>> {
183+
let result = parse_all(&contents);
184+
185+
return match result {
186+
Ok((_, entries)) => Ok(entries),
187+
Result::Err(_) => anyhow::bail!("Failed to parse grub menuentry"),
188+
};
189+
}
190+
191+
#[cfg(test)]
192+
mod test {
193+
use super::*;
194+
195+
#[test]
196+
fn test_menuconfig_parser() {
197+
let menuentry = r#"
198+
if [ -f ${config_directory}/efiuuid.cfg ]; then
199+
source ${config_directory}/efiuuid.cfg
200+
fi
201+
202+
# Skip this comment
203+
204+
menuentry "Fedora 42: (Verity-42)" {
205+
insmod fat
206+
insmod chain
207+
# This should also be skipped
208+
search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
209+
chainloader /EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi
210+
}
211+
212+
menuentry "Fedora 43: (Verity-43)" {
213+
insmod fat
214+
insmod chain
215+
search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
216+
chainloader /EFI/Linux/uki.efi
217+
extra_field1 this is extra
218+
extra_field2 this is also extra
219+
}
220+
"#;
221+
222+
let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
223+
224+
let expected = vec![
225+
MenuEntry {
226+
title: "Fedora 42: (Verity-42)",
227+
body: MenuentryBody {
228+
insmod: vec!["fat", "chain"],
229+
search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
230+
chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi",
231+
version: 0,
232+
extra: vec![],
233+
},
234+
},
235+
MenuEntry {
236+
title: "Fedora 43: (Verity-43)",
237+
body: MenuentryBody {
238+
insmod: vec!["fat", "chain"],
239+
search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
240+
chainloader: "/EFI/Linux/uki.efi",
241+
version: 0,
242+
extra: vec![
243+
("extra_field1", "this is extra"),
244+
("extra_field2", "this is also extra")
245+
]
246+
},
247+
},
248+
];
249+
250+
println!("{}", expected[0]);
251+
252+
assert_eq!(result, expected);
253+
}
254+
}

crates/lib/src/parsers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod grub_menuconfig;

0 commit comments

Comments
 (0)