use anyhow::{Context, Result};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CFunctionSignature {
pub name: String,
pub return_type: String,
pub params: Vec<CParam>,
pub likely_alloc: bool,
pub likely_free: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CParam {
pub c_type: String,
pub name: String,
pub is_pointer: bool,
pub is_const: bool,
}
static FUNC_DECL_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?m)^\s*(?:extern\s+)?(?:static\s+)?(?:inline\s+)?([\w\s\*]+?)\s+(\w+)\s*\(([^)]*)\)\s*;",
)
.expect("Failed to compile function declaration regex")
});
static PARAM_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"((?:const\s+)?[\w]+(?:\s*\*+)?)\s*(\w*)")
.expect("Failed to compile parameter regex")
});
pub fn parse_c_header(path: &str) -> Result<Vec<CFunctionSignature>> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read C header: {}", path))?;
parse_c_source(&content)
}
pub fn parse_c_source(source: &str) -> Result<Vec<CFunctionSignature>> {
let cleaned = strip_comments(source);
let mut signatures = Vec::new();
for cap in FUNC_DECL_RE.captures_iter(&cleaned) {
let return_type = cap[1].trim().to_string();
let name = cap[2].to_string();
let params_str = cap[3].trim();
let params = parse_params(params_str);
let likely_alloc = return_type.contains('*');
let likely_free = return_type.trim() == "void"
&& params.iter().any(|p| p.is_pointer && !p.is_const);
signatures.push(CFunctionSignature {
name,
return_type,
params,
likely_alloc,
likely_free,
});
}
Ok(signatures)
}
fn parse_params(params_str: &str) -> Vec<CParam> {
let trimmed = params_str.trim();
if trimmed.is_empty() || trimmed == "void" {
return Vec::new();
}
let mut params = Vec::new();
for part in trimmed.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(cap) = PARAM_RE.captures(part) {
let c_type = cap[1].trim().to_string();
let name = cap.get(2).map(|m| m.as_str().to_string()).unwrap_or_default();
let is_pointer = c_type.contains('*');
let is_const = c_type.starts_with("const");
params.push(CParam {
c_type,
name,
is_pointer,
is_const,
});
}
}
params
}
fn strip_comments(source: &str) -> String {
let mut result = String::with_capacity(source.len());
let chars: Vec<char> = source.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' {
while i < len && chars[i] != '\n' {
i += 1;
}
} else if i + 1 < len && chars[i] == '/' && chars[i + 1] == '*' {
i += 2;
while i + 1 < len && !(chars[i] == '*' && chars[i + 1] == '/') {
i += 1;
}
i += 2; } else {
result.push(chars[i]);
i += 1;
}
}
result
}
pub fn detect_ownership_pattern(sig: &CFunctionSignature) -> Option<&'static str> {
let name_lower = sig.name.to_lowercase();
if sig.likely_alloc
&& (name_lower.contains("alloc")
|| name_lower.contains("create")
|| name_lower.contains("new")
|| name_lower.contains("open")
|| name_lower.contains("init"))
{
return Some("alloc");
}
if sig.likely_free
&& (name_lower.contains("free")
|| name_lower.contains("destroy")
|| name_lower.contains("close")
|| name_lower.contains("release")
|| name_lower.contains("cleanup"))
{
return Some("free");
}
if sig.likely_alloc {
return Some("alloc");
}
if sig.likely_free {
return Some("free");
}
if sig.params.iter().any(|p| p.is_pointer)
&& sig.params.iter().filter(|p| p.is_pointer).all(|p| p.is_const)
{
return Some("borrow");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_function() {
let source = "void* malloc(size_t size);\n";
let sigs = parse_c_source(source).unwrap();
assert_eq!(sigs.len(), 1);
assert_eq!(sigs[0].name, "malloc");
assert!(sigs[0].return_type.contains("void*"));
assert!(sigs[0].likely_alloc);
}
#[test]
fn test_parse_free_function() {
let source = "void free(void* ptr);\n";
let sigs = parse_c_source(source).unwrap();
assert_eq!(sigs.len(), 1);
assert_eq!(sigs[0].name, "free");
assert!(sigs[0].likely_free);
assert!(!sigs[0].likely_alloc);
}
#[test]
fn test_parse_borrow_function() {
let source = "size_t strlen(const char* s);\n";
let sigs = parse_c_source(source).unwrap();
assert_eq!(sigs.len(), 1);
assert_eq!(sigs[0].name, "strlen");
assert!(!sigs[0].likely_alloc);
assert!(!sigs[0].likely_free);
}
#[test]
fn test_parse_multiple_functions() {
let source = r#"
void* malloc(size_t size);
void free(void* ptr);
int printf(const char* fmt);
FILE* fopen(const char* path, const char* mode);
int fclose(FILE* fp);
"#;
let sigs = parse_c_source(source).unwrap();
assert_eq!(sigs.len(), 5);
}
#[test]
fn test_strip_comments() {
let source = r#"
// This is a line comment
void* malloc(size_t size);
/* This is a
block comment */
void free(void* ptr);
"#;
let cleaned = strip_comments(source);
assert!(!cleaned.contains("line comment"));
assert!(!cleaned.contains("block comment"));
assert!(cleaned.contains("malloc"));
assert!(cleaned.contains("free"));
}
#[test]
fn test_detect_alloc_pattern() {
let sig = CFunctionSignature {
name: "mylib_create".to_string(),
return_type: "mylib_t*".to_string(),
params: vec![],
likely_alloc: true,
likely_free: false,
};
assert_eq!(detect_ownership_pattern(&sig), Some("alloc"));
}
#[test]
fn test_detect_free_pattern() {
let sig = CFunctionSignature {
name: "mylib_destroy".to_string(),
return_type: "void".to_string(),
params: vec![CParam {
c_type: "mylib_t*".to_string(),
name: "handle".to_string(),
is_pointer: true,
is_const: false,
}],
likely_alloc: false,
likely_free: true,
};
assert_eq!(detect_ownership_pattern(&sig), Some("free"));
}
#[test]
fn test_detect_borrow_pattern() {
let sig = CFunctionSignature {
name: "mylib_get_name".to_string(),
return_type: "int".to_string(),
params: vec![CParam {
c_type: "const mylib_t*".to_string(),
name: "handle".to_string(),
is_pointer: true,
is_const: true,
}],
likely_alloc: false,
likely_free: false,
};
assert_eq!(detect_ownership_pattern(&sig), Some("borrow"));
}
#[test]
fn test_void_params() {
let source = "int getpid(void);\n";
let sigs = parse_c_source(source).unwrap();
assert_eq!(sigs.len(), 1);
assert!(sigs[0].params.is_empty());
}
}