55// Unfortunately needed here to work with linkme
66#![ allow( unsafe_code) ]
77
8- use std:: collections:: BTreeSet ;
8+ use std:: collections:: { BTreeMap , BTreeSet } ;
99use std:: env:: consts:: ARCH ;
1010use std:: fmt:: Write as WriteFmt ;
11+ use std:: ops:: ControlFlow ;
1112use std:: os:: unix:: ffi:: OsStrExt ;
1213use std:: path:: Path ;
1314
@@ -17,7 +18,8 @@ use camino::{Utf8Path, Utf8PathBuf};
1718use cap_std:: fs:: Dir ;
1819use cap_std_ext:: cap_std;
1920use cap_std_ext:: cap_std:: fs:: MetadataExt ;
20- use cap_std_ext:: dirext:: { CapStdExtDirExt as _, WalkConfiguration } ;
21+ use cap_std_ext:: dirext:: WalkConfiguration ;
22+ use cap_std_ext:: dirext:: { CapStdExtDirExt as _, WalkComponent } ;
2123use fn_error_context:: context;
2224use indoc:: indoc;
2325use linkme:: distributed_slice;
@@ -61,6 +63,17 @@ impl LintError {
6163}
6264
6365type LintFn = fn ( & Dir ) -> LintResult ;
66+ type LintRecursiveResult = LintResult ;
67+ type LintRecursiveFn = fn ( & WalkComponent ) -> LintRecursiveResult ;
68+ /// A lint can either operate as it pleases on a target root, or it
69+ /// can be recursive.
70+ #[ derive( Debug ) ]
71+ enum LintFnTy {
72+ /// A lint that doesn't traverse the whole filesystem
73+ Regular ( LintFn ) ,
74+ /// A recursive lint
75+ Recursive ( LintRecursiveFn ) ,
76+ }
6477#[ distributed_slice]
6578pub ( crate ) static LINTS : [ Lint ] ;
6679
@@ -94,13 +107,38 @@ struct Lint {
94107 #[ serde( rename = "type" ) ]
95108 ty : LintType ,
96109 #[ serde( skip) ]
97- f : LintFn ,
110+ f : LintFnTy ,
98111 description : & ' static str ,
99112 // Set if this only applies to a specific root type.
100113 #[ serde( skip_serializing_if = "Option::is_none" ) ]
101114 root_type : Option < RootType > ,
102115}
103116
117+ // We require lint names to be unique, so we can just compare based on those.
118+ impl PartialEq for Lint {
119+ fn eq ( & self , other : & Self ) -> bool {
120+ self . name == other. name
121+ }
122+ }
123+ impl Eq for Lint { }
124+
125+ impl std:: hash:: Hash for Lint {
126+ fn hash < H : std:: hash:: Hasher > ( & self , state : & mut H ) {
127+ self . name . hash ( state) ;
128+ }
129+ }
130+
131+ impl PartialOrd for Lint {
132+ fn partial_cmp ( & self , other : & Self ) -> Option < std:: cmp:: Ordering > {
133+ Some ( self . cmp ( other) )
134+ }
135+ }
136+ impl Ord for Lint {
137+ fn cmp ( & self , other : & Self ) -> std:: cmp:: Ordering {
138+ self . name . cmp ( other. name )
139+ }
140+ }
141+
104142impl Lint {
105143 pub ( crate ) const fn new_fatal (
106144 name : & ' static str ,
@@ -110,7 +148,7 @@ impl Lint {
110148 Lint {
111149 name : name,
112150 ty : LintType :: Fatal ,
113- f : f ,
151+ f : LintFnTy :: Regular ( f ) ,
114152 description : description,
115153 root_type : None ,
116154 }
@@ -124,7 +162,7 @@ impl Lint {
124162 Lint {
125163 name : name,
126164 ty : LintType :: Warning ,
127- f : f ,
165+ f : LintFnTy :: Regular ( f ) ,
128166 description : description,
129167 root_type : None ,
130168 }
@@ -159,24 +197,78 @@ fn lint_inner<'skip>(
159197 let mut fatal = 0usize ;
160198 let mut warnings = 0usize ;
161199 let mut passed = 0usize ;
162- let mut skipped = 0usize ;
163200 let skip: std:: collections:: HashSet < _ > = skip. into_iter ( ) . collect ( ) ;
164- for lint in LINTS {
165- let name = lint. name ;
166-
167- if skip. contains ( name) {
168- skipped += 1 ;
169- continue ;
201+ let ( mut applicable_lints, skipped_lints) : ( Vec < _ > , Vec < _ > ) = LINTS . iter ( ) . partition ( |lint| {
202+ if skip. contains ( lint. name ) {
203+ return false ;
170204 }
171-
172205 if let Some ( lint_root_type) = lint. root_type {
173206 if lint_root_type != root_type {
174- skipped += 1 ;
175- continue ;
207+ return false ;
176208 }
177209 }
210+ true
211+ } ) ;
212+ // SAFETY: Length must be smaller.
213+ let skipped = skipped_lints. len ( ) ;
214+ // Default to predictablility here
215+ applicable_lints. sort_by ( |a, b| a. name . cmp ( b. name ) ) ;
216+ // Split the lints by type
217+ let ( nonrec_lints, recursive_lints) : ( Vec < _ > , Vec < _ > ) = applicable_lints
218+ . into_iter ( )
219+ . partition ( |lint| matches ! ( lint. f, LintFnTy :: Regular ( _) ) ) ;
220+ let mut results = Vec :: new ( ) ;
221+ for lint in nonrec_lints {
222+ let f = match lint. f {
223+ LintFnTy :: Regular ( f) => f,
224+ LintFnTy :: Recursive ( _) => unreachable ! ( ) ,
225+ } ;
226+ results. push ( ( lint, f ( & root) ) ) ;
227+ }
178228
179- let r = match ( lint. f ) ( & root) {
229+ let mut recursive_lints = BTreeSet :: from_iter ( recursive_lints. into_iter ( ) ) ;
230+ let mut recursive_errors = BTreeMap :: new ( ) ;
231+ root. walk (
232+ & WalkConfiguration :: default ( )
233+ . noxdev ( )
234+ . path_base ( Path :: new ( "/" ) ) ,
235+ |e| -> std:: io:: Result < _ > {
236+ // If there's no recursive lints, we're done!
237+ if recursive_lints. is_empty ( ) {
238+ return Ok ( ControlFlow :: Break ( ( ) ) ) ;
239+ }
240+ // Keep track of any errors we caught while iterating over
241+ // the recursive lints.
242+ let mut this_iteration_errors = Vec :: new ( ) ;
243+ // Call each recursive lint on this directory entry.
244+ for & lint in recursive_lints. iter ( ) {
245+ let f = match & lint. f {
246+ // SAFETY: We know this set only holds recursive lints
247+ LintFnTy :: Regular ( _) => unreachable ! ( ) ,
248+ LintFnTy :: Recursive ( f) => f,
249+ } ;
250+ // Keep track of the error if we found one
251+ match f ( e) {
252+ Ok ( Ok ( ( ) ) ) => { }
253+ o => this_iteration_errors. push ( ( lint, o) ) ,
254+ }
255+ }
256+ // For each recursive lint that errored, remove it from
257+ // the set that we will continue running.
258+ for ( lint, err) in this_iteration_errors {
259+ recursive_lints. remove ( lint) ;
260+ recursive_errors. insert ( lint, err) ;
261+ }
262+ Ok ( ControlFlow :: Continue ( ( ) ) )
263+ } ,
264+ ) ?;
265+ // Extend our overall result set with the recursive-lint errors.
266+ results. extend ( recursive_errors. into_iter ( ) . map ( |( lint, e) | ( lint, e) ) ) ;
267+ // Any recursive lint still in this list succeeded.
268+ results. extend ( recursive_lints. into_iter ( ) . map ( |lint| ( lint, lint_ok ( ) ) ) ) ;
269+ for ( lint, r) in results {
270+ let name = lint. name ;
271+ let r = match r {
180272 Ok ( r) => r,
181273 Err ( e) => anyhow:: bail!( "Unexpected runtime error running lint {name}: {e}" ) ,
182274 } ;
@@ -330,53 +422,36 @@ fn check_kernel(root: &Dir) -> LintResult {
330422
331423// This one can be lifted in the future, see https://github.com/containers/bootc/issues/975
332424#[ distributed_slice( LINTS ) ]
333- static LINT_UTF8 : Lint = Lint :: new_fatal (
334- "utf8" ,
335- indoc ! { r#"
425+ static LINT_UTF8 : Lint = Lint {
426+ name : "utf8" ,
427+ description : indoc ! { r#"
336428Check for non-UTF8 filenames. Currently, the ostree backend of bootc only supports
337429UTF-8 filenames. Non-UTF8 filenames will cause a fatal error.
338430"# } ,
339- check_utf8,
340- ) ;
341- fn check_utf8 ( dir : & Dir ) -> LintResult {
342- let mut err = None ;
343- dir. walk (
344- & WalkConfiguration :: default ( )
345- . noxdev ( )
346- . path_base ( Path :: new ( "/" ) ) ,
347- |e| -> std:: io:: Result < _ > {
348- // Right now we stop iteration on the first non-UTF8 filename found.
349- // However in the future it'd make sense to handle multiple.
350- if err. is_some ( ) {
351- return Ok ( std:: ops:: ControlFlow :: Break ( ( ) ) ) ;
352- }
353- let path = e. path ;
354- let filename = e. filename ;
355- let dirname = path. parent ( ) . unwrap_or ( Path :: new ( "/" ) ) ;
356- if filename. to_str ( ) . is_none ( ) {
357- // This escapes like "abc\xFFdéf"
358- err = Some ( format ! (
359- "{}: Found non-utf8 filename {filename:?}" ,
360- PathQuotedDisplay :: new( & dirname)
361- ) ) ;
362- return Ok ( std:: ops:: ControlFlow :: Break ( ( ) ) ) ;
363- } ;
364-
365- if e. file_type . is_symlink ( ) {
366- let target = e. dir . read_link_contents ( filename) ?;
367- if !target. to_str ( ) . is_some ( ) {
368- err = Some ( format ! (
369- "{}: Found non-utf8 symlink target" ,
370- PathQuotedDisplay :: new( & path)
371- ) ) ;
372- return Ok ( std:: ops:: ControlFlow :: Break ( ( ) ) ) ;
373- }
374- }
375- Ok ( std:: ops:: ControlFlow :: Continue ( ( ) ) )
376- } ,
377- ) ?;
378- if let Some ( err) = err {
379- return lint_err ( err) ;
431+ ty : LintType :: Fatal ,
432+ root_type : None ,
433+ f : LintFnTy :: Recursive ( check_utf8) ,
434+ } ;
435+ fn check_utf8 ( e : & WalkComponent ) -> LintRecursiveResult {
436+ let path = e. path ;
437+ let filename = e. filename ;
438+ let dirname = path. parent ( ) . unwrap_or ( Path :: new ( "/" ) ) ;
439+ if filename. to_str ( ) . is_none ( ) {
440+ // This escapes like "abc\xFFdéf"
441+ return lint_err ( format ! (
442+ "{}: Found non-utf8 filename {filename:?}" ,
443+ PathQuotedDisplay :: new( & dirname)
444+ ) ) ;
445+ } ;
446+
447+ if e. file_type . is_symlink ( ) {
448+ let target = e. dir . read_link_contents ( filename) ?;
449+ if !target. to_str ( ) . is_some ( ) {
450+ return lint_err ( format ! (
451+ "{}: Found non-utf8 symlink target" ,
452+ PathQuotedDisplay :: new( & path)
453+ ) ) ;
454+ }
380455 }
381456 lint_ok ( )
382457}
@@ -753,10 +828,10 @@ mod tests {
753828 let root_type = RootType :: Alternative ;
754829 let r = lint_inner ( root, root_type, [ ] , & mut out) . unwrap ( ) ;
755830 let running_only_lints = LINTS . len ( ) . checked_sub ( * ALTROOT_LINTS ) . unwrap ( ) ;
756- assert_eq ! ( r. passed , * ALTROOT_LINTS ) ;
831+ assert_eq ! ( r. warnings , 0 ) ;
757832 assert_eq ! ( r. fatal, 0 ) ;
758833 assert_eq ! ( r. skipped, running_only_lints) ;
759- assert_eq ! ( r. warnings , 0 ) ;
834+ assert_eq ! ( r. passed , * ALTROOT_LINTS ) ;
760835
761836 let r = lint_inner ( root, root_type, [ "var-log" ] , & mut out) . unwrap ( ) ;
762837 // Trigger a failure in var-log
@@ -862,6 +937,26 @@ mod tests {
862937 Ok ( ( ) )
863938 }
864939
940+ fn run_recursive_lint ( root : & Dir , f : LintRecursiveFn ) -> LintResult {
941+ let mut result = lint_ok ( ) ;
942+ root. walk (
943+ & WalkConfiguration :: default ( )
944+ . noxdev ( )
945+ . path_base ( Path :: new ( "/" ) ) ,
946+ |e| -> Result < _ > {
947+ let r = f ( e) ?;
948+ match r {
949+ Ok ( ( ) ) => Ok ( ControlFlow :: Continue ( ( ) ) ) ,
950+ Err ( e) => {
951+ result = Ok ( Err ( e) ) ;
952+ Ok ( ControlFlow :: Break ( ( ) ) )
953+ }
954+ }
955+ } ,
956+ ) ?;
957+ result
958+ }
959+
865960 #[ test]
866961 fn test_non_utf8 ( ) {
867962 use std:: { ffi:: OsStr , os:: unix:: ffi:: OsStrExt } ;
@@ -879,59 +974,59 @@ mod tests {
879974 // Out-of-scope symlinks
880975 root. symlink ( "../../x" , "escape" ) . unwrap ( ) ;
881976 // Should be fine
882- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ;
977+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ;
883978
884979 // But this will cause an issue
885980 let baddir = OsStr :: from_bytes ( b"subdir/2/bad\xff dir" ) ;
886981 root. create_dir ( "subdir/2" ) . unwrap ( ) ;
887982 root. create_dir ( baddir) . unwrap ( ) ;
888- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
983+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
889984 unreachable ! ( "Didn't fail" ) ;
890985 } ;
891986 assert_eq ! (
892987 err. to_string( ) ,
893988 r#"/subdir/2: Found non-utf8 filename "bad\xFFdir""#
894989 ) ;
895990 root. remove_dir ( baddir) . unwrap ( ) ; // Get rid of the problem
896- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
991+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
897992
898993 // Create a new problem in the form of a regular file
899994 let badfile = OsStr :: from_bytes ( b"regular\xff " ) ;
900995 root. write ( badfile, b"Hello, world!\n " ) . unwrap ( ) ;
901- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
996+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
902997 unreachable ! ( "Didn't fail" ) ;
903998 } ;
904999 assert_eq ! (
9051000 err. to_string( ) ,
9061001 r#"/: Found non-utf8 filename "regular\xFF""#
9071002 ) ;
9081003 root. remove_file ( badfile) . unwrap ( ) ; // Get rid of the problem
909- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
1004+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
9101005
9111006 // And now test invalid symlink targets
9121007 root. symlink ( badfile, "subdir/good-name" ) . unwrap ( ) ;
913- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
1008+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
9141009 unreachable ! ( "Didn't fail" ) ;
9151010 } ;
9161011 assert_eq ! (
9171012 err. to_string( ) ,
9181013 r#"/subdir/good-name: Found non-utf8 symlink target"#
9191014 ) ;
9201015 root. remove_file ( "subdir/good-name" ) . unwrap ( ) ; // Get rid of the problem
921- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
1016+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
9221017
9231018 // Finally, test a self-referential symlink with an invalid name.
9241019 // We should spot the invalid name before we check the target.
9251020 root. symlink ( badfile, badfile) . unwrap ( ) ;
926- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
1021+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
9271022 unreachable ! ( "Didn't fail" ) ;
9281023 } ;
9291024 assert_eq ! (
9301025 err. to_string( ) ,
9311026 r#"/: Found non-utf8 filename "regular\xFF""#
9321027 ) ;
9331028 root. remove_file ( badfile) . unwrap ( ) ; // Get rid of the problem
934- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
1029+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
9351030 }
9361031
9371032 #[ test]
0 commit comments