use crate::manifest::RuleManifest;
use crate::parser::CParser;
use lang_parsing_substrate::{detect_min_c_standard, query, CStandard};
use anyhow::Result;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
pub const CON_RULE_IDS: &[&str] = &[
"CON01-C", "CON02-C", "CON03-C", "CON04-C", "CON05-C", "CON06-C", "CON07-C", "CON08-C",
"CON09-C", "CON30-C", "CON31-C", "CON32-C", "CON33-C", "CON34-C", "CON35-C", "CON36-C",
"CON37-C", "CON38-C", "CON39-C", "CON40-C", "CON41-C", "CON43-C", "CON50-C",
];
pub const WIN_RULE_IDS: &[&str] = &[
"WIN00-C", "WIN01-C", "WIN02-C", "WIN03-C", "WIN04-C", "WIN30-C",
];
pub const C11_TANGLED_RULE_IDS: &[&str] = &[
"CON02-C", "CON03-C", "CON07-C", "CON31-C", "CON32-C", "CON33-C", "EXP44-C", "FIO11-C",
];
#[derive(Debug, Default, Clone)]
pub struct ProjectProfile {
pub has_threading: bool,
pub has_windows: bool,
pub max_c_standard: Option<CStandard>,
pub has_annex_k: bool,
}
const THREADING_INCLUDE_MARKERS: &[&str] = &["pthread.h", "threads.h"];
const THREADING_IDENTIFIER_MARKERS: &[&str] =
&["pthread_", "mtx_", "cnd_", "thrd_", "atomic_", "_Atomic"];
const WINDOWS_INCLUDE_MARKERS: &[&str] = &["windows.h", "winsock2.h", "windef.h"];
const WINDOWS_IDENTIFIER_MARKERS: &[&str] = &["Win32", "HANDLE", "LPCSTR"];
const C11_HEADER_MARKERS: &[&str] = &["stdatomic.h", "threads.h"];
const ANNEX_K_FUNCTION_NAMES: &[&str] = &[
"tmpfile_s",
"tmpnam_s",
"fopen_s",
"freopen_s",
"fprintf_s",
"fscanf_s",
"printf_s",
"scanf_s",
"snprintf_s",
"sprintf_s",
"sscanf_s",
"vfprintf_s",
"vfscanf_s",
"vprintf_s",
"vscanf_s",
"vsnprintf_s",
"vsprintf_s",
"vsscanf_s",
"gets_s",
"set_constraint_handler_s",
"abort_handler_s",
"ignore_handler_s",
"getenv_s",
"bsearch_s",
"qsort_s",
"wctomb_s",
"mbstowcs_s",
"wcstombs_s",
"memcpy_s",
"memmove_s",
"strcpy_s",
"strncpy_s",
"strcat_s",
"strncat_s",
"strtok_s",
"strerror_s",
"strerrorlen_s",
"strnlen_s",
"memset_s",
"asctime_s",
"ctime_s",
"wcscpy_s",
"wcsncpy_s",
"wmemcpy_s",
"wmemmove_s",
"wcscat_s",
"wcsncat_s",
"wcstok_s",
"wcsnlen_s",
"wcrtomb_s",
"mbsrtowcs_s",
"wcsrtombs_s",
];
fn contains_any(haystack: &str, needles: &[&str]) -> bool {
needles.iter().any(|n| haystack.contains(n))
}
fn contains_annex_k_call(root: tree_sitter::Node, source: &[u8]) -> bool {
for call in query::find_descendants_of_kind(root, "call_expression") {
let Some(func) = call.child_by_field_name("function") else {
continue;
};
if func.kind() != "identifier" {
continue;
}
let Ok(name) = func.utf8_text(source) else {
continue;
};
if ANNEX_K_FUNCTION_NAMES.contains(&name) {
return true;
}
}
false
}
pub fn detect(dirs: &[String]) -> Result<ProjectProfile> {
let mut profile = ProjectProfile::default();
let mut files: Vec<PathBuf> = Vec::new();
for dir in dirs {
for entry in WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
matches!(
e.path().extension().and_then(|ext| ext.to_str()),
Some("c") | Some("h")
)
})
{
files.push(entry.path().to_path_buf());
}
}
for file in &files {
detect_file(file, &mut profile);
}
Ok(profile)
}
fn detect_file(path: &Path, profile: &mut ProjectProfile) {
let mut parser = match CParser::new() {
Ok(p) => p,
Err(_) => return,
};
let (tree, source) = match parser.parse_file(&path.to_string_lossy()) {
Ok(t) => t,
Err(_) => return,
};
if !profile.has_threading
&& (contains_any(&source, THREADING_INCLUDE_MARKERS)
|| contains_any(&source, THREADING_IDENTIFIER_MARKERS))
{
profile.has_threading = true;
}
if !profile.has_windows
&& (contains_any(&source, WINDOWS_INCLUDE_MARKERS)
|| contains_any(&source, WINDOWS_IDENTIFIER_MARKERS))
{
profile.has_windows = true;
}
let header_evidence = contains_any(&source, C11_HEADER_MARKERS);
if let Some(found) = detect_min_c_standard(&tree, source.as_bytes()).or({
header_evidence.then_some(CStandard::C11)
}) {
profile.max_c_standard = Some(match profile.max_c_standard {
Some(existing) if existing >= found => existing,
_ => found,
});
}
if !profile.has_annex_k && contains_annex_k_call(tree.root_node(), source.as_bytes()) {
profile.has_annex_k = true;
}
}
pub fn generate_manifest_toml(base: &RuleManifest, profile: &ProjectProfile) -> String {
let mut out = String::new();
out.push_str("[metadata]\n");
out.push_str(&format!("name = {:?}\n", base.metadata.name));
out.push_str(&format!("version = {:?}\n", base.metadata.version));
let description = format!(
"{} (relevance-gated by aurora-lint --detect-relevance: threading={}, windows={})",
base.metadata
.description
.clone()
.unwrap_or_else(|| "Auto-generated rules manifest".to_string()),
profile.has_threading,
profile.has_windows,
);
out.push_str(&format!("description = {:?}\n", description));
out.push_str(&format!(
"cert_version = {:?}\n",
base.metadata.cert_version
));
let mut rule_ids: Vec<&String> = base
.rules
.cert_c
.keys()
.chain(base.rules.brules.keys())
.collect();
rule_ids.sort();
rule_ids.dedup();
for rule_id in rule_ids {
let Some(config) = base.get_rule(rule_id) else {
continue;
};
let (enabled, comment) = gate_rule(rule_id, config.enabled, profile);
out.push_str("\n[rules.cert_c.");
out.push_str(rule_id);
out.push_str("]\n");
out.push_str("enabled = ");
out.push_str(if enabled { "true" } else { "false" });
if let Some(comment) = comment {
out.push_str(" # ");
out.push_str(&comment);
}
out.push('\n');
}
out
}
fn gate_rule(
rule_id: &str,
base_enabled: bool,
profile: &ProjectProfile,
) -> (bool, Option<String>) {
if base_enabled && CON_RULE_IDS.contains(&rule_id) && !profile.has_threading {
return (
false,
Some(
"auto: no pthread/threads.h/atomic usage detected in corpus (task 216)".to_string(),
),
);
}
if base_enabled && WIN_RULE_IDS.contains(&rule_id) && !profile.has_windows {
return (
false,
Some("auto: no Win32 API usage detected in corpus (task 216)".to_string()),
);
}
if C11_TANGLED_RULE_IDS.contains(&rule_id) {
let standard = match profile.max_c_standard {
Some(CStandard::C99) => "C99",
Some(CStandard::C11) => "C11",
Some(CStandard::C23) => "C23",
None => "<=C99",
};
return (
base_enabled,
Some(format!(
"detected: corpus max C standard = {standard}, Annex-K calls = {} \
(reporting-only, task 300's per-rule audit found none of this group \
safe to auto-gate — see C11_TANGLED_RULE_IDS doc comment)",
profile.has_annex_k
)),
);
}
(base_enabled, None)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_file(dir: &Path, name: &str, contents: &str) {
let path = dir.join(name);
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(contents.as_bytes()).unwrap();
}
#[test]
fn detects_no_threading_no_windows_c99_corpus() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"a.c",
"int add(int a, int b) { return a + b; }\n",
);
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert!(!profile.has_threading);
assert!(!profile.has_windows);
assert_eq!(profile.max_c_standard, None);
}
#[test]
fn detects_pthread_usage_via_include() {
let dir = tempfile::tempdir().unwrap();
write_file(dir.path(), "a.c", "#include <pthread.h>\nvoid f(void) {}\n");
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert!(profile.has_threading);
}
#[test]
fn detects_windows_usage_via_include() {
let dir = tempfile::tempdir().unwrap();
write_file(dir.path(), "a.c", "#include <windows.h>\nvoid f(void) {}\n");
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert!(profile.has_windows);
}
#[test]
fn detects_c11_generic_marker() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"a.c",
"int f(int x) { return _Generic(x, int: 1, default: 0); }\n",
);
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert_eq!(profile.max_c_standard, Some(CStandard::C11));
}
#[test]
fn detects_c11_via_stdatomic_header_alone() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"a.c",
"#include <stdatomic.h>\natomic_bool ready;\n",
);
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert_eq!(profile.max_c_standard, Some(CStandard::C11));
}
#[test]
fn does_not_promote_standard_without_c11_header_or_syntax() {
let dir = tempfile::tempdir().unwrap();
write_file(dir.path(), "a.c", "#include <stdio.h>\nvoid f(void) {}\n");
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert_eq!(profile.max_c_standard, None);
}
#[test]
fn detects_annex_k_call_by_exact_name() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"a.c",
"void f(char *dst, const char *src) { strcpy_s(dst, 10, src); }\n",
);
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert!(profile.has_annex_k);
}
#[test]
fn does_not_false_match_user_defined_s_suffixed_function() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"a.c",
"int validate_s(int x) { return x; }\nint g(void) { return validate_s(1); }\n",
);
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert!(!profile.has_annex_k);
}
#[test]
fn does_not_false_match_s_suffixed_method_call() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"a.c",
"struct Ops { int (*encode_s)(int); };\nint g(struct Ops *o) { return o->encode_s(1); }\n",
);
let profile = detect(&[dir.path().to_string_lossy().to_string()]).unwrap();
assert!(!profile.has_annex_k);
}
#[test]
fn c11_tangled_list_excludes_rules_task_300_found_misclassified() {
for id in [
"ENV31-C", "API04-C", "API07-C", "PRE04-C", "PRE30-C", "PRE31-C",
] {
assert!(
!C11_TANGLED_RULE_IDS.contains(&id),
"{id} should have been removed from C11_TANGLED_RULE_IDS"
);
}
for id in [
"CON02-C", "CON03-C", "CON07-C", "CON31-C", "CON32-C", "CON33-C", "EXP44-C", "FIO11-C",
] {
assert!(
C11_TANGLED_RULE_IDS.contains(&id),
"{id} should still be in C11_TANGLED_RULE_IDS"
);
}
}
fn base_manifest() -> RuleManifest {
RuleManifest::load("rules_templates/rules-all.toml").unwrap()
}
#[test]
fn generate_disables_con_and_win_when_absent() {
let manifest = base_manifest();
let profile = ProjectProfile {
has_threading: false,
has_windows: false,
max_c_standard: None,
has_annex_k: false,
};
let toml = generate_manifest_toml(&manifest, &profile);
for id in CON_RULE_IDS {
assert!(
toml.contains(&format!("[rules.cert_c.{id}]\nenabled = false")),
"expected {id} disabled:\n{toml}"
);
}
for id in WIN_RULE_IDS {
assert!(
toml.contains(&format!("[rules.cert_c.{id}]\nenabled = false")),
"expected {id} disabled:\n{toml}"
);
}
assert!(toml.contains("[rules.cert_c.ARR30-C]\nenabled = true"));
}
#[test]
fn generate_keeps_con_and_win_when_present() {
let manifest = base_manifest();
let profile = ProjectProfile {
has_threading: true,
has_windows: true,
max_c_standard: None,
has_annex_k: false,
};
let toml = generate_manifest_toml(&manifest, &profile);
for id in CON_RULE_IDS {
assert!(toml.contains(&format!("[rules.cert_c.{id}]\nenabled = true")));
}
for id in WIN_RULE_IDS {
assert!(toml.contains(&format!("[rules.cert_c.{id}]\nenabled = true")));
}
}
#[test]
fn generate_never_disables_rule_the_base_already_disabled_via_gating_comment() {
let mut manifest = base_manifest();
manifest.get_rule_mut("CON01-C").unwrap().enabled = false;
let profile = ProjectProfile {
has_threading: false,
has_windows: false,
max_c_standard: None,
has_annex_k: false,
};
let toml = generate_manifest_toml(&manifest, &profile);
assert!(toml.contains("[rules.cert_c.CON01-C]\nenabled = false\n"));
assert!(!toml.contains("CON01-C]\nenabled = false # auto"));
}
}