commit 9b3b3529ba5fbcf22784b7c6766fcaf7da5fe697
parent 8f71b2b5582a9defbd03a713fc8b3561db9c7b68
Author: triesap <tyson@radroots.org>
Date: Wed, 1 Jul 2026 09:37:49 +0000
cli: harden source guard classification
- replace fail-open cfg-test stripping with fail-closed source classification
- mask comments and Rust literals before removed-surface and alias scans
- cover cfg-test modules, functions, fragments, malformed source, and literal false positives
- keep migrated workflow guard diagnostics line-aware for removed SDK surfaces
Diffstat:
| M | src/runtime/sdk.rs | | | 396 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- |
1 file changed, 294 insertions(+), 102 deletions(-)
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -1334,14 +1334,20 @@ mod tests {
.flat_map(|file| {
let source = fs::read_to_string(file).expect("read cli source");
let relative_path = relative_source_path(manifest_dir, file.as_path());
- let production_source = production_source_without_tests(&source);
- let mut findings =
- removed_sdk_status_surface_findings(&relative_path, production_source.as_str());
- findings.extend(root_trade_alias_findings(
- &relative_path,
- production_source.as_str(),
- ));
- findings
+ match production_source_without_tests(&relative_path, &source) {
+ Ok(production_source) => {
+ let mut findings = removed_sdk_status_surface_findings(
+ &relative_path,
+ production_source.as_str(),
+ );
+ findings.extend(root_trade_alias_findings(
+ &relative_path,
+ production_source.as_str(),
+ ));
+ findings
+ }
+ Err(error) => vec![error],
+ }
})
.collect::<Vec<_>>();
@@ -1369,7 +1375,8 @@ mod tests {
);
for source in [inline, multiline] {
- let production_source = production_source_without_tests(source);
+ let production_source =
+ production_source_without_tests("fixture.rs", source).expect("production source");
assert!(
removed_sdk_status_surface_findings("fixture.rs", production_source.as_str())
.is_empty()
@@ -1379,6 +1386,84 @@ mod tests {
}
#[test]
+ fn cli_production_source_scanner_strips_cfg_test_functions() {
+ let source = concat!(
+ "fn production() {}\n",
+ "#[cfg(test)]\n",
+ "fn test_only() { let _ = TradeValidationClient; }\n",
+ "fn after_tests() {}\n",
+ );
+ let production_source =
+ production_source_without_tests("fixture.rs", source).expect("production source");
+
+ assert!(!production_source.contains("TradeValidationClient"));
+ assert!(production_source.contains("fn production()"));
+ assert!(production_source.contains("fn after_tests()"));
+ assert!(
+ removed_sdk_status_surface_findings("fixture.rs", production_source.as_str())
+ .is_empty()
+ );
+ }
+
+ #[test]
+ fn cli_production_source_scanner_strips_cfg_test_fragments() {
+ let source = concat!(
+ "enum Provider {",
+ "#[cfg(test)] TestOnly,",
+ "Production",
+ "}\n",
+ "fn production(provider: Provider) { match provider {",
+ "#[cfg(test)] Provider::TestOnly => sdk.status_client(),",
+ "Provider::Production => {}",
+ "} }\n",
+ );
+ let production_source =
+ production_source_without_tests("fixture.rs", source).expect("production source");
+
+ assert!(!production_source.contains("TestOnly"));
+ assert!(
+ removed_sdk_status_surface_findings("fixture.rs", production_source.as_str())
+ .is_empty()
+ );
+ }
+
+ #[test]
+ fn cli_production_source_scanner_reports_malformed_cfg_test_items() {
+ let source = concat!(
+ "fn production() {}\n",
+ "#[cfg(test)]\n",
+ "mod tests { fn hidden() { let _ = TradeValidationClient; }\n",
+ "fn after_tests() { sdk.status_client(); }\n",
+ );
+ let error =
+ production_source_without_tests("fixture.rs", source).expect_err("classification");
+
+ assert!(error.contains("fixture.rs:2"));
+ assert!(error.contains("cfg(test) item is not closed"));
+ }
+
+ #[test]
+ fn removed_surface_scanner_ignores_comments_and_literals() {
+ let source = concat!(
+ "fn production() {\n",
+ " let literal = \"status_client(\";\n",
+ " let raw = r#\"RadrootsClient::trade_resync(&sdk)\"#;\n",
+ " let character = 'x';\n",
+ "}\n",
+ "// TradeValidationClient::new(root)\n",
+ "/* sdk.trade_status(request) */\n",
+ );
+ let production_source =
+ production_source_without_tests("fixture.rs", source).expect("production source");
+
+ assert!(
+ removed_sdk_status_surface_findings("fixture.rs", production_source.as_str())
+ .is_empty()
+ );
+ assert!(root_trade_alias_findings("fixture.rs", production_source.as_str()).is_empty());
+ }
+
+ #[test]
fn repo_wide_removed_surface_scanner_reports_production_violations() {
let source = "fn production() { sdk.status_client(); RadrootsClient::trade_resync(&sdk); }";
let status_findings = removed_sdk_status_surface_findings("fixture.rs", source);
@@ -1482,21 +1567,24 @@ mod tests {
}
fn assert_migrated_path(label: &str, source: &str, required_tokens: &[&str]) {
+ let source =
+ rust_code_without_non_code(label, source).expect("migrated path source classification");
+
for token in required_tokens {
assert!(
- source.contains(token),
+ source.as_str().contains(token),
"{label} does not contain required SDK token `{token}`"
);
}
for token in MIGRATED_PATH_DISALLOWED_TOKENS {
assert!(
- !source.contains(token),
+ !source.as_str().contains(token),
"{label} contains disallowed migrated-path token `{token}`"
);
}
- let findings = root_trade_alias_findings(label, source);
+ let findings = root_trade_alias_findings(label, source.as_str());
assert!(
findings.is_empty(),
"{label} contains removed SDK root trade aliases:\n{}",
@@ -1554,28 +1642,95 @@ mod tests {
.collect()
}
- fn production_source_without_tests(source: &str) -> String {
- let mut production_source = String::with_capacity(source.len());
+ fn production_source_without_tests(path: &str, source: &str) -> Result<String, String> {
+ let code_source = rust_code_without_non_code(path, source)?;
+ let mut production_source = String::with_capacity(code_source.len());
let mut cursor = 0;
- while let Some((attribute_start, attribute_end)) = find_cfg_test_attribute(source, cursor) {
- let Some(module_end) = find_cfg_test_module_end(source, attribute_end) else {
- production_source.push_str(&source[cursor..attribute_end]);
- cursor = attribute_end;
- continue;
- };
+ while let Some((attribute_start, attribute_end)) =
+ find_cfg_test_attribute(code_source.as_str(), cursor)
+ {
+ let item_end =
+ find_cfg_test_item_end(path, code_source.as_str(), attribute_start, attribute_end)?;
+
+ production_source.push_str(&code_source[cursor..attribute_start]);
+ push_masked_source(
+ &mut production_source,
+ &code_source[attribute_start..item_end],
+ );
+ cursor = item_end;
+ }
+
+ production_source.push_str(&code_source[cursor..]);
+ Ok(production_source)
+ }
- production_source.push_str(&source[cursor..attribute_start]);
- for character in source[attribute_start..module_end].chars() {
- if character == '\n' {
- production_source.push('\n');
+ fn rust_code_without_non_code(path: &str, source: &str) -> Result<String, String> {
+ let bytes = source.as_bytes();
+ let mut code = String::with_capacity(source.len());
+ let mut cursor = 0;
+
+ while cursor < bytes.len() {
+ match bytes[cursor] {
+ b'"' => {
+ let end = skip_quoted_rust_literal(source, cursor, b'"').ok_or_else(|| {
+ classification_error(path, source, cursor, "unterminated string literal")
+ })?;
+ push_masked_source(&mut code, &source[cursor..end]);
+ cursor = end;
+ }
+ b'\'' => {
+ if let Some(end) = skip_rust_char_literal(source, cursor) {
+ push_masked_source(&mut code, &source[cursor..end]);
+ cursor = end;
+ } else {
+ let character = source[cursor..].chars().next().expect("quote");
+ code.push(character);
+ cursor += character.len_utf8();
+ }
+ }
+ b'/' if bytes.get(cursor + 1) == Some(&b'/') => {
+ let end = source[cursor..]
+ .find('\n')
+ .map_or(source.len(), |newline| cursor + newline);
+ push_masked_source(&mut code, &source[cursor..end]);
+ cursor = end;
+ }
+ b'/' if bytes.get(cursor + 1) == Some(&b'*') => {
+ let end = skip_rust_block_comment(source, cursor).ok_or_else(|| {
+ classification_error(path, source, cursor, "unterminated block comment")
+ })?;
+ push_masked_source(&mut code, &source[cursor..end]);
+ cursor = end;
+ }
+ b'r' => {
+ if let Some(end) = skip_raw_rust_string(source, cursor) {
+ push_masked_source(&mut code, &source[cursor..end]);
+ cursor = end;
+ } else {
+ code.push('r');
+ cursor += 1;
+ }
+ }
+ _ => {
+ let character = source[cursor..].chars().next().expect("source character");
+ code.push(character);
+ cursor += character.len_utf8();
}
}
- cursor = module_end;
}
- production_source.push_str(&source[cursor..]);
- production_source
+ Ok(code)
+ }
+
+ fn push_masked_source(output: &mut String, source: &str) {
+ for character in source.chars() {
+ if character == '\n' {
+ output.push('\n');
+ } else {
+ output.push(' ');
+ }
+ }
}
fn find_cfg_test_attribute(source: &str, start: usize) -> Option<(usize, usize)> {
@@ -1599,42 +1754,100 @@ mod tests {
None
}
- fn find_cfg_test_module_end(source: &str, attribute_end: usize) -> Option<usize> {
+ fn find_cfg_test_item_end(
+ path: &str,
+ source: &str,
+ attribute_start: usize,
+ attribute_end: usize,
+ ) -> Result<usize, String> {
let mut cursor = skip_rust_whitespace(source, attribute_end);
while source[cursor..].starts_with("#[") {
- let attribute_end = source[cursor..].find(']').map(|end| cursor + end + 1)?;
+ let attribute_end = source[cursor..]
+ .find(']')
+ .map(|end| cursor + end + 1)
+ .ok_or_else(|| {
+ classification_error(path, source, cursor, "unterminated attribute")
+ })?;
cursor = skip_rust_whitespace(source, attribute_end);
}
- cursor = skip_optional_visibility(source, cursor);
- if !starts_with_rust_keyword(source, cursor, "mod") {
- return None;
- }
- cursor = skip_rust_whitespace(source, cursor + "mod".len());
- cursor = skip_rust_identifier(source, cursor)?;
- cursor = skip_rust_whitespace(source, cursor);
-
- if source[cursor..].starts_with(';') {
- return Some(cursor + 1);
- }
- if !source[cursor..].starts_with('{') {
- return None;
- }
-
- find_matching_rust_brace(source, cursor).or(Some(source.len()))
+ cursor = skip_optional_visibility(path, source, cursor)?;
+ find_rust_item_end(path, source, attribute_start, cursor)
}
- fn skip_optional_visibility(source: &str, cursor: usize) -> usize {
+ fn skip_optional_visibility(path: &str, source: &str, cursor: usize) -> Result<usize, String> {
if !starts_with_rust_keyword(source, cursor, "pub") {
- return cursor;
+ return Ok(cursor);
}
let mut cursor = skip_rust_whitespace(source, cursor + "pub".len());
if source[cursor..].starts_with('(') {
- cursor = skip_balanced_parentheses(source, cursor).unwrap_or(cursor);
+ cursor = skip_balanced_parentheses(source, cursor).ok_or_else(|| {
+ classification_error(path, source, cursor, "malformed visibility")
+ })?;
cursor = skip_rust_whitespace(source, cursor);
}
- cursor
+ Ok(cursor)
+ }
+
+ fn find_rust_item_end(
+ path: &str,
+ source: &str,
+ attribute_start: usize,
+ item_start: usize,
+ ) -> Result<usize, String> {
+ let bytes = source.as_bytes();
+ let mut cursor = item_start;
+ let mut brace_depth = 0usize;
+ let mut bracket_depth = 0usize;
+ let mut paren_depth = 0usize;
+ let mut saw_brace = false;
+
+ while cursor < bytes.len() {
+ match bytes[cursor] {
+ b'(' => paren_depth += 1,
+ b')' => {
+ paren_depth = paren_depth.checked_sub(1).ok_or_else(|| {
+ classification_error(path, source, cursor, "unbalanced closing parenthesis")
+ })?;
+ }
+ b'[' => bracket_depth += 1,
+ b']' => {
+ bracket_depth = bracket_depth.checked_sub(1).ok_or_else(|| {
+ classification_error(path, source, cursor, "unbalanced closing bracket")
+ })?;
+ }
+ b'{' if paren_depth == 0 && bracket_depth == 0 => {
+ brace_depth += 1;
+ saw_brace = true;
+ }
+ b'}' if paren_depth == 0 && bracket_depth == 0 => {
+ brace_depth = brace_depth.checked_sub(1).ok_or_else(|| {
+ classification_error(path, source, cursor, "unbalanced closing brace")
+ })?;
+ cursor += 1;
+ if saw_brace && brace_depth == 0 {
+ return Ok(cursor);
+ }
+ continue;
+ }
+ b';' if paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 => {
+ return Ok(cursor + 1);
+ }
+ b',' if paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 => {
+ return Ok(cursor + 1);
+ }
+ _ => {}
+ }
+ cursor += 1;
+ }
+
+ Err(classification_error(
+ path,
+ source,
+ attribute_start,
+ "cfg(test) item is not closed",
+ ))
}
fn skip_balanced_parentheses(source: &str, open_index: usize) -> Option<usize> {
@@ -1667,23 +1880,6 @@ mod tests {
cursor
}
- fn skip_rust_identifier(source: &str, cursor: usize) -> Option<usize> {
- let mut end = cursor;
- let mut chars = source[cursor..].char_indices();
- let (_, first) = chars.next()?;
- if first != '_' && !first.is_ascii_alphabetic() {
- return None;
- }
- end += first.len_utf8();
- for (relative_index, character) in chars {
- if character != '_' && !character.is_ascii_alphanumeric() {
- return Some(cursor + relative_index);
- }
- end = cursor + relative_index + character.len_utf8();
- }
- Some(end)
- }
-
fn starts_with_rust_keyword(source: &str, cursor: usize, keyword: &str) -> bool {
source[cursor..].starts_with(keyword)
&& source[cursor + keyword.len()..]
@@ -1692,47 +1888,26 @@ mod tests {
.is_none_or(|character| !is_rust_identifier_character(character))
}
- fn find_matching_rust_brace(source: &str, open_index: usize) -> Option<usize> {
+ fn skip_rust_block_comment(source: &str, start: usize) -> Option<usize> {
let bytes = source.as_bytes();
- let mut cursor = open_index;
+ let mut cursor = start;
let mut depth = 0usize;
- while cursor < bytes.len() {
- match bytes[cursor] {
- b'{' => {
- depth += 1;
- cursor += 1;
- }
- b'}' => {
- depth = depth.checked_sub(1)?;
- cursor += 1;
- if depth == 0 {
- return Some(cursor);
- }
- }
- b'"' => cursor = skip_quoted_rust_literal(source, cursor, b'"')?,
- b'\''
- if bytes
- .get(cursor + 1)
- .is_none_or(|byte| !byte.is_ascii_alphabetic() && *byte != b'_') =>
- {
- cursor = skip_quoted_rust_literal(source, cursor, b'\'')?
- }
- b'/' if bytes.get(cursor + 1) == Some(&b'/') => {
- cursor = source[cursor..]
- .find('\n')
- .map_or(source.len(), |newline| cursor + newline + 1);
- }
- b'/' if bytes.get(cursor + 1) == Some(&b'*') => {
- cursor = source[cursor + 2..]
- .find("*/")
- .map(|end| cursor + 2 + end + 2)?;
- }
- b'r' => {
- cursor = skip_raw_rust_string(source, cursor).unwrap_or(cursor + 1);
+ while cursor + 1 < bytes.len() {
+ if bytes[cursor] == b'/' && bytes[cursor + 1] == b'*' {
+ depth += 1;
+ cursor += 2;
+ continue;
+ }
+ if bytes[cursor] == b'*' && bytes[cursor + 1] == b'/' {
+ depth = depth.checked_sub(1)?;
+ cursor += 2;
+ if depth == 0 {
+ return Some(cursor);
}
- _ => cursor += 1,
+ continue;
}
+ cursor += 1;
}
None
@@ -1756,6 +1931,16 @@ mod tests {
None
}
+ fn skip_rust_char_literal(source: &str, start: usize) -> Option<usize> {
+ let end = skip_quoted_rust_literal(source, start, b'\'')?;
+ let literal = &source[start + 1..end - 1];
+ if literal.starts_with('\\') || literal.chars().count() == 1 {
+ Some(end)
+ } else {
+ None
+ }
+ }
+
fn skip_raw_rust_string(source: &str, start: usize) -> Option<usize> {
let bytes = source.as_bytes();
let mut cursor = start + 1;
@@ -1802,6 +1987,13 @@ mod tests {
+ 1
}
+ fn classification_error(path: &str, source: &str, index: usize, reason: &str) -> String {
+ format!(
+ "{path}:{} source classification failed: {reason}",
+ line_number(source, index)
+ )
+ }
+
fn sample_config(root: &Path, relays: Vec<String>) -> RuntimeConfig {
let data = root.join("data");
let cache = root.join("cache");