use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::Path;
const MAX_PROXIMITY: usize = 5;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Suppression {
pub rule_id: String,
pub hash: String,
pub justification: String,
pub comment_line: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TomlSuppressionEntry {
pub file: String,
pub rule: String,
pub hash: String,
#[serde(default)]
pub justification: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TomlWildcardEntry {
#[serde(default)]
pub file_glob: Option<String>,
#[serde(default)]
pub rule: Option<String>,
#[serde(default)]
pub rule_glob: Option<String>,
#[serde(default)]
pub function_prefix: Option<String>,
#[serde(default)]
pub justification: String,
}
#[derive(Clone)]
struct CompiledWildcard {
file_glob: Option<regex::Regex>,
rule: Option<String>,
rule_glob: Option<regex::Regex>,
function_prefix: Option<String>,
justification: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TomlSuppressionFile {
#[serde(default)]
pub suppression: Vec<TomlSuppressionEntry>,
#[serde(default)]
pub wildcard: Vec<TomlWildcardEntry>,
}
impl Suppression {
pub fn parse(comment: &str, line_number: usize) -> Option<Self> {
if !comment.contains("SQC-SUPPRESS") {
return None;
}
let rule_id = Self::extract_rule_id(comment)?;
let hash = Self::extract_hash(comment)?;
let justification = Self::extract_justification(comment).unwrap_or_default();
Some(Suppression {
rule_id,
hash,
justification,
comment_line: line_number + 1, })
}
pub fn matches(&self, source: &str, violation_line: usize) -> bool {
if self.comment_line > 0 {
if self.comment_line > violation_line
|| violation_line - self.comment_line > MAX_PROXIMITY
{
return false;
}
}
let code = Self::get_line(source, violation_line);
let code = Self::strip_suppress_comment(&code);
let current_hash = SuppressionManager::calculate_suppression_hash(&self.rule_id, &code);
current_hash == self.hash
}
fn get_line(source: &str, line: usize) -> String {
source
.lines()
.nth(line.saturating_sub(1))
.unwrap_or("")
.to_string()
}
fn strip_suppress_comment(line: &str) -> String {
if let Some(pos) = line.find("// SQC-SUPPRESS") {
line[..pos].to_string()
} else if let Some(pos) = line.find("/* SQC-SUPPRESS") {
line[..pos].to_string()
} else {
line.to_string()
}
}
fn extract_rule_id(comment: &str) -> Option<String> {
let re = regex::Regex::new(r"SQC-SUPPRESS:\s*([A-Z0-9]+-[A-Z0-9]+)").ok()?;
re.captures(comment)?.get(1).map(|m| m.as_str().to_string())
}
fn extract_hash(comment: &str) -> Option<String> {
let re = regex::Regex::new(r"HASH:([a-f0-9]+)").ok()?;
re.captures(comment)?.get(1).map(|m| m.as_str().to_string())
}
fn extract_justification(comment: &str) -> Option<String> {
let re = regex::Regex::new(
r#"JUSTIFICATION:\s*"([^"]+)"|JUSTIFICATION:\s*(.+?)(?:\s+[A-Z]+:|$)"#,
)
.ok()?;
re.captures(comment).and_then(|cap| {
cap.get(1)
.or(cap.get(2))
.map(|m| m.as_str().trim().to_string())
})
}
}
#[derive(Default, Clone)]
pub struct SuppressionManager {
suppressions: HashMap<String, Vec<Suppression>>,
toml_suppressions: Vec<(String, Suppression)>,
wildcard_suppressions: Vec<CompiledWildcard>,
}
impl SuppressionManager {
pub fn new() -> Self {
Self::default()
}
pub fn load_from_toml(&mut self, toml_path: &str) -> Result<usize, String> {
let content = std::fs::read_to_string(toml_path)
.map_err(|e| format!("Cannot read {}: {}", toml_path, e))?;
let parsed: TomlSuppressionFile = toml::from_str(&content)
.map_err(|e| format!("Invalid TOML in {}: {}", toml_path, e))?;
let count = parsed.suppression.len();
for entry in parsed.suppression {
let suppression = Suppression {
rule_id: entry.rule,
hash: entry.hash,
justification: entry.justification,
comment_line: 0, };
self.toml_suppressions.push((entry.file, suppression));
}
let wildcard_count = parsed.wildcard.len();
for entry in parsed.wildcard {
let compiled = CompiledWildcard::try_from_entry(entry)?;
self.wildcard_suppressions.push(compiled);
}
Ok(count + wildcard_count)
}
pub fn extract_from_source(&mut self, file_path: &str, source: &str) {
let mut file_suppressions = Vec::new();
for (line_num, line) in source.lines().enumerate() {
if line.contains("SQC-SUPPRESS") {
if let Some(suppression) = Suppression::parse(line, line_num) {
file_suppressions.push(suppression);
}
}
}
if !file_suppressions.is_empty() {
self.suppressions
.insert(file_path.to_string(), file_suppressions);
}
}
pub fn should_suppress(
&self,
file_path: &str,
rule_id: &str,
line: usize,
source: &str,
message: &str,
) -> Option<&str> {
if let Some(file_supps) = self.suppressions.get(file_path) {
if let Some(s) = file_supps
.iter()
.find(|s| s.rule_id == rule_id && s.matches(source, line))
{
return Some(&s.justification);
}
}
if let Some(s) = self.toml_suppressions.iter().find_map(|(pattern, s)| {
if s.rule_id == rule_id
&& file_path_matches(file_path, pattern)
&& s.matches(source, line)
{
Some(s)
} else {
None
}
}) {
return Some(&s.justification);
}
for w in &self.wildcard_suppressions {
if w.matches(file_path, rule_id, message) {
return Some(&w.justification);
}
}
None
}
pub fn wildcard_count(&self) -> usize {
self.wildcard_suppressions.len()
}
pub fn calculate_suppression_hash(rule_id: &str, code: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(rule_id.as_bytes());
hasher.update(b":");
let normalized = code.split_whitespace().collect::<Vec<_>>().join(" ");
hasher.update(normalized.as_bytes());
format!("{:x}", hasher.finalize())[..16].to_string()
}
}
impl CompiledWildcard {
fn try_from_entry(entry: TomlWildcardEntry) -> Result<Self, String> {
if entry.file_glob.is_none()
&& entry.rule.is_none()
&& entry.rule_glob.is_none()
&& entry.function_prefix.is_none()
{
return Err(
"Wildcard entry must have at least one of: file_glob, rule, rule_glob, function_prefix".to_string()
);
}
let file_glob = entry
.file_glob
.as_deref()
.map(|g| glob_to_regex(g, true))
.transpose()?;
let rule_glob = entry
.rule_glob
.as_deref()
.map(|g| glob_to_regex(g, false))
.transpose()?;
Ok(CompiledWildcard {
file_glob,
rule: entry.rule,
rule_glob,
function_prefix: entry.function_prefix,
justification: entry.justification,
})
}
fn matches(&self, file_path: &str, rule_id: &str, message: &str) -> bool {
if let Some(ref re) = self.file_glob {
let normalized = file_path.replace('\\', "/");
if !re.is_match(&normalized) {
return false;
}
}
if let Some(ref rule) = self.rule {
if rule_id != rule {
return false;
}
}
if let Some(ref re) = self.rule_glob {
if !re.is_match(rule_id) {
return false;
}
}
if let Some(ref prefix) = self.function_prefix {
if !message_contains_prefix(message, prefix) {
return false;
}
}
true
}
}
fn glob_to_regex(pattern: &str, is_path: bool) -> Result<regex::Regex, String> {
let mut regex_str = String::new();
let chars: Vec<char> = pattern.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
match chars[i] {
'*' => {
if i + 1 < len && chars[i + 1] == '*' {
regex_str.push_str(".*");
i += 2;
if i < len && chars[i] == '/' {
regex_str.push_str("/?");
i += 1;
}
} else if is_path {
regex_str.push_str("[^/]*");
i += 1;
} else {
regex_str.push_str(".*");
i += 1;
}
}
'?' => {
if is_path {
regex_str.push_str("[^/]");
} else {
regex_str.push('.');
}
i += 1;
}
c => {
if ".()+|[]{}^$\\".contains(c) {
regex_str.push('\\');
}
regex_str.push(c);
i += 1;
}
}
}
let anchored = if is_path {
format!("(?:^|/){regex_str}$")
} else {
format!("^{regex_str}$")
};
regex::Regex::new(&anchored).map_err(|e| format!("Invalid glob pattern '{}': {}", pattern, e))
}
fn message_contains_prefix(message: &str, prefix: &str) -> bool {
if prefix.is_empty() {
return false;
}
let mut search_from = 0;
while let Some(pos) = message[search_from..].find(prefix) {
let abs_pos = search_from + pos;
let at_boundary = abs_pos == 0
|| !message.as_bytes()[abs_pos - 1].is_ascii_alphanumeric()
&& message.as_bytes()[abs_pos - 1] != b'_';
if at_boundary {
return true;
}
search_from = abs_pos + 1;
}
false
}
fn file_path_matches(full_path: &str, pattern: &str) -> bool {
if full_path == pattern {
return true;
}
let normalized = full_path.replace('\\', "/");
let norm_pattern = pattern.replace('\\', "/");
if normalized.ends_with(&norm_pattern) {
let prefix_len = normalized.len() - norm_pattern.len();
prefix_len == 0
|| normalized.as_bytes().get(prefix_len - 1) == Some(&b'/')
|| normalized.as_bytes().get(prefix_len - 1) == Some(&b'\\')
} else {
let file_name = Path::new(full_path)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
file_name == pattern
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_basic() {
let comment =
"// SQC-SUPPRESS: ARR30-C HASH:a3f5d2b1 JUSTIFICATION: \"Bounds checked by caller\"";
let s = Suppression::parse(comment, 0).unwrap();
assert_eq!(s.rule_id, "ARR30-C");
assert_eq!(s.hash, "a3f5d2b1");
assert_eq!(s.justification, "Bounds checked by caller");
assert_eq!(s.comment_line, 1);
}
#[test]
fn test_parse_no_hash_returns_none() {
let comment = "// SQC-SUPPRESS: ARR30-C JUSTIFICATION: \"no hash\"";
assert!(Suppression::parse(comment, 0).is_none());
}
#[test]
fn test_match_line_before() {
let rule_id = "EXP34-C";
let code_line = " *ptr = value;";
let hash = SuppressionManager::calculate_suppression_hash(rule_id, code_line);
let source = format!(
"// SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"test\"\n{}",
hash, code_line
);
let s = Suppression::parse(source.lines().next().unwrap(), 0).unwrap();
assert!(s.matches(&source, 2));
}
#[test]
fn test_match_inline() {
let rule_id = "EXP34-C";
let code_line = " *ptr = value;";
let hash = SuppressionManager::calculate_suppression_hash(rule_id, code_line);
let line_with_suppress = format!(
"{} // SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"test\"",
code_line, hash
);
let source = format!("int x;\n{}\nint y;", line_with_suppress);
let s = Suppression::parse(&line_with_suppress, 1).unwrap();
assert!(s.matches(&source, 2));
}
#[test]
fn test_match_stacked() {
let code_line = " bytes_read = fread(buf, 1, file_size, fp);";
let hash_err = SuppressionManager::calculate_suppression_hash("ERR00-C", code_line);
let hash_exp = SuppressionManager::calculate_suppression_hash("EXP34-C", code_line);
let source = format!(
"// SQC-SUPPRESS: ERR00-C HASH:{} JUSTIFICATION: \"r1\"\n\
// SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"r2\"\n\
{}",
hash_err, hash_exp, code_line
);
let mut lines = source.lines();
let s1 = Suppression::parse(lines.next().unwrap(), 0).unwrap();
let s2 = Suppression::parse(lines.next().unwrap(), 1).unwrap();
assert!(s1.matches(&source, 3));
assert!(s2.matches(&source, 3));
}
#[test]
fn test_no_match_wrong_rule() {
let code_line = " *ptr = value;";
let hash = SuppressionManager::calculate_suppression_hash("INT30-C", code_line);
let source = format!(
"// SQC-SUPPRESS: INT30-C HASH:{} JUSTIFICATION: \"test\"\n{}",
hash, code_line
);
let s = Suppression::parse(source.lines().next().unwrap(), 0).unwrap();
assert!(s.matches(&source, 2));
}
#[test]
fn test_no_match_too_far() {
let code_line = " *ptr = value;";
let hash = SuppressionManager::calculate_suppression_hash("EXP34-C", code_line);
let mut lines = vec![format!(
"// SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"test\"",
hash
)];
for _ in 0..18 {
lines.push("// filler".to_string());
}
lines.push(code_line.to_string());
let source = lines.join("\n");
let s = Suppression::parse(source.lines().next().unwrap(), 0).unwrap();
assert!(!s.matches(&source, 20));
}
#[test]
fn test_no_match_code_changed() {
let original_code = " *ptr = value;";
let hash = SuppressionManager::calculate_suppression_hash("EXP34-C", original_code);
let modified_source = format!(
"// SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"test\"\n *ptr = new_value;",
hash
);
let s = Suppression::parse(modified_source.lines().next().unwrap(), 0).unwrap();
assert!(!s.matches(&modified_source, 2));
}
#[test]
fn test_strip_suppress_comment() {
let line = " *ptr = val; // SQC-SUPPRESS: EXP34-C HASH:abc JUSTIFICATION: \"test\"";
let stripped = Suppression::strip_suppress_comment(line);
assert_eq!(stripped, " *ptr = val; ");
}
#[test]
fn test_manager_roundtrip() {
let rule_id = "EXP34-C";
let code_line = " *ptr = value;";
let hash = SuppressionManager::calculate_suppression_hash(rule_id, code_line);
let source = format!(
"void f(int *ptr) {{\n\
// SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"validated by caller\"\n\
{}\n\
}}",
hash, code_line
);
let mut mgr = SuppressionManager::new();
mgr.extract_from_source("test.c", &source);
assert!(mgr
.should_suppress("test.c", "EXP34-C", 3, &source, "")
.is_some());
assert!(mgr
.should_suppress("test.c", "INT30-C", 3, &source, "")
.is_none());
assert!(mgr
.should_suppress("other.c", "EXP34-C", 3, &source, "")
.is_none());
}
#[test]
fn test_file_path_matches_bare_filename() {
assert!(file_path_matches(
"/home/user/project/src/ringbuffer.c",
"ringbuffer.c"
));
assert!(file_path_matches("ringbuffer.c", "ringbuffer.c"));
assert!(!file_path_matches(
"/home/user/project/src/other.c",
"ringbuffer.c"
));
}
#[test]
fn test_file_path_matches_relative_path() {
assert!(file_path_matches(
"/home/user/project/src/ringbuffer.c",
"src/ringbuffer.c"
));
assert!(!file_path_matches(
"/home/user/project/lib/ringbuffer.c",
"src/ringbuffer.c"
));
}
#[test]
fn test_toml_suppression() {
let rule_id = "INT30-C";
let code_line = " result = a + b;";
let hash = SuppressionManager::calculate_suppression_hash(rule_id, code_line);
let source = format!(
"void f() {{\n int a = 1;\n int b = 2;\n unsigned result;\n{}\n}}",
code_line
);
let mut mgr = SuppressionManager::new();
mgr.toml_suppressions.push((
"test.c".to_string(),
Suppression {
rule_id: rule_id.to_string(),
hash,
justification: "overflow checked".to_string(),
comment_line: 0,
},
));
assert!(mgr
.should_suppress("/some/path/test.c", rule_id, 5, &source, "")
.is_some());
assert!(mgr
.should_suppress("/some/path/other.c", rule_id, 5, &source, "")
.is_none());
}
#[test]
fn test_inline_and_toml_coexist() {
let code_line = " result = *ptr + offset;";
let hash_exp = SuppressionManager::calculate_suppression_hash("EXP34-C", code_line);
let hash_int = SuppressionManager::calculate_suppression_hash("INT30-C", code_line);
let source = format!(
"// SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"ptr checked\"\n{}",
hash_exp, code_line
);
let mut mgr = SuppressionManager::new();
mgr.extract_from_source("/project/test.c", &source);
mgr.toml_suppressions.push((
"test.c".to_string(),
Suppression {
rule_id: "INT30-C".to_string(),
hash: hash_int,
justification: "overflow checked".to_string(),
comment_line: 0,
},
));
assert!(mgr
.should_suppress("/project/test.c", "EXP34-C", 2, &source, "")
.is_some());
assert!(mgr
.should_suppress("/project/test.c", "INT30-C", 2, &source, "")
.is_some());
assert!(mgr
.should_suppress("/project/test.c", "ARR30-C", 2, &source, "")
.is_none());
}
#[test]
fn test_inline_detection() {
let code_line = " free(ptr);";
let hash = SuppressionManager::calculate_suppression_hash("EXP34-C", code_line);
let source = format!(
"void f(void *ptr) {{\n\
{} // SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"NULL is valid\"\n\
}}",
code_line, hash
);
let mut mgr = SuppressionManager::new();
mgr.extract_from_source("test.c", &source);
assert!(mgr
.should_suppress("test.c", "EXP34-C", 2, &source, "")
.is_some());
}
#[test]
fn test_glob_to_regex_simple_star() {
let re = glob_to_regex("*.c", true).unwrap();
assert!(re.is_match("test.c"));
assert!(re.is_match("/project/src/test.c"));
assert!(!re.is_match("test.h"));
assert!(re.is_match("/project/src/sub/test.c"));
let re2 = glob_to_regex("sub/*.c", true).unwrap();
assert!(re2.is_match("/project/sub/test.c"));
assert!(!re2.is_match("/project/sub/deep/test.c"));
}
#[test]
fn test_glob_to_regex_double_star() {
let re = glob_to_regex("src/vendor/**", true).unwrap();
assert!(re.is_match("src/vendor/lib.c"));
assert!(re.is_match("src/vendor/sub/deep/lib.c"));
assert!(re.is_match("/project/src/vendor/lib.c"));
assert!(!re.is_match("src/other/lib.c"));
}
#[test]
fn test_glob_to_regex_double_star_prefix() {
let re = glob_to_regex("**/*.generated.c", true).unwrap();
assert!(re.is_match("src/foo.generated.c"));
assert!(re.is_match("/a/b/c/foo.generated.c"));
assert!(!re.is_match("src/foo.c"));
}
#[test]
fn test_glob_to_regex_question_mark() {
let re = glob_to_regex("test?.c", true).unwrap();
assert!(re.is_match("test1.c"));
assert!(re.is_match("/project/testA.c"));
assert!(!re.is_match("test.c")); assert!(!re.is_match("test12.c")); }
#[test]
fn test_glob_to_regex_rule_pattern() {
let re = glob_to_regex("DCL*", false).unwrap();
assert!(re.is_match("DCL31-C"));
assert!(re.is_match("DCL07-C"));
assert!(!re.is_match("INT30-C"));
}
#[test]
fn test_glob_to_regex_rule_exact() {
let re = glob_to_regex("INT3?-C", false).unwrap();
assert!(re.is_match("INT30-C"));
assert!(re.is_match("INT32-C"));
assert!(!re.is_match("INT300-C"));
}
#[test]
fn test_message_contains_prefix_basic() {
let msg = "Function 'wolfSSL_Init' is called without prior declaration";
assert!(message_contains_prefix(msg, "wolfSSL_"));
assert!(!message_contains_prefix(msg, "openSSL_"));
}
#[test]
fn test_message_contains_prefix_word_boundary() {
let msg = "Function 'myWolfSSL_Init' is called without prior declaration";
assert!(!message_contains_prefix(msg, "wolfSSL_"));
}
#[test]
fn test_message_contains_prefix_at_start() {
let msg = "wolfSSL_Init is undeclared";
assert!(message_contains_prefix(msg, "wolfSSL_"));
}
#[test]
fn test_message_contains_prefix_after_quote() {
let msg = "Use of undeclared function 'cJSON_Parse'";
assert!(message_contains_prefix(msg, "cJSON_"));
}
#[test]
fn test_message_contains_prefix_empty() {
assert!(!message_contains_prefix("anything", ""));
}
#[test]
fn test_wildcard_file_glob() {
let mut mgr = SuppressionManager::new();
mgr.wildcard_suppressions.push(
CompiledWildcard::try_from_entry(TomlWildcardEntry {
file_glob: Some("src/vendor/**".to_string()),
rule: Some("DCL31-C".to_string()),
rule_glob: None,
function_prefix: None,
justification: "Vendor code".to_string(),
})
.unwrap(),
);
assert!(mgr
.should_suppress(
"/project/src/vendor/lib.c",
"DCL31-C",
10,
"",
"Function 'foo' is called without prior declaration",
)
.is_some());
assert!(mgr
.should_suppress(
"/project/src/core/lib.c",
"DCL31-C",
10,
"",
"Function 'foo' is called without prior declaration",
)
.is_none());
assert!(mgr
.should_suppress(
"/project/src/vendor/lib.c",
"INT30-C",
10,
"",
"Unsigned overflow",
)
.is_none());
}
#[test]
fn test_wildcard_rule_glob() {
let mut mgr = SuppressionManager::new();
mgr.wildcard_suppressions.push(
CompiledWildcard::try_from_entry(TomlWildcardEntry {
file_glob: Some("vendor/**".to_string()),
rule: None,
rule_glob: Some("DCL*".to_string()),
function_prefix: None,
justification: "All DCL rules suppressed for vendor".to_string(),
})
.unwrap(),
);
assert!(mgr
.should_suppress("/project/vendor/lib.c", "DCL31-C", 10, "", "")
.is_some());
assert!(mgr
.should_suppress("/project/vendor/lib.c", "DCL07-C", 10, "", "")
.is_some());
assert!(mgr
.should_suppress("/project/vendor/lib.c", "INT30-C", 10, "", "")
.is_none());
}
#[test]
fn test_wildcard_function_prefix() {
let mut mgr = SuppressionManager::new();
mgr.wildcard_suppressions.push(
CompiledWildcard::try_from_entry(TomlWildcardEntry {
file_glob: None,
rule: Some("DCL31-C".to_string()),
rule_glob: None,
function_prefix: Some("wolfSSL_".to_string()),
justification: "wolfSSL library functions".to_string(),
})
.unwrap(),
);
assert!(mgr
.should_suppress(
"any_file.c",
"DCL31-C",
10,
"",
"Function 'wolfSSL_Init' is called without prior declaration",
)
.is_some());
assert!(mgr
.should_suppress(
"any_file.c",
"DCL31-C",
10,
"",
"Function 'openSSL_Init' is called without prior declaration",
)
.is_none());
assert!(mgr
.should_suppress("any_file.c", "INT30-C", 10, "", "wolfSSL_Init overflow",)
.is_none());
}
#[test]
fn test_wildcard_justification_returned() {
let mut mgr = SuppressionManager::new();
mgr.wildcard_suppressions.push(
CompiledWildcard::try_from_entry(TomlWildcardEntry {
file_glob: Some("**".to_string()),
rule: Some("DCL31-C".to_string()),
rule_glob: None,
function_prefix: None,
justification: "Global DCL31-C suppression".to_string(),
})
.unwrap(),
);
let result = mgr.should_suppress("any.c", "DCL31-C", 1, "", "");
assert_eq!(result, Some("Global DCL31-C suppression"));
}
#[test]
fn test_wildcard_requires_at_least_one_field() {
let result = CompiledWildcard::try_from_entry(TomlWildcardEntry {
file_glob: None,
rule: None,
rule_glob: None,
function_prefix: None,
justification: "No filters".to_string(),
});
assert!(result.is_err());
}
#[test]
fn test_wildcard_toml_parsing() {
let toml_str = r#"
[[wildcard]]
file_glob = "src/vendor/**"
rule = "DCL31-C"
justification = "Vendor code"
[[wildcard]]
rule_glob = "DCL*"
function_prefix = "wolfSSL_"
justification = "wolfSSL library"
"#;
let parsed: TomlSuppressionFile = toml::from_str(toml_str).unwrap();
assert_eq!(parsed.wildcard.len(), 2);
assert_eq!(
parsed.wildcard[0].file_glob.as_deref(),
Some("src/vendor/**")
);
assert_eq!(parsed.wildcard[0].rule.as_deref(), Some("DCL31-C"));
assert_eq!(parsed.wildcard[1].rule_glob.as_deref(), Some("DCL*"));
assert_eq!(
parsed.wildcard[1].function_prefix.as_deref(),
Some("wolfSSL_")
);
}
#[test]
fn test_wildcard_and_hash_coexist_in_toml() {
let toml_str = r#"
[[suppression]]
file = "ringbuffer.c"
rule = "INT30-C"
hash = "a1f5861150a1e5b8"
justification = "Hash-matched suppression"
[[wildcard]]
file_glob = "src/vendor/**"
rule = "DCL31-C"
justification = "Wildcard suppression"
"#;
let parsed: TomlSuppressionFile = toml::from_str(toml_str).unwrap();
assert_eq!(parsed.suppression.len(), 1);
assert_eq!(parsed.wildcard.len(), 1);
}
#[test]
fn test_hash_suppression_takes_priority_over_wildcard() {
let rule_id = "EXP34-C";
let code_line = " *ptr = value;";
let hash = SuppressionManager::calculate_suppression_hash(rule_id, code_line);
let source = format!(
"// SQC-SUPPRESS: EXP34-C HASH:{} JUSTIFICATION: \"inline justification\"\n{}",
hash, code_line
);
let mut mgr = SuppressionManager::new();
mgr.extract_from_source("test.c", &source);
mgr.wildcard_suppressions.push(
CompiledWildcard::try_from_entry(TomlWildcardEntry {
file_glob: Some("**".to_string()),
rule: Some("EXP34-C".to_string()),
rule_glob: None,
function_prefix: None,
justification: "wildcard justification".to_string(),
})
.unwrap(),
);
let result = mgr.should_suppress("test.c", "EXP34-C", 2, &source, "");
assert_eq!(result, Some("inline justification"));
}
}