cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

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:
Msrc/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");