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..4ae0b5ac 100644 --- a/crates/digstore-cli/src/lib.rs +++ b/crates/digstore-cli/src/lib.rs @@ -4,6 +4,7 @@ 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; 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;