Skip to content

Commit 78b0484

Browse files
committed
lints: Add recursive lint traversal infrastructure
Right now the `utf8` lint does a tree walk. I want to add more, but it'd be good to avoid walking the whole filesystem multiple times. In paticular I wanted to add a check for `ostree.usermeta` should never be present. Signed-off-by: Colin Walters <[email protected]>
1 parent 0371407 commit 78b0484

File tree

1 file changed

+167
-71
lines changed

1 file changed

+167
-71
lines changed

lib/src/lints.rs

+167-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,79 @@ 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+
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 {
180273
Ok(r) => r,
181274
Err(e) => anyhow::bail!("Unexpected runtime error running lint {name}: {e}"),
182275
};
@@ -330,53 +423,36 @@ fn check_kernel(root: &Dir) -> LintResult {
330423

331424
// This one can be lifted in the future, see https://github.com/containers/bootc/issues/975
332425
#[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#"
336429
Check for non-UTF8 filenames. Currently, the ostree backend of bootc only supports
337430
UTF-8 filenames. Non-UTF8 filenames will cause a fatal error.
338431
"#},
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+
}
380456
}
381457
lint_ok()
382458
}
@@ -753,10 +829,10 @@ mod tests {
753829
let root_type = RootType::Alternative;
754830
let r = lint_inner(root, root_type, [], &mut out).unwrap();
755831
let running_only_lints = LINTS.len().checked_sub(*ALTROOT_LINTS).unwrap();
756-
assert_eq!(r.passed, *ALTROOT_LINTS);
832+
assert_eq!(r.warnings, 0);
757833
assert_eq!(r.fatal, 0);
758834
assert_eq!(r.skipped, running_only_lints);
759-
assert_eq!(r.warnings, 0);
835+
assert_eq!(r.passed, *ALTROOT_LINTS);
760836

761837
let r = lint_inner(root, root_type, ["var-log"], &mut out).unwrap();
762838
// Trigger a failure in var-log
@@ -862,6 +938,26 @@ mod tests {
862938
Ok(())
863939
}
864940

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+
865961
#[test]
866962
fn test_non_utf8() {
867963
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
@@ -879,59 +975,59 @@ mod tests {
879975
// Out-of-scope symlinks
880976
root.symlink("../../x", "escape").unwrap();
881977
// Should be fine
882-
check_utf8(root).unwrap().unwrap();
978+
run_recursive_lint(root, check_utf8).unwrap().unwrap();
883979

884980
// But this will cause an issue
885981
let baddir = OsStr::from_bytes(b"subdir/2/bad\xffdir");
886982
root.create_dir("subdir/2").unwrap();
887983
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 {
889985
unreachable!("Didn't fail");
890986
};
891987
assert_eq!(
892988
err.to_string(),
893989
r#"/subdir/2: Found non-utf8 filename "bad\xFFdir""#
894990
);
895991
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
897993

898994
// Create a new problem in the form of a regular file
899995
let badfile = OsStr::from_bytes(b"regular\xff");
900996
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 {
902998
unreachable!("Didn't fail");
903999
};
9041000
assert_eq!(
9051001
err.to_string(),
9061002
r#"/: Found non-utf8 filename "regular\xFF""#
9071003
);
9081004
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
9101006

9111007
// And now test invalid symlink targets
9121008
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 {
9141010
unreachable!("Didn't fail");
9151011
};
9161012
assert_eq!(
9171013
err.to_string(),
9181014
r#"/subdir/good-name: Found non-utf8 symlink target"#
9191015
);
9201016
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
9221018

9231019
// Finally, test a self-referential symlink with an invalid name.
9241020
// We should spot the invalid name before we check the target.
9251021
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 {
9271023
unreachable!("Didn't fail");
9281024
};
9291025
assert_eq!(
9301026
err.to_string(),
9311027
r#"/: Found non-utf8 filename "regular\xFF""#
9321028
);
9331029
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
9351031
}
9361032

9371033
#[test]

0 commit comments

Comments
 (0)