use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use syn::visit::Visit;
const COMPLETENESS_ALLOW: &[&str] = &[
"next_head", "prev_head", "set_enum_width",
"is_public_name",
"is_weak_name",
"color",
"flags",
"has_external_refs",
"has_jump_or_flow_xref",
"info",
"accept_eula",
];
const VALIDITY_ALLOW: &[&str] = &[];
fn sources() -> Vec<(PathBuf, String)> {
let mut files = Vec::new();
collect_rs(
&Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
&mut files,
);
files.sort();
files
.into_iter()
.map(|p| {
let text =
fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
(p, text)
})
.collect()
}
fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in fs::read_dir(dir).expect("read_dir src") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
collect_rs(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
fn alias_strings(src: &str) -> Vec<String> {
const NEEDLE: &str = "doc(alias(";
let bytes = src.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while let Some(rel) = src[i..].find(NEEDLE) {
let mut j = i + rel + NEEDLE.len();
let mut depth = 1i32;
while j < bytes.len() && depth > 0 {
match bytes[j] {
b'(' => depth += 1,
b')' => depth -= 1,
b'"' => {
let mut k = j + 1;
let mut lit = String::new();
while k < bytes.len() && bytes[k] != b'"' {
if bytes[k] == b'\\' && k + 1 < bytes.len() {
lit.push(bytes[k + 1] as char);
k += 2;
} else {
lit.push(bytes[k] as char);
k += 1;
}
}
out.push(lit);
j = k;
}
_ => {}
}
j += 1;
}
i = j;
}
out
}
fn sdk_identifiers() -> Option<HashSet<String>> {
let dir = option_env!("IDA_SDK_INCLUDE")?;
let mut headers = Vec::new();
collect_headers(Path::new(dir), &mut headers);
let mut idents = HashSet::new();
for header in headers {
let text = fs::read_to_string(&header).unwrap_or_default();
let mut cur = String::new();
for ch in text.chars() {
if ch == '_' || ch.is_ascii_alphanumeric() {
cur.push(ch);
} else if !cur.is_empty() {
idents.insert(std::mem::take(&mut cur));
}
}
if !cur.is_empty() {
idents.insert(cur);
}
}
Some(idents)
}
fn collect_headers(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_headers(&path, out);
} else if path.extension().is_some_and(|e| e == "h" || e == "hpp") {
out.push(path);
}
}
}
#[test]
fn every_alias_names_a_real_sdk_symbol() {
let Some(idents) = sdk_identifiers() else {
eprintln!("skipping: IDA_SDK_INCLUDE unset (no SDK headers to validate aliases against)");
return;
};
let mut violations = Vec::new();
for (path, src) in sources() {
for alias in alias_strings(&src) {
let leaf = alias.rsplit("::").next().unwrap_or(&alias);
if leaf.is_empty() || idents.contains(leaf) || VALIDITY_ALLOW.contains(&alias.as_str())
{
continue;
}
violations.push(format!(
"{}: alias {alias:?} (leaf `{leaf}`) is not an SDK identifier",
path.display()
));
}
}
assert!(
violations.is_empty(),
"every #[doc(alias)] must name a real SDK symbol; fix the name or, if it is genuinely \
not an SDK identifier, add it to VALIDITY_ALLOW:\n{}",
violations.join("\n")
);
}
fn is_doc_alias(attr: &syn::Attribute) -> bool {
attr.path().is_ident("doc")
&& matches!(&attr.meta, syn::Meta::List(list) if list.tokens.to_string().contains("alias"))
}
fn is_self_db(expr: &syn::Expr) -> bool {
let syn::Expr::Field(field) = expr else {
return false;
};
let syn::Member::Named(name) = &field.member else {
return false;
};
name == "db" && matches!(&*field.base, syn::Expr::Path(p) if p.path.is_ident("self"))
}
fn is_self(expr: &syn::Expr) -> bool {
matches!(expr, syn::Expr::Path(p) if p.path.is_ident("self"))
}
struct KernelReach<'a> {
forwarders: &'a HashSet<String>,
found: bool,
}
impl<'ast> Visit<'ast> for KernelReach<'_> {
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
if is_self_db(&node.receiver)
|| (is_self(&node.receiver) && self.forwarders.contains(&node.method.to_string()))
{
self.found = true;
}
syn::visit::visit_expr_method_call(self, node);
}
}
fn forwarders() -> HashSet<String> {
let raw = fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/raw.rs"))
.expect("read raw.rs");
let mut set = HashSet::new();
for (idx, _) in raw.match_indices("fn ") {
let rest = &raw[idx + 3..];
let name: String = rest
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() && rest[name.len()..].trim_start().starts_with("(&") {
set.insert(name);
}
}
set
}
struct Completeness<'a> {
path: &'a Path,
forwarders: &'a HashSet<String>,
violations: &'a mut Vec<String>,
}
impl<'ast> Visit<'ast> for Completeness<'_> {
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
if matches!(node.vis, syn::Visibility::Public(_)) {
let mut reach = KernelReach {
forwarders: self.forwarders,
found: false,
};
reach.visit_block(&node.block);
let name = node.sig.ident.to_string();
if reach.found
&& !node.attrs.iter().any(is_doc_alias)
&& !COMPLETENESS_ALLOW.contains(&name.as_str())
{
self.violations.push(format!(
"{}: pub fn {name} reaches the kernel but carries no #[doc(alias)]",
self.path.display()
));
}
}
syn::visit::visit_impl_item_fn(self, node);
}
}
#[test]
fn forwarding_methods_carry_an_alias() {
let forwarders = forwarders();
let mut violations = Vec::new();
for (path, src) in sources() {
let file =
syn::parse_file(&src).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
Completeness {
path: &path,
forwarders: &forwarders,
violations: &mut violations,
}
.visit_file(&file);
}
assert!(
violations.is_empty(),
"every public method that forwards to the kernel must carry a #[doc(alias)] naming the \
SDK symbol it wraps; add one, or if it maps to no single symbol add it to \
COMPLETENESS_ALLOW:\n{}",
violations.join("\n")
);
}