Skip to content

Commit 3dbff8c

Browse files
authored
Merge pull request #1218 from cgwalters/lint-global-traversal-prep
lints: Add recursive lint traversal infrastructure
2 parents 5f1cc22 + 67283ac commit 3dbff8c

File tree

1 file changed

+166
-71
lines changed

1 file changed

+166
-71
lines changed

lib/src/lints.rs

+166-71
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
// Unfortunately needed here to work with linkme
66
#![allow(unsafe_code)]
77

8-
use std::collections::BTreeSet;
8+
use std::collections::{BTreeMap, BTreeSet};
99
use std::env::consts::ARCH;
1010
use std::fmt::Write as WriteFmt;
11+
use std::ops::ControlFlow;
1112
use std::os::unix::ffi::OsStrExt;
1213
use std::path::Path;
1314

@@ -17,7 +18,8 @@ use camino::{Utf8Path, Utf8PathBuf};
1718
use cap_std::fs::Dir;
1819
use cap_std_ext::cap_std;
1920
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};
2123
use fn_error_context::context;
2224
use indoc::indoc;
2325
use linkme::distributed_slice;
@@ -61,6 +63,17 @@ impl LintError {
6163
}
6264

6365
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+
}
6477
#[distributed_slice]
6578
pub(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+
104142
impl 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#"
336428
Check for non-UTF8 filenames. Currently, the ostree backend of bootc only supports
337429
UTF-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\xffdir");
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

Comments
 (0)