@@ -19,6 +19,8 @@ use crate::{
1919 fat:: { self , Read } ,
2020} ;
2121
22+ const ENTRY_DIRECTORY : & str = "/loader/entries" ;
23+
2224pub 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 ) ]
2931pub enum Error {
30- FileError ( fat:: Error ) ,
31- BzImageError ( bzimage:: Error ) ,
32+ File ( fat:: Error ) ,
33+ BzImage ( bzimage:: Error ) ,
34+ UnterminatedString ,
3235}
3336
3437impl From < fat:: Error > for Error {
3538 fn from ( e : fat:: Error ) -> Error {
36- Error :: FileError ( e)
39+ Error :: File ( e)
3740 }
3841}
3942
4043impl 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
77163fn 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}
0 commit comments