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,79 @@ 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
+ assert ! ( !recursive_lints. is_empty( ) ) ;
230
+ let mut recursive_lints = BTreeSet :: from_iter ( recursive_lints. into_iter ( ) ) ;
231
+ let mut recursive_errors = BTreeMap :: new ( ) ;
232
+ root. walk (
233
+ & WalkConfiguration :: default ( )
234
+ . noxdev ( )
235
+ . path_base ( Path :: new ( "/" ) ) ,
236
+ |e| -> std:: io:: Result < _ > {
237
+ // If there's no recursive lints, we're done!
238
+ if recursive_lints. is_empty ( ) {
239
+ return Ok ( ControlFlow :: Break ( ( ) ) ) ;
240
+ }
241
+ // Keep track of any errors we caught while iterating over
242
+ // the recursive lints.
243
+ let mut this_iteration_errors = Vec :: new ( ) ;
244
+ // Call each recursive lint on this directory entry.
245
+ for & lint in recursive_lints. iter ( ) {
246
+ let f = match & lint. f {
247
+ // SAFETY: We know this set only holds recursive lints
248
+ LintFnTy :: Regular ( _) => unreachable ! ( ) ,
249
+ LintFnTy :: Recursive ( f) => f,
250
+ } ;
251
+ // Keep track of the error if we found one
252
+ match f ( e) {
253
+ Ok ( Ok ( ( ) ) ) => { }
254
+ o => this_iteration_errors. push ( ( lint, o) ) ,
255
+ }
256
+ }
257
+ // For each recursive lint that errored, remove it from
258
+ // the set that we will continue running.
259
+ for ( lint, err) in this_iteration_errors {
260
+ recursive_lints. remove ( lint) ;
261
+ recursive_errors. insert ( lint, err) ;
262
+ }
263
+ Ok ( ControlFlow :: Continue ( ( ) ) )
264
+ } ,
265
+ ) ?;
266
+ // Extend our overall result set with the recursive-lint errors.
267
+ results. extend ( recursive_errors. into_iter ( ) . map ( |( lint, e) | ( lint, e) ) ) ;
268
+ // Any recursive lint still in this list succeeded.
269
+ results. extend ( recursive_lints. into_iter ( ) . map ( |lint| ( lint, lint_ok ( ) ) ) ) ;
270
+ for ( lint, r) in results {
271
+ let name = lint. name ;
272
+ let r = match r {
180
273
Ok ( r) => r,
181
274
Err ( e) => anyhow:: bail!( "Unexpected runtime error running lint {name}: {e}" ) ,
182
275
} ;
@@ -330,53 +423,36 @@ fn check_kernel(root: &Dir) -> LintResult {
330
423
331
424
// This one can be lifted in the future, see https://github.com/containers/bootc/issues/975
332
425
#[ distributed_slice( LINTS ) ]
333
- static LINT_UTF8 : Lint = Lint :: new_fatal (
334
- "utf8" ,
335
- indoc ! { r#"
426
+ static LINT_UTF8 : Lint = Lint {
427
+ name : "utf8" ,
428
+ description : indoc ! { r#"
336
429
Check for non-UTF8 filenames. Currently, the ostree backend of bootc only supports
337
430
UTF-8 filenames. Non-UTF8 filenames will cause a fatal error.
338
431
"# } ,
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) ;
432
+ ty : LintType :: Fatal ,
433
+ root_type : None ,
434
+ f : LintFnTy :: Recursive ( check_utf8) ,
435
+ } ;
436
+ fn check_utf8 ( e : & WalkComponent ) -> LintRecursiveResult {
437
+ let path = e. path ;
438
+ let filename = e. filename ;
439
+ let dirname = path. parent ( ) . unwrap_or ( Path :: new ( "/" ) ) ;
440
+ if filename. to_str ( ) . is_none ( ) {
441
+ // This escapes like "abc\xFFdéf"
442
+ return lint_err ( format ! (
443
+ "{}: Found non-utf8 filename {filename:?}" ,
444
+ PathQuotedDisplay :: new( & dirname)
445
+ ) ) ;
446
+ } ;
447
+
448
+ if e. file_type . is_symlink ( ) {
449
+ let target = e. dir . read_link_contents ( filename) ?;
450
+ if !target. to_str ( ) . is_some ( ) {
451
+ return lint_err ( format ! (
452
+ "{}: Found non-utf8 symlink target" ,
453
+ PathQuotedDisplay :: new( & path)
454
+ ) ) ;
455
+ }
380
456
}
381
457
lint_ok ( )
382
458
}
@@ -753,10 +829,10 @@ mod tests {
753
829
let root_type = RootType :: Alternative ;
754
830
let r = lint_inner ( root, root_type, [ ] , & mut out) . unwrap ( ) ;
755
831
let running_only_lints = LINTS . len ( ) . checked_sub ( * ALTROOT_LINTS ) . unwrap ( ) ;
756
- assert_eq ! ( r. passed , * ALTROOT_LINTS ) ;
832
+ assert_eq ! ( r. warnings , 0 ) ;
757
833
assert_eq ! ( r. fatal, 0 ) ;
758
834
assert_eq ! ( r. skipped, running_only_lints) ;
759
- assert_eq ! ( r. warnings , 0 ) ;
835
+ assert_eq ! ( r. passed , * ALTROOT_LINTS ) ;
760
836
761
837
let r = lint_inner ( root, root_type, [ "var-log" ] , & mut out) . unwrap ( ) ;
762
838
// Trigger a failure in var-log
@@ -862,6 +938,26 @@ mod tests {
862
938
Ok ( ( ) )
863
939
}
864
940
941
+ fn run_recursive_lint ( root : & Dir , f : LintRecursiveFn ) -> LintResult {
942
+ let mut result = lint_ok ( ) ;
943
+ root. walk (
944
+ & WalkConfiguration :: default ( )
945
+ . noxdev ( )
946
+ . path_base ( Path :: new ( "/" ) ) ,
947
+ |e| -> Result < _ > {
948
+ let r = f ( e) ?;
949
+ match r {
950
+ Ok ( ( ) ) => Ok ( ControlFlow :: Continue ( ( ) ) ) ,
951
+ Err ( e) => {
952
+ result = Ok ( Err ( e) ) ;
953
+ Ok ( ControlFlow :: Break ( ( ) ) )
954
+ }
955
+ }
956
+ } ,
957
+ ) ?;
958
+ result
959
+ }
960
+
865
961
#[ test]
866
962
fn test_non_utf8 ( ) {
867
963
use std:: { ffi:: OsStr , os:: unix:: ffi:: OsStrExt } ;
@@ -879,59 +975,59 @@ mod tests {
879
975
// Out-of-scope symlinks
880
976
root. symlink ( "../../x" , "escape" ) . unwrap ( ) ;
881
977
// Should be fine
882
- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ;
978
+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ;
883
979
884
980
// But this will cause an issue
885
981
let baddir = OsStr :: from_bytes ( b"subdir/2/bad\xff dir" ) ;
886
982
root. create_dir ( "subdir/2" ) . unwrap ( ) ;
887
983
root. create_dir ( baddir) . unwrap ( ) ;
888
- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
984
+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
889
985
unreachable ! ( "Didn't fail" ) ;
890
986
} ;
891
987
assert_eq ! (
892
988
err. to_string( ) ,
893
989
r#"/subdir/2: Found non-utf8 filename "bad\xFFdir""#
894
990
) ;
895
991
root. remove_dir ( baddir) . unwrap ( ) ; // Get rid of the problem
896
- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
992
+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
897
993
898
994
// Create a new problem in the form of a regular file
899
995
let badfile = OsStr :: from_bytes ( b"regular\xff " ) ;
900
996
root. write ( badfile, b"Hello, world!\n " ) . unwrap ( ) ;
901
- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
997
+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
902
998
unreachable ! ( "Didn't fail" ) ;
903
999
} ;
904
1000
assert_eq ! (
905
1001
err. to_string( ) ,
906
1002
r#"/: Found non-utf8 filename "regular\xFF""#
907
1003
) ;
908
1004
root. remove_file ( badfile) . unwrap ( ) ; // Get rid of the problem
909
- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
1005
+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
910
1006
911
1007
// And now test invalid symlink targets
912
1008
root. symlink ( badfile, "subdir/good-name" ) . unwrap ( ) ;
913
- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
1009
+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
914
1010
unreachable ! ( "Didn't fail" ) ;
915
1011
} ;
916
1012
assert_eq ! (
917
1013
err. to_string( ) ,
918
1014
r#"/subdir/good-name: Found non-utf8 symlink target"#
919
1015
) ;
920
1016
root. remove_file ( "subdir/good-name" ) . unwrap ( ) ; // Get rid of the problem
921
- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
1017
+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
922
1018
923
1019
// Finally, test a self-referential symlink with an invalid name.
924
1020
// We should spot the invalid name before we check the target.
925
1021
root. symlink ( badfile, badfile) . unwrap ( ) ;
926
- let Err ( err) = check_utf8 ( root) . unwrap ( ) else {
1022
+ let Err ( err) = run_recursive_lint ( root, check_utf8 ) . unwrap ( ) else {
927
1023
unreachable ! ( "Didn't fail" ) ;
928
1024
} ;
929
1025
assert_eq ! (
930
1026
err. to_string( ) ,
931
1027
r#"/: Found non-utf8 filename "regular\xFF""#
932
1028
) ;
933
1029
root. remove_file ( badfile) . unwrap ( ) ; // Get rid of the problem
934
- check_utf8 ( root) . unwrap ( ) . unwrap ( ) ; // Check it
1030
+ run_recursive_lint ( root, check_utf8 ) . unwrap ( ) . unwrap ( ) ; // Check it
935
1031
}
936
1032
937
1033
#[ test]
0 commit comments