Skip to content

Commit 8b3b4ef

Browse files
benmaddisonrbradford
authored andcommitted
loader: systemd-boot compatible default entry matching
Implement matching of boot entry names against the pattern in the default option from `loader/loader.conf`. Signed-off-by: Ben Maddison <benm@workonline.africa>
1 parent 26adcf7 commit 8b3b4ef

File tree

3 files changed

+169
-29
lines changed

3 files changed

+169
-29
lines changed

src/fat.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ pub struct DirectoryEntry {
8686
cluster: u32,
8787
}
8888

89+
impl DirectoryEntry {
90+
pub fn long_name(&self) -> [u8; 255] {
91+
self.long_name
92+
}
93+
94+
pub fn is_file(&self) -> bool {
95+
matches!(self.file_type, FileType::File)
96+
}
97+
}
98+
8999
#[derive(Debug, PartialEq, Eq)]
90100
enum FatType {
91101
Unknown,
@@ -121,6 +131,7 @@ pub enum Error {
121131
NotFound,
122132
EndOfFile,
123133
InvalidOffset,
134+
NodeTypeMismatch,
124135
}
125136

126137
#[derive(Debug, PartialEq, Eq)]
@@ -147,23 +158,23 @@ impl<'a> From<Directory<'a>> for Node<'a> {
147158
}
148159

149160
impl<'a> TryFrom<Node<'a>> for File<'a> {
150-
type Error = ();
161+
type Error = Error;
151162

152163
fn try_from(from: Node<'a>) -> Result<Self, Self::Error> {
153164
match from {
154165
Node::File(f) => Ok(f),
155-
_ => Err(()),
166+
_ => Err(Self::Error::NodeTypeMismatch),
156167
}
157168
}
158169
}
159170

160171
impl<'a> TryFrom<Node<'a>> for Directory<'a> {
161-
type Error = ();
172+
type Error = Error;
162173

163174
fn try_from(from: Node<'a>) -> Result<Self, Self::Error> {
164175
match from {
165176
Node::Directory(d) => Ok(d),
166-
_ => Err(()),
177+
_ => Err(Self::Error::NodeTypeMismatch),
167178
}
168179
}
169180
}

src/loader.rs

Lines changed: 153 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use crate::{
1919
fat::{self, Read},
2020
};
2121

22+
const ENTRY_DIRECTORY: &str = "/loader/entries";
23+
2224
pub struct LoaderConfig {
2325
pub bzimage_path: [u8; 260],
2426
pub initrd_path: [u8; 260],
@@ -27,29 +29,29 @@ pub struct LoaderConfig {
2729

2830
#[derive(Debug)]
2931
pub enum Error {
30-
FileError(fat::Error),
31-
BzImageError(bzimage::Error),
32+
File(fat::Error),
33+
BzImage(bzimage::Error),
34+
UnterminatedString,
3235
}
3336

3437
impl From<fat::Error> for Error {
3538
fn from(e: fat::Error) -> Error {
36-
Error::FileError(e)
39+
Error::File(e)
3740
}
3841
}
3942

4043
impl From<bzimage::Error> for Error {
4144
fn from(e: bzimage::Error) -> Error {
42-
Error::BzImageError(e)
45+
Error::BzImage(e)
4346
}
4447
}
4548

46-
const ENTRY_EXTENSION: &str = ".conf";
47-
48-
fn default_entry_file(f: &mut fat::File) -> Result<[u8; 260], fat::Error> {
49+
/// Given a `loader.conf` file, find the `default` option value.
50+
fn default_entry_pattern(f: &mut fat::File) -> Result<[u8; 260], fat::Error> {
4951
let mut data = [0; 4096];
5052
assert!(f.get_size() as usize <= data.len());
5153

52-
let mut entry_file_name = [0; 260];
54+
let mut entry_pattern = [0; 260];
5355
let mut offset = 0;
5456
loop {
5557
match f.read(&mut data[offset..offset + 512]) {
@@ -63,15 +65,99 @@ fn default_entry_file(f: &mut fat::File) -> Result<[u8; 260], fat::Error> {
6365

6466
let conf = unsafe { core::str::from_utf8_unchecked(&data) };
6567
for line in conf.lines() {
66-
if let Some(entry) = line.strip_prefix("default") {
67-
let entry = entry.trim();
68-
entry_file_name[0..entry.len()].copy_from_slice(entry.as_bytes());
69-
entry_file_name[entry.len()..entry.len() + ENTRY_EXTENSION.len()]
70-
.copy_from_slice(ENTRY_EXTENSION.as_bytes());
68+
if let Some(mut pattern) = line.strip_prefix("default") {
69+
pattern = pattern.trim();
70+
entry_pattern[0..pattern.len()].copy_from_slice(pattern.as_bytes());
7171
}
7272
}
7373

74-
Ok(entry_file_name)
74+
Ok(entry_pattern)
75+
}
76+
77+
/// Given a glob-like pattern, select a boot entry from `/loader/entries/`,
78+
/// falling back to the first entry encountered if no match is found.
79+
fn find_entry(fs: &fat::Filesystem, pattern: &[u8]) -> Result<[u8; 255], Error> {
80+
let mut dir: fat::Directory = fs.open(ENTRY_DIRECTORY)?.try_into()?;
81+
let mut fallback = None;
82+
loop {
83+
match dir.next_entry() {
84+
Ok(de) => {
85+
if !de.is_file() {
86+
continue;
87+
}
88+
let file_name = de.long_name();
89+
// return the first matching file name
90+
if compare_entry(&file_name, pattern)? {
91+
return Ok(file_name);
92+
}
93+
// only fallback to entry files ending with `.conf`
94+
if fallback.is_none() && compare_entry(&file_name, b"*.conf\0")? {
95+
fallback = Some(file_name);
96+
}
97+
}
98+
Err(fat::Error::EndOfFile) => break,
99+
Err(err) => return Err(err.into()),
100+
}
101+
}
102+
fallback.ok_or_else(|| fat::Error::NotFound.into())
103+
}
104+
105+
/// Attempt to match a file name with a glob-like pattern.
106+
/// An error is returned if either `file_name` or `pattern` are not `\0`
107+
/// terminated.
108+
fn compare_entry(file_name: &[u8], pattern: &[u8]) -> Result<bool, Error> {
109+
fn compare_entry_inner<I>(
110+
mut name_iter: core::iter::Peekable<I>,
111+
mut pattern: &[u8],
112+
max_depth: usize,
113+
) -> Result<bool, Error>
114+
where
115+
I: Iterator<Item = u8> + Clone,
116+
{
117+
if max_depth == 0 {
118+
return Ok(false);
119+
}
120+
while let Some(p) = pattern.take_first() {
121+
let f = name_iter.peek().ok_or(Error::UnterminatedString)?;
122+
#[cfg(test)]
123+
println!("{} ~ {}", *p as char, *f as char);
124+
match p {
125+
b'\0' => return Ok(*f == b'\0'),
126+
b'\\' => {
127+
match pattern.take_first() {
128+
// trailing escape
129+
Some(b'\0') | None => return Ok(false),
130+
// no match
131+
Some(p) if p != f => return Ok(false),
132+
// continue
133+
_ => (),
134+
}
135+
}
136+
b'?' => {
137+
if *f == b'\0' {
138+
return Ok(false);
139+
}
140+
}
141+
b'*' => {
142+
while name_iter.peek().is_some() {
143+
if compare_entry_inner(name_iter.clone(), pattern, max_depth - 1)? {
144+
return Ok(true);
145+
}
146+
name_iter.next().ok_or(Error::UnterminatedString)?;
147+
}
148+
return Ok(*pattern.first().ok_or(Error::UnterminatedString)? == b'\0');
149+
}
150+
// TODO
151+
b'[' => todo!("patterns containing `[...]` sets are not supported"),
152+
_ if p != f => return Ok(false),
153+
_ => (),
154+
}
155+
name_iter.next().ok_or(Error::UnterminatedString)?;
156+
}
157+
Ok(false)
158+
}
159+
let name_iter = file_name.iter().copied().peekable();
160+
compare_entry_inner(name_iter, pattern, 32)
75161
}
76162

77163
fn parse_entry(f: &mut fat::File) -> Result<LoaderConfig, fat::Error> {
@@ -110,20 +196,20 @@ fn parse_entry(f: &mut fat::File) -> Result<LoaderConfig, fat::Error> {
110196
Ok(loader_config)
111197
}
112198

113-
const ENTRY_DIRECTORY: &str = "/loader/entries/";
114-
115-
fn default_entry_path(fs: &fat::Filesystem) -> Result<[u8; 260], fat::Error> {
199+
fn default_entry_path(fs: &fat::Filesystem) -> Result<[u8; 260], Error> {
116200
let mut f = match fs.open("/loader/loader.conf")? {
117201
fat::Node::File(f) => f,
118-
_ => return Err(fat::Error::NotFound),
202+
_ => return Err(fat::Error::NotFound.into()),
119203
};
120-
let default_entry = default_entry_file(&mut f)?;
204+
let default_entry_pattern = default_entry_pattern(&mut f)?;
205+
206+
let default_entry = find_entry(fs, &default_entry_pattern)?;
121207
let default_entry = ascii_strip(&default_entry);
122208

123209
let mut entry_path = [0u8; 260];
124210
entry_path[0..ENTRY_DIRECTORY.len()].copy_from_slice(ENTRY_DIRECTORY.as_bytes());
125-
126-
entry_path[ENTRY_DIRECTORY.len()..ENTRY_DIRECTORY.len() + default_entry.len()]
211+
entry_path[ENTRY_DIRECTORY.len()] = b'/';
212+
entry_path[ENTRY_DIRECTORY.len() + 1..ENTRY_DIRECTORY.len() + default_entry.len() + 1]
127213
.copy_from_slice(default_entry.as_bytes());
128214
Ok(entry_path)
129215
}
@@ -134,7 +220,7 @@ pub fn load_default_entry(fs: &fat::Filesystem, info: &dyn boot::Info) -> Result
134220

135221
let mut f = match fs.open(default_entry_path)? {
136222
fat::Node::File(f) => f,
137-
_ => return Err(Error::FileError(fat::Error::NotFound)),
223+
_ => return Err(Error::File(fat::Error::NotFound)),
138224
};
139225
let entry = parse_entry(&mut f)?;
140226

@@ -173,16 +259,16 @@ mod tests {
173259
fs.init().expect("Error initialising filesystem");
174260

175261
let mut f: crate::fat::File = fs.open("/loader/loader.conf").unwrap().try_into().unwrap();
176-
let s = super::default_entry_file(&mut f).unwrap();
262+
let s = super::default_entry_pattern(&mut f).unwrap();
177263
let s = super::ascii_strip(&s);
178-
assert_eq!(s, "Clear-linux-kvm-5.0.6-318.conf");
264+
assert_eq!(s, "Clear-linux-kvm-5.0.6-318");
179265

180266
let default_entry_path = super::default_entry_path(&fs).unwrap();
181267
let default_entry_path = super::ascii_strip(&default_entry_path);
182268

183269
assert_eq!(
184270
default_entry_path,
185-
format!("/loader/entries/{}", s).as_str()
271+
format!("/loader/entries/{}.conf", s).as_str()
186272
);
187273

188274
let mut f: crate::fat::File = fs.open(default_entry_path).unwrap().try_into().unwrap();
@@ -193,4 +279,46 @@ mod tests {
193279
let s = s.trim_matches(char::from(0));
194280
assert_eq!(s, "root=PARTUUID=ae06d187-e9fc-4d3b-9e5b-8e6ff28e894f console=tty0 console=ttyS0,115200n8 console=hvc0 quiet init=/usr/lib/systemd/systemd-bootchart initcall_debug tsc=reliable no_timer_check noreplace-smp cryptomgr.notests rootfstype=ext4,btrfs,xfs kvm-intel.nested=1 rw");
195281
}
282+
283+
macro_rules! entry_pattern_matches {
284+
(match $entry:literal with {
285+
$(
286+
$( #[$attr:meta] )*
287+
$id:ident: $pat:literal => $result:literal
288+
),* $(,)?
289+
} ) => {
290+
mod entry_pattern {
291+
$(
292+
#[test]
293+
$( #[$attr] )*
294+
fn $id() {
295+
assert_eq!(super::super::compare_entry($entry, $pat).unwrap(), $result);
296+
}
297+
)*
298+
}
299+
}
300+
}
301+
302+
entry_pattern_matches! {
303+
match b"foobar.conf\0" with {
304+
empty: b"\0" => false,
305+
306+
exact: b"foobar.conf\0" => true,
307+
inexact: b"barfoo.conf\0" => false,
308+
309+
wildcard: b"*\0" => true,
310+
leading_wildcard: b"*.conf\0" => true,
311+
internal_wildcard: b"foo*.conf\0" => true,
312+
trailing_wildcard: b"foob*\0" => true,
313+
mismatched_wildcard: b"bar*\0" => false,
314+
wildcard_backtrack: b"*obar.conf\0" => true,
315+
316+
single_wildcard: b"fo?bar.conf\0" => true,
317+
mismatched_single_wildcard: b"foo?bar.conf\0" => false,
318+
319+
escaped_regular_char: b"foo\\bar.conf\0" => true,
320+
escaped_special_char: b"foo\\?ar.conf\0" => false,
321+
trailing_escape: b"foobar.conf\\\0" => false,
322+
}
323+
}
196324
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#![feature(alloc_error_handler)]
1616
#![feature(stmt_expr_attributes)]
17+
#![feature(slice_take)]
1718
#![cfg_attr(not(test), no_std)]
1819
#![cfg_attr(not(test), no_main)]
1920
#![cfg_attr(test, allow(unused_imports, dead_code))]

0 commit comments

Comments
 (0)