From 8efb3729971822ef961e84389ccb7dd74ab43be9 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sat, 5 Sep 2026 21:57:47 -0700 Subject: [PATCH 1/3] chore: open lane for #3130 From e72662f5448df5a83accc0735fa3f7f6a4505193 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Mon, 7 Sep 2026 23:17:39 -0700 Subject: [PATCH 2/3] test(hygiene): port the lost-string-continuation guard to all 14 crates (#3130) Ports dig-node's merged reference implementation (continuation_guard.rs, dig-node#526/#583) into every member of the digs workspace, adopting the settled discriminator rather than inventing a second one: a non-comment line carrying a run of 10 or more spaces is a defect, unless immediately followed by `//` or covered by a per-file exclusion. Each copy is byte-identical to the reference apart from its doc comment and a per-crate MIN_FILES_SCANNED floor tuned to that crate's real .rs count, so a walker that stops finding files fails the test instead of silently passing on zero. All three exemption lists are empty in every crate. Covers the 14 published workspace members. The two workspace-excluded crates (digstore-prover/guest, dig-client-wasm) are not built by `cargo test --workspace` and are out of scope. This commit is the guard only; no string literal is changed by it. Refs https://github.com/DIG-Network/dig_ecosystem/issues/3130 --- crates/dig-resolver/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/dig-resolver/src/main.rs | 2 + .../digstore-chain/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-chain/src/lib.rs | 2 + .../src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-chunker/src/lib.rs | 2 + crates/digstore-cli/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-cli/src/lib.rs | 1 + .../src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-compiler/src/lib.rs | 2 + .../digstore-core/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-core/src/lib.rs | 2 + .../digstore-crypto/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-crypto/src/lib.rs | 2 + .../digstore-guest/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-guest/src/lib.rs | 2 + .../digstore-host/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-host/src/lib.rs | 2 + .../digstore-prover/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-prover/src/lib.rs | 2 + .../digstore-remote/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-remote/src/lib.rs | 3 + .../digstore-stage/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-stage/src/lib.rs | 2 + .../digstore-store/src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-store/src/lib.rs | 2 + .../src/continuation_guard.rs | 199 ++++++++++++++++++ crates/digstore-subscription/src/lib.rs | 2 + 28 files changed, 2814 insertions(+) create mode 100644 crates/dig-resolver/src/continuation_guard.rs create mode 100644 crates/digstore-chain/src/continuation_guard.rs create mode 100644 crates/digstore-chunker/src/continuation_guard.rs create mode 100644 crates/digstore-cli/src/continuation_guard.rs create mode 100644 crates/digstore-compiler/src/continuation_guard.rs create mode 100644 crates/digstore-core/src/continuation_guard.rs create mode 100644 crates/digstore-crypto/src/continuation_guard.rs create mode 100644 crates/digstore-guest/src/continuation_guard.rs create mode 100644 crates/digstore-host/src/continuation_guard.rs create mode 100644 crates/digstore-prover/src/continuation_guard.rs create mode 100644 crates/digstore-remote/src/continuation_guard.rs create mode 100644 crates/digstore-stage/src/continuation_guard.rs create mode 100644 crates/digstore-store/src/continuation_guard.rs create mode 100644 crates/digstore-subscription/src/continuation_guard.rs diff --git a/crates/dig-resolver/src/continuation_guard.rs b/crates/dig-resolver/src/continuation_guard.rs new file mode 100644 index 00000000..a8671f87 --- /dev/null +++ b/crates/dig-resolver/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 0; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/dig-resolver/src/main.rs b/crates/dig-resolver/src/main.rs index fd2214c3..f3f52b9f 100644 --- a/crates/dig-resolver/src/main.rs +++ b/crates/dig-resolver/src/main.rs @@ -11,6 +11,8 @@ //! never from the serving node) behind the scenes. Exit codes: 0 = printed the //! root; 1 = resolution failed (see stderr); 2 = bad usage/argument. +mod continuation_guard; + use chia_protocol::Bytes32; use digstore_chain::coinset::Coinset; use digstore_chain::singleton::sync_datastore; diff --git a/crates/digstore-chain/src/continuation_guard.rs b/crates/digstore-chain/src/continuation_guard.rs new file mode 100644 index 00000000..105594c4 --- /dev/null +++ b/crates/digstore-chain/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 15; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-chain/src/lib.rs b/crates/digstore-chain/src/lib.rs index 9d6deed0..0d25ce0c 100644 --- a/crates/digstore-chain/src/lib.rs +++ b/crates/digstore-chain/src/lib.rs @@ -1,5 +1,7 @@ //! Seed management and (later) Chia anchoring for digstore. +mod continuation_guard; + pub mod anchor; pub mod cat; pub mod chip0002; diff --git a/crates/digstore-chunker/src/continuation_guard.rs b/crates/digstore-chunker/src/continuation_guard.rs new file mode 100644 index 00000000..40d6f110 --- /dev/null +++ b/crates/digstore-chunker/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 3; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-chunker/src/lib.rs b/crates/digstore-chunker/src/lib.rs index 36abf94f..c8128dde 100644 --- a/crates/digstore-chunker/src/lib.rs +++ b/crates/digstore-chunker/src/lib.rs @@ -14,6 +14,8 @@ //! `ChunkerConfig::min_size` (no cut below it) and `ChunkerConfig::max_size` //! (forced cut at it). +mod continuation_guard; + mod boundary; mod chunk; mod chunker; diff --git a/crates/digstore-cli/src/continuation_guard.rs b/crates/digstore-cli/src/continuation_guard.rs new file mode 100644 index 00000000..ce34cd34 --- /dev/null +++ b/crates/digstore-cli/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 50; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-cli/src/lib.rs b/crates/digstore-cli/src/lib.rs index 8ff28162..9e26f594 100644 --- a/crates/digstore-cli/src/lib.rs +++ b/crates/digstore-cli/src/lib.rs @@ -1,3 +1,4 @@ +mod continuation_guard; pub mod beacon; pub mod branding; pub mod cli; diff --git a/crates/digstore-compiler/src/continuation_guard.rs b/crates/digstore-compiler/src/continuation_guard.rs new file mode 100644 index 00000000..bc10e4bd --- /dev/null +++ b/crates/digstore-compiler/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 8; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-compiler/src/lib.rs b/crates/digstore-compiler/src/lib.rs index 584c0ba6..d7e78d88 100644 --- a/crates/digstore-compiler/src/lib.rs +++ b/crates/digstore-compiler/src/lib.rs @@ -28,6 +28,8 @@ //! crate reads from the same offset. The compiler's old private `SEG_*` format is //! deleted: core is the single source of truth. +mod continuation_guard; + mod atomic_write; mod chunk_index; mod config; diff --git a/crates/digstore-core/src/continuation_guard.rs b/crates/digstore-core/src/continuation_guard.rs new file mode 100644 index 00000000..105594c4 --- /dev/null +++ b/crates/digstore-core/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 15; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-core/src/lib.rs b/crates/digstore-core/src/lib.rs index a20a9321..6ea9a1b6 100644 --- a/crates/digstore-core/src/lib.rs +++ b/crates/digstore-core/src/lib.rs @@ -2,6 +2,8 @@ extern crate alloc; +mod continuation_guard; + pub mod abi; pub mod bytes; pub mod capsule; diff --git a/crates/digstore-crypto/src/continuation_guard.rs b/crates/digstore-crypto/src/continuation_guard.rs new file mode 100644 index 00000000..40d6f110 --- /dev/null +++ b/crates/digstore-crypto/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 3; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-crypto/src/lib.rs b/crates/digstore-crypto/src/lib.rs index 0d5a27b2..d1b2c50b 100644 --- a/crates/digstore-crypto/src/lib.rs +++ b/crates/digstore-crypto/src/lib.rs @@ -27,6 +27,8 @@ //! `BlsPublicKey`). All public byte material uses canonical `digstore-core` //! types (`Bytes32`/`Bytes48`/`Bytes96`/`SecretSalt`). +mod continuation_guard; + pub mod aead; pub mod bls; pub mod error; diff --git a/crates/digstore-guest/src/continuation_guard.rs b/crates/digstore-guest/src/continuation_guard.rs new file mode 100644 index 00000000..49edfbaa --- /dev/null +++ b/crates/digstore-guest/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 12; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-guest/src/lib.rs b/crates/digstore-guest/src/lib.rs index 62299bc3..54b46ba2 100644 --- a/crates/digstore-guest/src/lib.rs +++ b/crates/digstore-guest/src/lib.rs @@ -24,6 +24,8 @@ extern crate alloc; +mod continuation_guard; + pub mod allocator; pub mod host; diff --git a/crates/digstore-host/src/continuation_guard.rs b/crates/digstore-host/src/continuation_guard.rs new file mode 100644 index 00000000..bc10e4bd --- /dev/null +++ b/crates/digstore-host/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 8; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-host/src/lib.rs b/crates/digstore-host/src/lib.rs index f370bfbd..10d8bd7d 100644 --- a/crates/digstore-host/src/lib.rs +++ b/crates/digstore-host/src/lib.rs @@ -6,6 +6,8 @@ //! swappable TEE-alternative attestation hook (§13.6). The host NEVER decrypts //! or inspects served payloads. +mod continuation_guard; + mod clock; mod config; mod error; diff --git a/crates/digstore-prover/src/continuation_guard.rs b/crates/digstore-prover/src/continuation_guard.rs new file mode 100644 index 00000000..3ee927d7 --- /dev/null +++ b/crates/digstore-prover/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 7; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-prover/src/lib.rs b/crates/digstore-prover/src/lib.rs index f690d5b7..8f599bc7 100644 --- a/crates/digstore-prover/src/lib.rs +++ b/crates/digstore-prover/src/lib.rs @@ -8,6 +8,8 @@ //! default backend so the rest of the system is fully functional while the //! real risc0 circuit matures. +mod continuation_guard; + pub mod chain; pub mod coinset; pub mod commitment; diff --git a/crates/digstore-remote/src/continuation_guard.rs b/crates/digstore-remote/src/continuation_guard.rs new file mode 100644 index 00000000..105594c4 --- /dev/null +++ b/crates/digstore-remote/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 15; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-remote/src/lib.rs b/crates/digstore-remote/src/lib.rs index 85e9aa50..50ff86c2 100644 --- a/crates/digstore-remote/src/lib.rs +++ b/crates/digstore-remote/src/lib.rs @@ -11,6 +11,9 @@ //! `digstore_crypto::{push_signing_message, verify_push}` with argument order //! `(root, store_id)` (message = `SHA-256(root || store_id)`), the single source //! of truth shared with `digstore-cli`. + +mod continuation_guard; + pub mod auth; pub mod backend; pub mod backend_inmem; diff --git a/crates/digstore-stage/src/continuation_guard.rs b/crates/digstore-stage/src/continuation_guard.rs new file mode 100644 index 00000000..a8671f87 --- /dev/null +++ b/crates/digstore-stage/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 0; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-stage/src/lib.rs b/crates/digstore-stage/src/lib.rs index 0356682b..37220b8d 100644 --- a/crates/digstore-stage/src/lib.rs +++ b/crates/digstore-stage/src/lib.rs @@ -28,6 +28,8 @@ //! §21 push are the wallet method + remote push respectively — Pass C is the //! staging/compile half. +mod continuation_guard; + use std::path::{Path, PathBuf}; use digstore_chunker::{chunk_slice, Chunk}; diff --git a/crates/digstore-store/src/continuation_guard.rs b/crates/digstore-store/src/continuation_guard.rs new file mode 100644 index 00000000..bc10e4bd --- /dev/null +++ b/crates/digstore-store/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 8; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-store/src/lib.rs b/crates/digstore-store/src/lib.rs index 9bd60dd5..c29dbf26 100644 --- a/crates/digstore-store/src/lib.rs +++ b/crates/digstore-store/src/lib.rs @@ -5,6 +5,8 @@ //! produced here are consumed by `digstore-compiler` (which owns §8.3 pool //! ordering and §19.3 byte-identical compilation) and `digstore-guest`. +mod continuation_guard; + mod chunkstore; mod clock; mod config; diff --git a/crates/digstore-subscription/src/continuation_guard.rs b/crates/digstore-subscription/src/continuation_guard.rs new file mode 100644 index 00000000..707e5f84 --- /dev/null +++ b/crates/digstore-subscription/src/continuation_guard.rs @@ -0,0 +1,199 @@ +//! Test-only guard against the "lost string continuation" defect class +//! (dig_ecosystem#3130; ported from dig-node's `continuation_guard.rs`, dig-node#526/#583). +//! +//! A Rust string literal continued with a trailing `\` renders correctly. When that +//! backslash is lost -- `cargo fmt` rejoining a wrapped literal, or a mechanical regex +//! repair -- the literal keeps the SOURCE's leading indentation, so the emitted text +//! carries a multi-space run in the middle of a sentence. It compiles, every other test +//! stays green, and the mangled and correct forms are indistinguishable in a normal +//! diff. The only witness is a person reading the emitted text -- so this scanner reads +//! it instead, on every build. +#![cfg(test)] + +use std::path::Path; + +/// No directory under `src` is exempt from the crate-wide scan. +const EXCLUDED_DIRS: &[&str] = &[]; + +/// No fixture in this crate pins byte-identical captured external output that needs a +/// line-range carve-out. Add an entry here (file name, start line, end line) if one is +/// found, with a comment naming what the fixture pins and why its alignment is real. +const EXCLUDED_LINE_RANGES: &[(&str, u32, u32)] = &[]; + +/// No file in this crate has adopted the "hand-aligned `\n`-joined banner" idiom. Once a +/// line has committed to that idiom -- it contains a literal `\n` escape anywhere -- +/// every space run on it is column alignment, not a torn sentence, so the whole line +/// would be exempt. +const CLI_COLUMN_FILES: &[&str] = &[]; + +/// A lost continuation always leaves the source's own indentation as a mid-sentence +/// space run; ordinary column-alignment padding never exceeds 8. Ten leaves margin on +/// both sides: comfortably above every legitimate pad, comfortably below the smallest +/// real defect. Shared across every crate that carries this guard -- do not invent a +/// second discriminator (dig_ecosystem#3130). +const MIN_DEFECT_RUN: usize = 10; + +/// One offending run found by the scan. +struct Offense { + file: String, + line: u32, + fragment: String, +} + +fn is_excluded_line(file_name: &str, line_no: u32) -> bool { + EXCLUDED_LINE_RANGES + .iter() + .any(|(f, start, end)| *f == file_name && line_no >= *start && line_no <= *end) +} + +fn is_excluded_dir(rel_path: &Path) -> bool { + rel_path + .components() + .any(|c| EXCLUDED_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())) +} + +/// Walks every `.rs` file under `src`, returning `(files_scanned, offenses)`. +fn scan_source_tree() -> (usize, Vec) { + let src_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files_scanned = 0usize; + let mut offenses = Vec::new(); + + let mut stack = vec![src_root.clone()]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let path = entry.path(); + let rel = path.strip_prefix(&src_root).unwrap_or(&path); + if path.is_dir() { + if is_excluded_dir(rel) { + continue; + } + stack.push(path); + continue; + } + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + if is_excluded_dir(rel) { + continue; + } + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + files_scanned += 1; + + for (idx, raw_line) in contents.lines().enumerate() { + let line_no = (idx + 1) as u32; + if is_excluded_line(&file_name, line_no) { + continue; + } + + // Leading indentation is source layout, not literal content -- ignore it. + let trimmed_start = raw_line.trim_start(); + if trimmed_start.is_empty() { + continue; + } + + // A comment line is never scanned, structurally -- rewording a comment + // (including this guard's own prose) must never dodge the check by + // reformatting it as non-comment text; it stays excluded because it + // starts with `//`, not because of what it says. + if trimmed_start.starts_with("//") { + continue; + } + + // Control characters (excluding the line's own trailing newline, which + // `.lines()` already stripped) are always a defect signature. + if let Some(pos) = trimmed_start.char_indices().find(|(_, c)| c.is_control()) { + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: format!("", pos.0), + }); + continue; + } + + // A line in one of the CLI banner files that carries a literal `\n` + // escape anywhere is deliberate column layout end to end -- see + // CLI_COLUMN_FILES above. + if CLI_COLUMN_FILES.contains(&file_name.as_str()) && trimmed_start.contains(r"\n") { + continue; + } + + // Find every run of 2+ spaces; only a run at or above MIN_DEFECT_RUN is + // a candidate, and only once it clears the trailing-comment check below. + let bytes = trimmed_start.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] != b' ' { + i += 1; + continue; + } + let run_start = i; + while i < bytes.len() && bytes[i] == b' ' { + i += 1; + } + let run_len = i - run_start; + if run_len < MIN_DEFECT_RUN { + continue; + } + + // A run immediately followed by `//` is aligning a TRAILING + // COMMENT to a fixed column -- structurally never inside a string + // literal's body. + if trimmed_start[i..].starts_with("//") { + continue; + } + + offenses.push(Offense { + file: file_name.clone(), + line: line_no, + fragment: trimmed_start.to_string(), + }); + break; + } + } + } + } + + (files_scanned, offenses) +} + +/// This crate has a couple dozen source files; a scan that reads too few (a wrong +/// `CARGO_MANIFEST_DIR`, a moved `src/`, a walk that silently matched nothing) is a +/// broken guard, not a passing one, and must FAIL rather than vacuously succeed. +const MIN_FILES_SCANNED: usize = 4; + +#[test] +fn no_lost_string_continuation_leaves_a_multi_space_run_mid_sentence() { + let (files_scanned, offenses) = scan_source_tree(); + + assert!( + files_scanned > MIN_FILES_SCANNED, + "scanned {files_scanned} files, expected more than {MIN_FILES_SCANNED} -- a guard \ + that reads zero (or too few) files is not scanning the crate, and a scan that \ + reads nothing must fail rather than pass vacuously" + ); + + if !offenses.is_empty() { + let report: Vec = offenses + .iter() + .map(|o| format!(" {}:{} -> {:?}", o.file, o.line, o.fragment)) + .collect(); + panic!( + "found {} site(s) with a lost string continuation (a run of {}+ spaces mid-line, \ + outside a comment/fixture/CLI-column exemption):\n{}", + offenses.len(), + MIN_DEFECT_RUN, + report.join("\n") + ); + } +} diff --git a/crates/digstore-subscription/src/lib.rs b/crates/digstore-subscription/src/lib.rs index 9cf94c9a..90f3d5fe 100644 --- a/crates/digstore-subscription/src/lib.rs +++ b/crates/digstore-subscription/src/lib.rs @@ -31,6 +31,8 @@ #![forbid(unsafe_code)] +mod continuation_guard; + pub mod decide; pub mod lineage; pub mod reconcile; From 66c8701e34980e95e22fd0dafe61a05f98d5b2c2 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Tue, 8 Sep 2026 04:21:29 -0700 Subject: [PATCH 3/3] style: cargo fmt -- sort continuation_guard mod declaration (#3130) --- crates/digstore-cli/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/digstore-cli/src/lib.rs b/crates/digstore-cli/src/lib.rs index 9e26f594..4ae0b5ac 100644 --- a/crates/digstore-cli/src/lib.rs +++ b/crates/digstore-cli/src/lib.rs @@ -1,10 +1,10 @@ -mod continuation_guard; pub mod beacon; pub mod branding; pub mod cli; pub mod commands; pub mod config; pub mod context; +mod continuation_guard; pub mod dig_toml; pub mod error; pub mod ops;