mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
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.
This commit is contained in:
@@ -0,0 +1,692 @@
|
||||
//! 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}");
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,9 @@ SS-4 The TEST UNIT READY command (see table 202) provides a means to check if th
|
||||
SS-5 The Drive maintains two separate Prevent states: Prevent and Persistent Prevent as described in 4.1.7. The Persistent and Prevent bits are used to independently select values for these states. See Table 329. Table 329 — State Selection Persistent Prevent Meaning 0 0 Prevent State shall be cleared (Unlocked) 0 1 Prevent State shall be set (Locked) 1 0 Persistent Prevent State shall be cleared (Persistent Allow) 1 1 Persistent Prevent State shall be set (Persistent Prevent)
|
||||
SS-6 When Power Conditions field is zero and FL is zero, the meanings of LoEj and Start are defined in Table 633. Table 633 — LoEj and Start Meanings when Power Conditions = 0 LoEj Start Operation 0 0 Stop the disc 0 1 Start the disc and make ready for access 1 0 Eject the disc if permitted. It is not an error If no media is present. See 6.13, PREVENT ALLOW MEDIUM REMOVAL Command 1 1 Load the disc, Start the disc and make ready for access. It is not an error if no media is present.
|
||||
SS-7 static int _mmc_report_key(MMC *mmc, uint8_t agid, uint32_t addr, uint8_t blocks, uint8_t format, uint8_t *buf, uint16_t len) { uint8_t cmd[MMC_CDB_SIZE]; memset(cmd, 0, sizeof(cmd)); BD_DEBUG(DBG_MMC, "MMC report key...\n"); cmd[0] = MMC_REPORT_KEY; cmd[2] = (addr >> 24) & 0xff; cmd[3] = (addr >> 16) & 0xff; cmd[4] = (addr >> 8) & 0xff; cmd[5] = addr & 0xff; cmd[6] = blocks; cmd[7] = MMC_KEY_CLASS_AACS; cmd[8] = (len >> 8) & 0xff; cmd[9] = len & 0xff; cmd[10] = (agid << 6) | (format & 0x3f); return device_send_cmd(mmc->dev, cmd, buf, 0, len); } … static int _mmc_invalidate_agid(MMC *mmc, uint8_t agid) { uint8_t buf[2]; memset(buf, 0, sizeof(buf)); return _mmc_report_key(mmc, agid, 0, 0, MMC_REPORT_KEY_AACS_INVALIDATE_AGID, buf, sizeof(buf)); } static void _mmc_invalidate_agids(MMC *mmc) { int agid; /* invalidate all agids */ for (agid = 0; agid < 4; agid++) { _mmc_invalidate_agid(mmc, agid); } } static int _mmc_report_agid(MMC *mmc, uint8_t *agid) { uint8_t buf[8]; memset(buf, 0, sizeof(buf)); int result = _mmc_report_key(mmc, 0, 0, 0, MMC_REPORT_KEY_AACS_AGID, buf, sizeof(buf)); if (result) { *agid = (buf[7] & 0xff) >> 6; } return result; }
|
||||
SS-8 LOCK_EX Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time. LOCK_UN Remove an existing lock held by this process. A call to flock() may block if an incompatible lock is held by another process. To make a nonblocking request, include LOCK_NB (by ORing) with any of the above operations. A single file may not simultaneously have both shared and exclusive locks. Locks created by flock() are associated with an open file description (see open(2)). This means that duplicate file descriptors (created by, for example, fork(2) or dup(2)) refer to the same lock, and this lock may be modified or released using any of these file descriptors. Furthermore, the lock is released either by an explicit LOCK_UN operation on any of these duplicate file descriptors, or when all such file descriptors have been closed. If a process uses open(2) (or similar) to obtain more than one file descriptor for the same file, these file descriptors are treated independently by flock(). An attempt to lock the file using one of these file descriptors may be denied by a lock that the calling process has already placed via another file descriptor. EWOULDBLOCK The file is locked and the LOCK_NB flag was selected. Since Linux 2.6.12, NFS clients support flock() locks by emulating them as fcntl(2) byte-range locks on the entire file. This means that fcntl(2) and flock() locks do interact with one another over NFS. It also means that in order to place an exclusive lock, the file must be opened for writing.
|
||||
SS-9 A file identity is uniquely determined by the combination of st_dev and st_ino. At any given time in a system, distinct files shall have distinct file identities; hard links to the same file shall have the same file identity. Over time, these file identities can be reused for different files.
|
||||
SS-10 LOCKFILE_EXCLUSIVE_LOCK 0x00000002 The function requests an exclusive lock. Otherwise, it requests a shared lock. LOCKFILE_FAIL_IMMEDIATELY 0x00000001 The function returns immediately if it is unable to acquire the requested lock. Otherwise, it waits. FILE_SHARE_DELETE 0x00000004 Enables subsequent open operations on a file or device to request delete access. Otherwise, no process can open the file or device if it requests delete access. The identifier (low and high parts) and the volume serial number uniquely identify a file on a single computer. To determine whether two open handles represent the same file, combine the identifier and the volume serial number for each file and compare them.
|
||||
SS-12 The fsync() function shall request that all data for the open file descriptor named by fildes is to be transferred to the storage device associated with the file described by fildes. The nature of the transfer is implementation-defined. The fsync() function shall not return until the system has completed that action or until an error is detected. The fdatasync() function shall force all currently queued I/O operations associated with the file indicated by file descriptor fildes to the synchronized I/O completion state. The functionality shall be equivalent to fsync() with the symbol _POSIX_SYNCHRONIZED_IO defined, with the exception that all I/O operations shall be completed as defined for synchronized I/O data integrity completion.
|
||||
SS-13 This system call is extremely dangerous and should not be used in portable programs. None of these operations writes out the file's metadata. Therefore, unless the application is strictly performing overwrites of already-instantiated disk blocks, there are no guarantees that the data will be available after a crash. There is no user interface to know if a write is purely an overwrite. On filesystems using copy-on-write semantics (e.g., btrfs) an overwrite of existing allocated blocks is impossible. When writing into preallocated space, many filesystems also require calls into the block allocator, which this system call does not sync out to disk. This system call does not flush disk write caches and thus does not provide any data integrity on systems with volatile disk write caches. Some details SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any I/O errors or ENOSPC conditions and will return these to the caller. Useful combinations of the flags bits are: SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE Ensures that all pages in the specified range which were dirty when sync_file_range() was called are placed under write-out. This is a start-write-for-data-integrity operation. SYNC_FILE_RANGE_WRITE Start write-out of all dirty pages in the specified range which are not presently under write-out. This is an asynchronous flush-to-disk operation. This is not suitable for data integrity operations. SYNC_FILE_RANGE_WAIT_BEFORE (or SYNC_FILE_RANGE_WAIT_AFTER) Wait for completion of write-out of all pages in the specified range. This can be used after an earlier SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE operation to wait for completion of that operation, and obtain its result. SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_AFTER This is a write-for-data-integrity operation that will ensure that all pages in the specified range which were dirty when sync_file_range() was called are committed to disk.
|
||||
SS-14 Does the same thing as fsync(2) then asks the drive to flush all buffered data to the permanent storage device (arg is ignored). As this drains the entire queue of the device and acts as a barrier, data that had been fsync'd on the same device before is guaranteed to be persisted when this call returns. This is currently implemented on HFS, MS-DOS (FAT), Universal Disk Format (UDF) and APFS file systems. The operation may take quite a while to complete. Certain FireWire drives have also been known to ignore the request to flush their buffered data.
|
||||
|
||||
@@ -24,9 +24,13 @@ src/drive/mod.rs lock_tray SS-5
|
||||
src/drive/mod.rs unlock_tray SS-5
|
||||
src/drive/allow.rs is_eject SS-6
|
||||
src/drive/mod.rs eject SS-6
|
||||
src/drive/mod.rs finish_eject SS-6
|
||||
src/drive/mod.rs start_unit SS-6
|
||||
src/drive/mod.rs note_ledger SS-7
|
||||
src/keys/resolve.rs resolve KS-14
|
||||
src/io/artifact_lock.rs acquire_ids SS-8
|
||||
src/io/artifact_lock.rs acquire_ids SS-9
|
||||
src/io/artifact_lock.rs acquire_ids SS-10
|
||||
src/keys/resolve.rs resolve_observed KS-14
|
||||
src/keys/resolve.rs borrow_samples KS-10
|
||||
src/keys/resolve.rs apply_single_unit_rule KS-14
|
||||
src/keys/arrival.rs decrypt_proven KS-5
|
||||
|
||||
Reference in New Issue
Block a user