5
5
// Unfortunately needed here to work with linkme
6
6
#![ allow( unsafe_code) ]
7
7
8
- use std:: collections:: BTreeSet ;
8
+ use std:: collections:: { BTreeMap , BTreeSet } ;
9
9
use std:: env:: consts:: ARCH ;
10
10
use std:: fmt:: Write as WriteFmt ;
11
+ use std:: ops:: ControlFlow ;
11
12
use std:: os:: unix:: ffi:: OsStrExt ;
12
13
use std:: path:: Path ;
13
14
@@ -17,7 +18,8 @@ use camino::{Utf8Path, Utf8PathBuf};
17
18
use cap_std:: fs:: Dir ;
18
19
use cap_std_ext:: cap_std;
19
20
use 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 } ;
21
23
use fn_error_context:: context;
22
24
use indoc:: indoc;
23
25
use linkme:: distributed_slice;
@@ -61,6 +63,17 @@ impl LintError {
61
63
}
62
64
63
65
type 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
+ }
64
77
#[ distributed_slice]
65
78
pub ( crate ) static LINTS : [ Lint ] ;
66
79
@@ -94,13 +107,38 @@ struct Lint {
94
107
#[ serde( rename = "type" ) ]
95
108
ty : LintType ,
96
109
#[ serde( skip) ]
97
- f : LintFn ,
110
+ f : LintFnTy ,
98
111
description : & ' static str ,
99
112
// Set if this only applies to a specific root type.
100
113
#[ serde( skip_serializing_if = "Option::is_none" ) ]
101
114
root_type : Option < RootType > ,
102
115
}
103
116
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
+
104
142
impl Lint {
105
143
pub ( crate ) const fn new_fatal (
106
144
name : & ' static str ,
@@ -110,7 +148,7 @@ impl Lint {
110
148
Lint {
111
149
name : name,
112
150
ty : LintType :: Fatal ,
113
- f : f ,
151
+ f : LintFnTy :: Regular ( f ) ,
114
152
description : description,
115
153
root_type : None ,
116
154
}
@@ -124,7 +162,7 @@ impl Lint {
124
162
Lint {
125
163
name : name,
126
164
ty : LintType :: Warning ,
127
- f : f ,
165
+ f : LintFnTy :: Regular ( f ) ,
128
166
description : description,
129
167
root_type : None ,
130
168
}
@@ -159,24 +197,78 @@ fn lint_inner<'skip>(
159
197
let mut fatal = 0usize ;
160
198
let mut warnings = 0usize ;
161
199
let mut passed = 0usize ;
162
- let mut skipped = 0usize ;
163
200
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 ;
170
204
}
171
-
172
205
if let Some ( lint_root_type) = lint. root_type {
173
206
if lint_root_type != root_type {
174
- skipped += 1 ;
175
- continue ;
207
+ return false ;
176
208
}
177
209
}
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
+ }
178
228
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 {
180
272
Ok ( r) => r,
181
273
Err ( e) => anyhow:: bail!( "Unexpected runtime error running lint {name}: {e}" ) ,
182
274
} ;
@@ -330,53 +422,36 @@ fn check_kernel(root: &Dir) -> LintResult {
330
422
331
423
// This one can be lifted in the future, see https://github.com/containers/bootc/issues/975
332
424
#[ 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#"
336
428
Check for non-UTF8 filenames. Currently, the ostree backend of bootc only supports
337
429
UTF-8 filenames. Non-UTF8 filenames will cause a fatal error.
338
430
"# } ,
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
+ }
380
455
}
381
456
lint_ok ( )
382
457
}
@@ -753,10 +828,10 @@ mod tests {
753
828
let root_type = RootType :: Alternative ;
754
829
let r = lint_inner ( root, root_type, [ ] , & mut out) . unwrap ( ) ;
755
830
let running_only_lints = LINTS . len ( ) . checked_sub ( * ALTROOT_LINTS ) . unwrap ( ) ;
756
- assert_eq ! ( r. passed , * ALTROOT_LINTS ) ;
831
+ assert_eq ! ( r. warnings , 0 ) ;
757
832
assert_eq ! ( r. fatal, 0 ) ;
758
833
assert_eq ! ( r. skipped, running_only_lints) ;
759
- assert_eq ! ( r. warnings , 0 ) ;
834
+ assert_eq ! ( r. passed , * ALTROOT_LINTS ) ;
760
835
761
836
let r = lint_inner ( root, root_type, [ "var-log" ] , & mut out) . unwrap ( ) ;
762
837
// Trigger a failure in var-log
@@ -862,6 +937,26 @@ mod tests {
862
937
Ok ( ( ) )
863
938
}
864
939
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
+
865
960
#[ test]
866
961
fn test_non_utf8 ( ) {
867
962
use std:: { ffi:: OsStr , os:: unix:: ffi:: OsStrExt } ;
@@ -879,59 +974,59 @@ mod tests {
879
974
// Out-of-scope symlinks
880
975
root. symlink ( "../../x" , "escape" ) . unwrap ( ) ;
881
976
// Should be fine
882
- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ;
977
+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ;
883
978
884
979
// But this will cause an issue
885
980
let baddir = OsStr :: from_bytes ( b"subdir/2/bad\xff dir" ) ;
886
981
root. create_dir ( "subdir/2" ) . unwrap ( ) ;
887
982
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 {
889
984
unreachable ! ( "Didn't fail" ) ;
890
985
} ;
891
986
assert_eq ! (
892
987
err. to_string( ) ,
893
988
r#"/subdir/2: Found non-utf8 filename "bad\xFFdir""#
894
989
) ;
895
990
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
897
992
898
993
// Create a new problem in the form of a regular file
899
994
let badfile = OsStr :: from_bytes ( b"regular\xff " ) ;
900
995
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 {
902
997
unreachable ! ( "Didn't fail" ) ;
903
998
} ;
904
999
assert_eq ! (
905
1000
err. to_string( ) ,
906
1001
r#"/: Found non-utf8 filename "regular\xFF""#
907
1002
) ;
908
1003
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
910
1005
911
1006
// And now test invalid symlink targets
912
1007
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 {
914
1009
unreachable ! ( "Didn't fail" ) ;
915
1010
} ;
916
1011
assert_eq ! (
917
1012
err. to_string( ) ,
918
1013
r#"/subdir/good-name: Found non-utf8 symlink target"#
919
1014
) ;
920
1015
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
922
1017
923
1018
// Finally, test a self-referential symlink with an invalid name.
924
1019
// We should spot the invalid name before we check the target.
925
1020
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 {
927
1022
unreachable ! ( "Didn't fail" ) ;
928
1023
} ;
929
1024
assert_eq ! (
930
1025
err. to_string( ) ,
931
1026
r#"/: Found non-utf8 filename "regular\xFF""#
932
1027
) ;
933
1028
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
935
1030
}
936
1031
937
1032
#[ test]
0 commit comments