Files
libfreemkv/tests/lint_stop.rs
T
Matthew Jackson 25fd393c04 session, io: Stop through held sessions, kept partials, <final>.lock
Key-source contexts carry the op token and progress; DiscSession gains
open_with, scan_with and finish on the held handle. A consumer that
closes after a cancel keeps *.partial, and an artifact lock sidecar
guards each final output. Stop rules are enforced as lint tests.
2026-09-28 09:11:34 -07:00

693 lines
23 KiB
Rust

//! Stop lints (stop design §5.8, ST-X2): grep tests over the production source.
//!
//! "These replace v4's Sim-motivated `disallowed-methods` list": clippy's list is
//! crate-wide, and these rules are scoped by directory and exempt tests. The walk
//! follows `mod` declarations from `src/lib.rs`, drops every item or file gated on a
//! test-only `cfg` (`test`, `feature = "test-util"`), and masks comments and literals,
//! so only code that ships is checked. Every allow-list entry names its site and must
//! still match, so an entry cannot outlive its reason.
use std::collections::BTreeMap;
use std::path::Path;
struct Src {
path: String,
raw: String,
code: String,
fns: Vec<(usize, usize, String)>,
}
impl Src {
fn line(&self, at: usize) -> usize {
self.code[..at].matches('\n').count() + 1
}
// The innermost `fn` whose body contains `at`.
fn enclosing_fn(&self, at: usize) -> &str {
self.fns
.iter()
.filter(|(s, e, _)| *s <= at && at < *e)
.min_by_key(|(s, e, _)| e - s)
.map_or("", |(_, _, n)| n.as_str())
}
fn hits(&self, pat: &str) -> Vec<usize> {
self.code.match_indices(pat).map(|(i, _)| i).collect()
}
}
fn is_ident(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn blank(out: &mut [u8], from: usize, to: usize) {
for b in &mut out[from..to] {
if *b != b'\n' {
*b = b' ';
}
}
}
// Comments and string/char literal bodies become spaces; offsets and newlines are kept.
fn mask(src: &str) -> Vec<u8> {
let s = src.as_bytes();
let mut out = s.to_vec();
let mut i = 0;
while i < s.len() {
let prev_ident = i > 0 && is_ident(s[i - 1]);
if s[i..].starts_with(b"//") {
let end = s[i..]
.iter()
.position(|&b| b == b'\n')
.map_or(s.len(), |p| i + p);
blank(&mut out, i, end);
i = end;
} else if s[i..].starts_with(b"/*") {
let (mut depth, mut j) = (0, i);
while j < s.len() {
if s[j..].starts_with(b"/*") {
depth += 1;
j += 2;
} else if s[j..].starts_with(b"*/") {
depth -= 1;
j += 2;
if depth == 0 {
break;
}
} else {
j += 1;
}
}
blank(&mut out, i, j);
i = j;
} else if s[i] == b'r' && !prev_ident || s[i..].starts_with(b"br") && !prev_ident {
let start = if s[i] == b'b' { i + 2 } else { i + 1 };
let hashes = s[start..].iter().take_while(|&&b| b == b'#').count();
if s.get(start + hashes) != Some(&b'"') {
i += 1;
continue;
}
let body = start + hashes + 1;
let close: Vec<u8> = std::iter::once(b'"')
.chain(std::iter::repeat_n(b'#', hashes))
.collect();
let end = body
+ s[body..]
.windows(close.len())
.position(|w| w == close.as_slice())
.expect("unterminated raw string");
blank(&mut out, body, end);
i = end + close.len();
} else if s[i] == b'"' {
let mut j = i + 1;
while s[j] != b'"' {
j += if s[j] == b'\\' { 2 } else { 1 };
}
blank(&mut out, i + 1, j);
i = j + 1;
} else if s[i] == b'\'' {
let end = if s.get(i + 1) == Some(&b'\\') {
Some(i + 2 + s[i + 2..].iter().position(|&b| b == b'\'').unwrap())
} else {
let c = src[i + 1..].chars().next().map_or(1, char::len_utf8);
(s.get(i + 1 + c) == Some(&b'\'')).then_some(i + 1 + c)
};
match end {
Some(e) => {
blank(&mut out, i + 1, e);
i = e + 1;
}
None => i += 1, // a lifetime or label
}
} else {
i += 1;
}
}
out
}
// The index just past the `]` or `}` that closes the `[` or `{` at `open`.
fn close_of(code: &[u8], open: usize) -> usize {
let (o, c) = match code[open] {
b'[' => (b'[', b']'),
b'{' => (b'{', b'}'),
b => panic!("close_of on `{}`", b as char),
};
let mut depth = 0i32;
for (k, &b) in code[open..].iter().enumerate() {
if b == o {
depth += 1;
} else if b == c {
depth -= 1;
if depth == 0 {
return open + k + 1;
}
}
}
panic!("unbalanced `{}`", o as char);
}
// The end of the item starting at `from`: its `{…}` body, or the first top-level `;` or
// `,` (a field, variant or arm); an enclosing block's `}` also ends it.
fn item_end(code: &[u8], from: usize) -> usize {
let (mut depth, mut angle) = (0i32, 0i32);
for k in from..code.len() {
match code[k] {
b'(' | b'[' => depth += 1,
b')' | b']' => depth -= 1,
b'<' => angle += 1,
b'>' if k > 0 && !matches!(code[k - 1], b'-' | b'=') => angle -= 1,
b'}' if depth == 0 => return k,
b';' if depth == 0 => return k + 1,
b',' if depth == 0 && angle <= 0 => return k + 1,
b'{' if depth == 0 => {
let end = close_of(code, k);
return end + usize::from(code.get(end) == Some(&b';'));
}
_ => {}
}
}
code.len()
}
// Whether a `cfg` predicate holds only in test builds (`test`, the `test-util` fixtures).
fn test_only(pred: &str) -> bool {
let pred = pred.trim();
if pred == "test" || pred.replace(' ', "") == "feature=\"test-util\"" {
return true;
}
let Some((head, rest)) = pred.split_once('(') else {
return false;
};
let inner = rest.strip_suffix(')').unwrap_or(rest);
let mut args = Vec::new();
let (mut depth, mut start) = (0, 0);
for (k, c) in inner.char_indices() {
match c {
'(' => depth += 1,
')' => depth -= 1,
',' if depth == 0 => {
args.push(&inner[start..k]);
start = k + 1;
}
_ => {}
}
}
args.push(&inner[start..]);
args.retain(|a| !a.trim().is_empty());
match head.trim() {
"all" => args.iter().any(|a| test_only(a)),
"any" => !args.is_empty() && args.iter().all(|a| test_only(a)),
_ => false,
}
}
// The attributes `#[..]` / `#![..]` in `code`: (start, end, raw text inside the brackets).
fn attrs<'a>(code: &[u8], raw: &'a str) -> Vec<(usize, usize, &'a str)> {
let mut out = Vec::new();
for (i, _) in raw.match_indices('#') {
if code[i] != b'#' {
continue; // inside a masked comment or literal
}
let open = match (code.get(i + 1), code.get(i + 2)) {
(Some(b'['), _) => i + 1,
(Some(b'!'), Some(b'[')) => i + 2,
_ => continue,
};
let end = close_of(code, open);
out.push((i, end, &raw[open + 1..end - 1]));
}
out
}
fn cfg_pred(attr: &str) -> Option<&str> {
let a = attr
.trim()
.strip_prefix("cfg")?
.trim_start()
.strip_prefix('(')?;
a.strip_suffix(')')
}
// Blank every test-only item; `None` when the whole file is test-only (`#![cfg(test)]`).
fn drop_test_items(raw: &str, code: &mut [u8]) -> Option<()> {
for (start, end, attr) in attrs(code, raw) {
if code[start] != b'#' || !cfg_pred(attr).is_some_and(test_only) {
continue;
}
if code[start + 1] == b'!' {
return None;
}
let stop = item_end(code, end);
blank(code, start, stop);
}
Some(())
}
fn fn_spans(code: &[u8]) -> Vec<(usize, usize, String)> {
let text = std::str::from_utf8(code).unwrap();
let mut out = Vec::new();
for (i, _) in text.match_indices("fn ") {
if i > 0 && is_ident(code[i - 1]) {
continue;
}
let name: String = text[i + 3..]
.trim_start()
.chars()
.take_while(|&c| c.is_alphanumeric() || c == '_')
.collect();
if name.is_empty() {
continue;
}
let mut depth = 0i32;
for k in i..code.len() {
match code[k] {
b'(' | b'[' => depth += 1,
b')' | b']' => depth -= 1,
b';' if depth == 0 => break,
b'{' if depth == 0 => {
out.push((i, close_of(code, k), name));
break;
}
_ => {}
}
}
}
out
}
// Every production source file, reached from `src/lib.rs` through non-test `mod` items.
fn production() -> Vec<Src> {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut out = Vec::new();
let mut todo = vec![root.join("src/lib.rs")];
while let Some(file) = todo.pop() {
let raw = std::fs::read_to_string(&file).unwrap_or_else(|e| panic!("{file:?}: {e}"));
let mut code = mask(&raw);
if drop_test_items(&raw, &mut code).is_none() {
continue;
}
let text = std::str::from_utf8(&code).unwrap();
let name = file.file_name().unwrap().to_str().unwrap();
let dir = if matches!(name, "lib.rs" | "mod.rs") {
file.parent().unwrap().to_path_buf()
} else {
file.with_extension("")
};
let attrs = attrs(&code, &raw);
for (i, _) in text.match_indices("mod ") {
if i > 0 && is_ident(code[i - 1]) {
continue;
}
let rest = &text[i + 4..];
let m: String = rest.chars().take_while(|&c| is_ident(c as u8)).collect();
let after = rest[m.len()..].trim_start();
if m.is_empty() || !after.starts_with(';') {
continue;
}
// `#[path]` in the attribute run just above this declaration.
let mut head = text[..i].trim_end();
head = head.strip_suffix("pub(crate)").unwrap_or(head);
head = head.strip_suffix("pub").unwrap_or(head).trim_end();
let mut path = None;
while let Some((s, _, a)) = attrs.iter().find(|(_, e, _)| *e == head.len()) {
if let Some(p) = a.trim().strip_prefix("path") {
path = p.split('"').nth(1);
}
head = text[..*s].trim_end();
}
let next = match path {
Some(p) => file.parent().unwrap().join(p),
None if dir.join(format!("{m}.rs")).exists() => dir.join(format!("{m}.rs")),
None => dir.join(&m).join("mod.rs"),
};
todo.push(next);
}
let rel = file
.strip_prefix(root)
.unwrap()
.to_string_lossy()
.replace('\\', "/");
let code = String::from_utf8(code).unwrap();
let fns = fn_spans(code.as_bytes());
out.push(Src {
path: rel,
raw,
code,
fns,
});
}
out.sort_by(|a, b| a.path.cmp(&b.path));
out
}
// An allow-listed site: (file, enclosing fn, why).
type Allow = (&'static str, &'static str, &'static str);
// Hits not on `allow`, plus allow entries that matched nothing (stale).
fn check(rule: &str, hits: Vec<(&Src, usize)>, allow: &[Allow]) {
let mut used = vec![false; allow.len()];
let mut bad = Vec::new();
for (src, at) in hits {
let f = src.enclosing_fn(at);
match allow.iter().position(|(p, n, _)| *p == src.path && *n == f) {
Some(k) => used[k] = true,
None => bad.push(format!("{}:{} (fn {f})", src.path, src.line(at))),
}
}
for (k, (p, n, _)) in allow.iter().enumerate() {
if !used[k] {
bad.push(format!("stale allow-list entry {p} fn {n}"));
}
}
assert!(bad.is_empty(), "{rule}:\n {}", bad.join("\n "));
}
fn in_dirs(src: &Src, dirs: &[&str]) -> bool {
dirs.iter().any(|d| src.path.starts_with(d))
}
fn not_halt(src: &Src) -> bool {
src.path != "src/halt.rs" && !src.path.starts_with("src/halt/")
}
/// §5.8 "Ban `std::thread::sleep` on op paths: libfreemkv `src/{drive,disc,sector,io,mux}`
/// … Use `Halt::wait` / `pause` instead." Also catches `use std::thread::{sleep, ..}`.
#[test]
fn no_thread_sleep_on_op_paths() {
const OP_DIRS: &[&str] = &[
"src/drive/",
"src/disc/",
"src/sector/",
"src/io/",
"src/mux/",
];
const ALLOW: &[Allow] = &[
(
"src/disc/extract.rs",
"probe_case_insensitive",
"20 ms filesystem remove retry; no drive wait, no token in reach",
),
(
"src/mux/network.rs",
"accept_from_with_halt",
"halt-checked accept poll; open item: convert to Halt::wait",
),
];
let srcs = production();
let mut hits = Vec::new();
for s in srcs.iter().filter(|s| in_dirs(s, OP_DIRS)) {
hits.extend(s.hits("thread::sleep").into_iter().map(|i| (s, i)));
for i in s.hits("thread::{") {
let group = &s.code[i..close_of(s.code.as_bytes(), i + 8)];
if group
.split(|c: char| !is_ident(c as u8))
.any(|w| w == "sleep")
{
hits.push((s, i));
}
}
}
check("thread::sleep on an op path", hits, ALLOW);
}
/// §2.2: "**Every** raw `self.scsi.as_mut().execute` in `drive/mod.rs` and `identity.rs`
/// goes through [`Drive::exec`]"; §5.8 widens the grep to all of `drive/`.
#[test]
fn no_raw_execute_in_drive_or_identity() {
const ALLOW: &[Allow] = &[
(
"src/drive/mod.rs",
"dispatch",
"the one transport call behind exec / exec_cleanup / exec_uncancellable",
),
(
"src/identity.rs",
"from_drive",
"public API over a bare transport; Drive::open uses identify(exec)",
),
];
let srcs = production();
let mut hits = Vec::new();
let mut scanned = 0;
for s in srcs
.iter()
.filter(|s| s.path.starts_with("src/drive/") || s.path == "src/identity.rs")
{
scanned += 1;
for pat in [".execute(", ".execute_cleanup("] {
hits.extend(s.hits(pat).into_iter().map(|i| (s, i)));
}
}
assert!(scanned >= 5, "the walk lost drive/ files: {scanned}");
check("raw transport execute outside Drive::exec", hits, ALLOW);
}
/// §5.8 "A raw-halt-load grep test. Its allow-list: the `from_arc` bridges and the patch
/// latch." A `Halt`'s flag is read via `check` / `is_cancelled`, never as a raw atomic.
/// The patch latch is the engine's (§4.2 `EngineHalt`); libfreemkv has none.
#[test]
fn no_raw_halt_loads_in_production() {
const ALLOW: &[Allow] = &[
(
"src/drive/mod.rs",
"halt_flag",
"from_arc bridge: the token as the pub Arc<AtomicBool> view",
),
(
"src/scsi/macos.rs",
"cancel_byte",
"from_arc bridge: the token as the macOS shim's cancel byte",
),
];
const ATOMIC_OPS: &[&str] = &[
".load(",
".store(",
".swap(",
".fetch_or(",
".fetch_and(",
".fetch_xor(",
".fetch_nand(",
".compare_exchange(",
".compare_exchange_weak(",
".get_mut(",
".into_inner(",
".as_ptr(",
];
let srcs = production();
let mut hits = Vec::new();
for s in srcs.iter().filter(|s| not_halt(s)) {
for pat in [".as_arc()", "from_arc(", ".halt_flag()"] {
for i in s.hits(pat) {
let line = s.code[..i].rfind('\n').map_or(0, |n| n + 1);
let eol = s.code[i..].find('\n').map_or(s.code.len(), |n| i + n);
// `Arc::ptr_eq` compares the two tokens' identity; it reads no flag.
if !s.code[line..eol].contains("Arc::ptr_eq(") {
hits.push((s, i));
}
}
}
for pat in ATOMIC_OPS {
for i in s.hits(pat) {
let recv = s.code[..i]
.rsplit(|c: char| c.is_whitespace() || "(&*!,{;=".contains(c))
.next()
.unwrap_or("")
.to_ascii_lowercase();
if recv.contains("halt") || recv.contains("cancel") {
hits.push((s, i));
}
}
}
}
check("raw halt flag access outside halt.rs", hits, ALLOW);
}
/// §5.8 "A `JoinHandle::is_finished` spin ban (use `join_within`)."
#[test]
fn no_is_finished_spins_outside_halt() {
let srcs = production();
let mut hits = Vec::new();
for s in srcs.iter().filter(|s| not_halt(s)) {
hits.extend(s.hits("is_finished").into_iter().map(|i| (s, i)));
}
check("is_finished (use halt::join_within)", hits, &[]);
}
// The `#[allow]` / `#[expect]` sites present when ST-X2 landed, per (file, attribute):
// each is a §5.8 violation awaiting a per-site fix. The list only shrinks.
const ALLOW_BASELINE: &[(&str, &str, usize)] = &[
("src/aacs/provider.rs", r#"allow(dead_code)"#, 1),
(
"src/disc/extract.rs",
r#"allow(clippy::too_many_arguments)"#,
1,
),
(
"src/disc/extract.rs",
r#"allow(clippy::unnecessary_cast,clippy::useless_conversion)"#,
1,
),
("src/drive/linux.rs", r#"allow(dead_code)"#, 1),
("src/drive/mod.rs", r#"allow(dead_code)"#, 1),
(
"src/drive/mod.rs",
r#"cfg_attr(not(target_os="linux"),allow(unused_variables))"#,
1,
),
("src/io/sink/mod.rs", r#"allow(dead_code)"#, 1),
(
"src/keys/arrival.rs",
r#"allow(clippy::too_many_arguments)"#,
1,
),
("src/labels/class_reader.rs", r#"allow(dead_code)"#, 2),
(
"src/labels/ctrm.rs",
r#"allow(clippy::items_after_test_module)"#,
1,
),
("src/mpls.rs", r#"allow(dead_code)"#, 2),
("src/mux/demux_sink.rs", r#"allow(dead_code)"#, 2),
("src/mux/demux_thread.rs", r#"allow(dead_code)"#, 1),
("src/mux/fmp4/mod.rs", r#"allow(dead_code)"#, 1),
("src/mux/hevc/mod.rs", r#"allow(dead_code)"#, 1),
("src/mux/m2ts_mux/mod.rs", r#"allow(dead_code)"#, 1),
("src/mux/mkv.rs", r#"allow(clippy::too_many_arguments)"#, 3),
("src/mux/mod.rs", r#"allow(dead_code)"#, 6),
("src/mux/pipelined_stream.rs", r#"allow(dead_code)"#, 1),
(
"src/mux/resolve.rs",
r#"allow(clippy::too_many_arguments)"#,
2,
),
(
"src/platform/fs_type/linux.rs",
r#"allow(clippy::unnecessary_cast)"#,
3,
),
("src/platform/fs_type/mod.rs", r#"allow(dead_code)"#, 1),
("src/scsi/linux.rs", r#"allow(non_camel_case_types)"#, 1),
("src/scsi/mod.rs", r#"allow(dead_code)"#, 1),
(
"src/scsi/mod.rs",
r#"cfg_attr(not(any(feature="rip",target_os="windows")),allow(dead_code))"#,
1,
),
(
"src/scsi/mod.rs",
r#"cfg_attr(not(any(target_os="linux",target_os="windows")),allow(dead_code))"#,
1,
),
(
"src/scsi/mod.rs",
r#"cfg_attr(not(feature="rip"),allow(dead_code))"#,
1,
),
(
"src/scsi/mod.rs",
r#"cfg_attr(not(target_os="linux"),allow(dead_code))"#,
5,
),
(
"src/scsi/mod.rs",
r#"cfg_attr(not(target_os="windows"),allow(dead_code))"#,
2,
),
(
"src/scsi/mod.rs",
r#"cfg_attr(target_os="macos",allow(dead_code))"#,
1,
),
("src/scsi/windows.rs", r#"allow(non_snake_case)"#, 3),
("src/udf.rs", r#"allow(clippy::only_used_in_recursion)"#, 1),
("src/udf.rs", r#"allow(clippy::too_many_arguments)"#, 1),
];
/// §5.8 "`#[allow]` only in `halt.rs` and tests." `#[expect]` and `cfg_attr(.., allow)`
/// suppress the same way, so they count too.
#[test]
fn allow_attributes_only_in_halt_and_tests() {
let srcs = production();
let mut found: BTreeMap<(String, String), usize> = BTreeMap::new();
for s in srcs.iter().filter(|s| not_halt(s)) {
for (start, end, _) in attrs(s.code.as_bytes(), &s.raw) {
let inner: String = s.raw[start..end].split_whitespace().collect();
let body = inner.trim_start_matches('#').trim_start_matches('!');
let body = &body[1..body.len() - 1];
let lint = body.starts_with("allow(")
|| body.starts_with("expect(")
|| body.starts_with("cfg_attr(")
&& (body.contains(",allow(") || body.contains(",expect("));
if lint {
*found.entry((s.path.clone(), body.to_string())).or_default() += 1;
}
}
}
let base: BTreeMap<(String, String), usize> = ALLOW_BASELINE
.iter()
.map(|(p, a, n)| ((p.to_string(), a.to_string()), *n))
.collect();
let new: Vec<_> = found
.iter()
.filter(|(k, n)| base.get(*k).is_none_or(|b| *n > b))
.map(|((p, a), n)| format!("(\"{p}\", r#\"{a}\"#, {n}),"))
.collect();
let gone: Vec<_> = base
.iter()
.filter(|(k, n)| found.get(*k).is_none_or(|f| f < n))
.map(|((p, a), n)| format!("{p} {a} x{n}"))
.collect();
assert!(
new.is_empty() && gone.is_empty(),
"#[allow] outside halt.rs and tests:\n {}\nfixed (shrink ALLOW_BASELINE):\n {}",
new.join("\n "),
gone.join("\n ")
);
}
// The scanner itself: masking, cfg evaluation and item blanking.
#[test]
fn scanner_sees_production_only() {
assert!(test_only("test"));
assert!(test_only("all(test, not(loom))"));
assert!(test_only("any(test, feature = \"test-util\")"));
assert!(!test_only("any(target_os = \"linux\", test)"));
assert!(!test_only("not(test)"));
let raw = "fn a() { std::thread::sleep(d); } // thread::sleep\n\
const S: &str = \"thread::sleep\"; const C: char = '{';\n\
#[cfg(all(test, unix))]\nmod tests { fn b() { thread::sleep(d); } }\n\
#[cfg(test)]\nuse std::thread::sleep;\nfn c<'a>(x: &'a u8) {}\n\
#[cfg(test)]\nimpl<A, B> T for S<A, B> { fn d() { thread::sleep(d); } }\n\
struct F { #[cfg(test)] g: Vec<u8>, h: u8 }\n";
let mut code = mask(raw);
drop_test_items(raw, &mut code).unwrap();
let code = String::from_utf8(code).unwrap();
assert_eq!(code.matches("thread::sleep").count(), 1, "{code}");
assert!(!code.contains("sleep;") && code.contains("fn c<'a>"));
assert!(!code.contains(" g:") && code.contains(" h: u8 }"), "{code}");
let spans = fn_spans(code.as_bytes());
assert_eq!(
spans.iter().map(|s| s.2.as_str()).collect::<Vec<_>>(),
["a", "c"]
);
let all = production();
for must in [
"src/drive/mod.rs",
"src/scsi/macos.rs",
"src/scsi/windows.rs",
"src/halt.rs",
] {
assert!(all.iter().any(|s| s.path == must), "walk lost {must}");
}
for never in [
"src/drive/stop_tests.rs",
"src/halt/tests.rs",
"src/test_util_tests.rs",
"src/whole_disc_tests.rs",
"src/test_util.rs",
] {
assert!(!all.iter().any(|s| s.path == never), "walk took {never}");
}
}