use crate::analyze::const_eval::{self, MacroConstantMap, ValueRange, VarRangeMap};
use crate::analyze::null_state::NullState;
use std::collections::{HashMap, HashSet};
use tree_sitter::Node;
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FunctionSummary {
pub frees_params: HashSet<usize>,
pub can_return_null: bool,
pub returns_allocation: bool,
pub checks_null_params: HashSet<usize>,
pub modifies_params: HashSet<usize>,
pub dereferences_params: HashSet<usize>,
pub never_returns: bool,
pub callsite_param_null_states: HashMap<usize, NullState>,
#[serde(default)]
pub callsite_param_field_null_states: HashMap<usize, HashMap<String, NullState>>,
#[serde(default)]
pub callsite_param_pointee_null_states: HashMap<usize, NullState>,
pub return_range: Option<ValueRange>,
#[serde(default)]
pub param_passthroughs: HashMap<usize, Vec<(String, usize)>>,
#[serde(default)]
pub has_env03_taint_source: bool,
#[serde(default)]
pub returns_tainted: bool,
#[serde(default)]
pub returns_from_callees: HashSet<String>,
#[serde(default)]
pub has_relative_command_write: bool,
#[serde(default)]
pub callsite_param_const_int: HashMap<usize, i64>,
}
pub const ENV03_TAINT_SOURCE_FUNCTIONS: &[&str] = &[
"recv",
"recvfrom",
"recvmsg",
"WSARecv",
"WSARecvFrom",
"accept",
"read",
"fread",
"fgets",
"gets",
"getchar",
"getc",
"fgetc",
"scanf",
"fscanf",
"sscanf",
"vscanf",
"vfscanf",
"fgetws",
"getwchar",
"getwc",
"fgetwc",
"wscanf",
"fwscanf",
"swscanf",
"vwscanf",
"vfwscanf",
"_getws",
"_getws_s",
"getenv",
"secure_getenv",
"_wgetenv",
"_wgetenv_s",
"ReadFile",
"ReadConsole",
"ReadConsoleA",
"ReadConsoleW",
"RegQueryValueExA",
"RegQueryValueExW",
];
fn body_contains_taint_source(body_text: &str) -> bool {
ENV03_TAINT_SOURCE_FUNCTIONS
.iter()
.any(|name| body_text.contains(&format!("{}(", name)))
}
fn body_contains_alias(body_text: &str, aliases: &[String]) -> bool {
aliases
.iter()
.any(|alias| body_text.contains(&format!("{}(", alias)))
}
fn body_has_relative_command_write(
body: &Node,
source: &str,
string_macros: &HashMap<String, String>,
) -> bool {
let mut found = false;
walk_for_relative_command_write(body, source, string_macros, &mut found);
found
}
fn walk_for_relative_command_write(
node: &Node,
source: &str,
string_macros: &HashMap<String, String>,
found: &mut bool,
) {
if *found {
return;
}
if node.kind() == "call_expression" {
if let Some(func) = node.child_by_field_name("function") {
let raw = func.utf8_text(source.as_bytes()).unwrap_or("");
let ident = raw
.rsplit(|c: char| !c.is_alphanumeric() && c != '_')
.next()
.unwrap_or(raw);
if matches!(
ident,
"strcpy"
| "strcat"
| "stncpy"
| "strncat"
| "wcscpy"
| "wcscat"
| "wcsncpy"
| "wcsncat"
) {
if let Some(args) = node.child_by_field_name("arguments") {
let named: Vec<_> = (0..args.child_count())
.filter_map(|i| args.child(i))
.filter(|c| c.is_named())
.collect();
if let Some(second) = named.get(1) {
let s = *second;
if s.kind() == "identifier" {
let nm = s.utf8_text(source.as_bytes()).unwrap_or("");
if const_eval::is_relative_command_macro(string_macros, nm) {
*found = true;
return;
}
}
}
}
}
}
return;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
walk_for_relative_command_write(&child, source, string_macros, found);
if *found {
return;
}
}
}
}
pub fn compute_summaries(
root: &Node,
source: &str,
macros: &MacroConstantMap,
compute_return_ranges: bool,
taint_source_aliases: &[String],
string_macros: &HashMap<String, String>,
) -> HashMap<String, FunctionSummary> {
let mut summaries = HashMap::new();
collect_function_summaries(
root,
source,
macros,
compute_return_ranges,
taint_source_aliases,
string_macros,
&mut summaries,
);
summaries
}
fn collect_function_summaries(
node: &Node,
source: &str,
macros: &MacroConstantMap,
compute_return_ranges: bool,
taint_source_aliases: &[String],
string_macros: &HashMap<String, String>,
summaries: &mut HashMap<String, FunctionSummary>,
) {
if node.kind() == "function_definition" {
if let Some(name) = extract_function_name(node, source) {
let summary = analyze_function(
node,
source,
macros,
compute_return_ranges,
taint_source_aliases,
string_macros,
);
summaries.insert(name, summary);
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
match child.kind() {
"function_definition" => {
if let Some(name) = extract_function_name(&child, source) {
let summary = analyze_function(
&child,
source,
macros,
compute_return_ranges,
taint_source_aliases,
string_macros,
);
summaries.insert(name, summary);
}
}
kind if kind.starts_with("preproc_") => {
collect_function_summaries(
&child,
source,
macros,
compute_return_ranges,
taint_source_aliases,
string_macros,
summaries,
);
}
_ => {}
}
}
}
}
fn analyze_function(
func_node: &Node,
source: &str,
macros: &MacroConstantMap,
compute_return_ranges: bool,
taint_source_aliases: &[String],
string_macros: &HashMap<String, String>,
) -> FunctionSummary {
let mut summary = FunctionSummary::default();
let params = collect_param_names(func_node, source);
let is_pointer_return;
let is_void_return;
if let Some(return_type) = func_node.child_by_field_name("type") {
let type_text = return_type.utf8_text(source.as_bytes()).unwrap_or("");
let decl_text = func_node
.child_by_field_name("declarator")
.map(|d| d.utf8_text(source.as_bytes()).unwrap_or(""))
.unwrap_or("");
is_pointer_return = decl_text.contains('*');
is_void_return = type_text == "void";
if is_pointer_return {
summary.can_return_null = true;
}
if is_void_return {
summary.can_return_null = false;
}
} else {
is_pointer_return = false;
is_void_return = false;
}
if let Some(body) = func_node.child_by_field_name("body") {
let body_text = body.utf8_text(source.as_bytes()).unwrap_or("");
summary.never_returns = check_never_returns(&body, source);
summary.returns_allocation = body_text.contains("malloc(")
|| body_text.contains("calloc(")
|| body_text.contains("realloc(")
|| body_text.contains("aligned_alloc(");
summary.has_env03_taint_source = body_contains_taint_source(body_text)
|| body_contains_alias(body_text, taint_source_aliases);
if !string_macros.is_empty() {
summary.has_relative_command_write =
body_has_relative_command_write(&body, source, string_macros);
}
if !is_void_return {
summary.returns_tainted = summary.has_env03_taint_source;
}
collect_returns_from_callees(&body, source, &mut summary.returns_from_callees);
if !summary.can_return_null {
summary.can_return_null = check_returns_null(&body, source);
}
analyze_param_usage(&body, source, ¶ms, &mut summary);
if compute_return_ranges && !is_void_return && !is_pointer_return {
summary.return_range = compute_return_range(&body, source, macros);
}
}
summary
}
pub fn collect_param_names(func_node: &Node, source: &str) -> Vec<String> {
let mut params = Vec::new();
if let Some(declarator) = func_node.child_by_field_name("declarator") {
collect_params_recursive(&declarator, source, &mut params);
}
params
}
fn collect_params_recursive(node: &Node, source: &str, params: &mut Vec<String>) {
if node.kind() == "function_declarator" {
if let Some(param_list) = node.child_by_field_name("parameters") {
for i in 0..param_list.child_count() {
if let Some(param) = param_list.child(i) {
if param.kind() == "parameter_declaration" {
if let Some(decl) = param.child_by_field_name("declarator") {
let name = extract_leaf_identifier(&decl, source);
params.push(name);
} else {
params.push(String::new()); }
}
}
}
}
} else {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
collect_params_recursive(&child, source, params);
}
}
}
}
fn check_never_returns(body: &Node, source: &str) -> bool {
let body_text = body.utf8_text(source.as_bytes()).unwrap_or("");
if !body_text.contains("abort(")
&& !body_text.contains("exit(")
&& !body_text.contains("_Exit(")
&& !body_text.contains("longjmp(")
&& !body_text.contains("quick_exit(")
{
return false;
}
let has_return = body_text.contains("return ");
let ends_with_noreturn = body_text.contains("abort()")
|| body_text.contains("exit(EXIT_FAILURE)")
|| body_text.contains("exit(1)")
|| body_text.contains("exit(EXIT_SUCCESS)")
|| body_text.contains("exit(0)");
if !has_return && ends_with_noreturn {
return true;
}
false
}
fn check_returns_null(body: &Node, source: &str) -> bool {
if body.kind() == "return_statement" {
for i in 0..body.child_count() {
if let Some(child) = body.child(i) {
if child.kind() != "return" {
let text = child.utf8_text(source.as_bytes()).unwrap_or("").trim();
if text == "NULL" || text == "0" || text == "nullptr" {
return true;
}
}
}
}
}
for i in 0..body.child_count() {
if let Some(child) = body.child(i) {
if check_returns_null(&child, source) {
return true;
}
}
}
false
}
fn analyze_param_usage(
body: &Node,
source: &str,
params: &[String],
summary: &mut FunctionSummary,
) {
let body_text = body.utf8_text(source.as_bytes()).unwrap_or("");
for (idx, param_name) in params.iter().enumerate() {
if param_name.is_empty() {
continue;
}
if body_text.contains(&format!("free({})", param_name))
|| body_text.contains(&format!("free( {} )", param_name))
{
summary.frees_params.insert(idx);
}
if body_matches_null_check(body_text, param_name)
|| body_matches_alias_null_check(body_text, param_name)
{
summary.checks_null_params.insert(idx);
}
if body_text.contains(&format!("*{} =", param_name))
|| body_text.contains(&format!("{}->", param_name))
|| body_text.contains(&format!("{}[", param_name))
{
summary.modifies_params.insert(idx);
}
if body_text.contains(&format!("*{}", param_name))
|| body_text.contains(&format!("{}->", param_name))
|| body_text.contains(&format!("{}[", param_name))
|| body_text.contains(&format!("*){}", param_name))
{
summary.dereferences_params.insert(idx);
}
}
collect_param_passthroughs(body, source, params, summary);
}
fn body_matches_null_check(body_text: &str, param_name: &str) -> bool {
if !body_text.contains(param_name) {
return false;
}
if contains_word_after_prefix(body_text, "!", param_name) {
return true;
}
for op in ["==", "!="] {
for lit in ["NULL", "0", "nullptr"] {
if contains_word_with_op(body_text, param_name, op, lit) {
return true;
}
if contains_lit_with_op_word(body_text, lit, op, param_name) {
return true;
}
}
}
false
}
fn contains_word_after_prefix(text: &str, prefix: &str, word: &str) -> bool {
let needle = format!("{}{}", prefix, word);
let bytes = text.as_bytes();
let needle_bytes = needle.as_bytes();
let mut start = 0;
while start + needle_bytes.len() <= bytes.len() {
if let Some(pos) = text[start..].find(&needle) {
let absolute = start + pos;
let after = absolute + needle_bytes.len();
let next_is_ident = bytes
.get(after)
.map(|b| is_ident_continue(*b))
.unwrap_or(false);
if !next_is_ident {
return true;
}
start = absolute + 1;
} else {
break;
}
}
false
}
fn contains_word_with_op(text: &str, word: &str, op: &str, lit: &str) -> bool {
let bytes = text.as_bytes();
let word_bytes = word.as_bytes();
let mut start = 0;
while start + word_bytes.len() <= bytes.len() {
let pos = match text[start..].find(word) {
Some(p) => start + p,
None => break,
};
let prev_is_ident = if pos == 0 {
false
} else {
is_ident_continue(bytes[pos - 1])
};
let after = pos + word_bytes.len();
let next_is_ident = bytes
.get(after)
.map(|b| is_ident_continue(*b))
.unwrap_or(false);
if !prev_is_ident && !next_is_ident {
let mut idx = after;
while idx < bytes.len() && (bytes[idx] == b' ' || bytes[idx] == b'\t') {
idx += 1;
}
if bytes[idx..].starts_with(op.as_bytes()) {
idx += op.len();
while idx < bytes.len() && (bytes[idx] == b' ' || bytes[idx] == b'\t') {
idx += 1;
}
if bytes[idx..].starts_with(lit.as_bytes()) {
let lit_end = idx + lit.len();
let next = bytes
.get(lit_end)
.map(|b| is_ident_continue(*b))
.unwrap_or(false);
if !next {
return true;
}
}
}
}
start = pos + 1;
}
false
}
fn contains_lit_with_op_word(text: &str, lit: &str, op: &str, word: &str) -> bool {
let bytes = text.as_bytes();
let lit_bytes = lit.as_bytes();
let mut start = 0;
while start + lit_bytes.len() <= bytes.len() {
let pos = match text[start..].find(lit) {
Some(p) => start + p,
None => break,
};
let prev_is_ident = if pos == 0 {
false
} else {
is_ident_continue(bytes[pos - 1])
};
let after = pos + lit_bytes.len();
let next_is_ident = bytes
.get(after)
.map(|b| is_ident_continue(*b))
.unwrap_or(false);
if !prev_is_ident && !next_is_ident {
let mut idx = after;
while idx < bytes.len() && (bytes[idx] == b' ' || bytes[idx] == b'\t') {
idx += 1;
}
if bytes[idx..].starts_with(op.as_bytes()) {
idx += op.len();
while idx < bytes.len() && (bytes[idx] == b' ' || bytes[idx] == b'\t') {
idx += 1;
}
if bytes[idx..].starts_with(word.as_bytes()) {
let word_end = idx + word.len();
let next = bytes
.get(word_end)
.map(|b| is_ident_continue(*b))
.unwrap_or(false);
if !next {
return true;
}
}
}
}
start = pos + 1;
}
false
}
fn is_ident_continue(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn body_matches_alias_null_check(body_text: &str, param_name: &str) -> bool {
let bytes = body_text.as_bytes();
let mut search_from = 0;
while search_from < bytes.len() {
let needle = format!("= {}", param_name);
let pos = match body_text[search_from..].find(&needle) {
Some(p) => search_from + p,
None => break,
};
search_from = pos + 1;
if pos > 0 {
let prev = bytes[pos - 1];
if prev == b'=' || prev == b'!' || prev == b'<' || prev == b'>' {
continue;
}
}
let after = pos + needle.len();
let next = bytes.get(after).copied().unwrap_or(0);
if is_ident_continue(next) {
continue;
}
if next != b';' && next != b',' && next != b')' && !next.is_ascii_whitespace() {
continue;
}
let mut end = pos;
while end > 0 && (bytes[end - 1] == b' ' || bytes[end - 1] == b'\t') {
end -= 1;
}
let lhs_end = end;
while end > 0 && is_ident_continue(bytes[end - 1]) {
end -= 1;
}
let lhs_start = end;
if lhs_start >= lhs_end {
continue;
}
let alias = &body_text[lhs_start..lhs_end];
if alias == param_name {
continue;
}
if !matches!(alias.as_bytes()[0], b'a'..=b'z' | b'A'..=b'Z' | b'_') {
continue;
}
if body_matches_null_check(body_text, alias) {
return true;
}
}
false
}
fn collect_param_passthroughs(
node: &Node,
source: &str,
params: &[String],
summary: &mut FunctionSummary,
) {
if node.kind() == "call_expression" {
if let Some(func_node) = node.child_by_field_name("function") {
let callee_name = func_node
.utf8_text(source.as_bytes())
.unwrap_or("")
.to_string();
if !callee_name.is_empty() && callee_name != "free" && callee_name != "realloc" {
if let Some(arguments) = node.child_by_field_name("arguments") {
let mut callee_idx = 0usize;
for i in 0..arguments.child_count() {
if let Some(arg) = arguments.child(i) {
if arg.kind() == "," || arg.kind() == "(" || arg.kind() == ")" {
continue;
}
if arg.kind() == "identifier" {
let arg_text = arg.utf8_text(source.as_bytes()).unwrap_or("");
for (param_idx, param_name) in params.iter().enumerate() {
if !param_name.is_empty() && arg_text == param_name {
summary
.param_passthroughs
.entry(param_idx)
.or_default()
.push((callee_name.clone(), callee_idx));
}
}
}
callee_idx += 1;
}
}
}
}
}
return; }
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
collect_param_passthroughs(&child, source, params, summary);
}
}
}
pub fn propagate_transitive_frees(summaries: &mut HashMap<String, FunctionSummary>) {
for _pass in 0..10 {
let mut changed = false;
let frees_snapshot: HashMap<String, HashSet<usize>> = summaries
.iter()
.map(|(n, s)| (n.clone(), s.frees_params.clone()))
.collect();
for summary in summaries.values_mut() {
for (caller_idx, callees) in &summary.param_passthroughs {
for (callee_name, callee_idx) in callees {
if let Some(callee_frees) = frees_snapshot.get(callee_name) {
if callee_frees.contains(callee_idx)
&& !summary.frees_params.contains(caller_idx)
{
summary.frees_params.insert(*caller_idx);
changed = true;
}
}
}
}
}
if !changed {
break;
}
}
}
fn collect_returns_from_callees(body: &Node, source: &str, out: &mut HashSet<String>) {
let mut returns = Vec::new();
collect_return_expressions(body, &mut returns);
for ret in returns {
let inner = unwrap_to_call_node(ret);
if inner.kind() == "call_expression" {
if let Some(func) = inner.child_by_field_name("function") {
let name = func.utf8_text(source.as_bytes()).unwrap_or("");
let ident = name
.rsplit(|c: char| !c.is_alphanumeric() && c != '_')
.next()
.unwrap_or(name);
if !ident.is_empty() {
out.insert(ident.to_string());
}
}
}
}
}
fn unwrap_to_call_node<'a>(mut node: Node<'a>) -> Node<'a> {
loop {
match node.kind() {
"parenthesized_expression" => {
if let Some(inner) = node.named_child(0) {
node = inner;
continue;
}
break;
}
"cast_expression" => {
if let Some(value) = node.child_by_field_name("value") {
node = value;
continue;
}
break;
}
_ => break,
}
}
node
}
pub fn propagate_return_taint(summaries: &mut HashMap<String, FunctionSummary>) {
for _pass in 0..10 {
let mut changed = false;
let snapshot: HashMap<String, bool> = summaries
.iter()
.map(|(n, s)| (n.clone(), s.returns_tainted))
.collect();
for summary in summaries.values_mut() {
if summary.returns_tainted {
continue;
}
for callee in &summary.returns_from_callees {
if let Some(&callee_tainted) = snapshot.get(callee) {
if callee_tainted {
summary.returns_tainted = true;
changed = true;
break;
}
}
}
}
if !changed {
break;
}
}
}
fn compute_return_range(
body: &Node,
source: &str,
macros: &MacroConstantMap,
) -> Option<ValueRange> {
let mut return_exprs = Vec::new();
collect_return_expressions(body, &mut return_exprs);
if return_exprs.is_empty() {
return None;
}
let empty_vars = VarRangeMap::new();
let mut combined: Option<ValueRange> = None;
for expr_node in &return_exprs {
let range = const_eval::try_evaluate_range(expr_node, source, macros, &empty_vars)?;
combined = Some(match combined {
Some(existing) => {
ValueRange::new(existing.min.min(range.min), existing.max.max(range.max))
}
None => range,
});
}
combined
}
fn collect_return_expressions<'a>(node: &Node<'a>, out: &mut Vec<Node<'a>>) {
if node.kind() == "return_statement" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() != "return" && child.kind() != ";" {
out.push(child);
return;
}
}
}
return;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
collect_return_expressions(&child, out);
}
}
}
pub fn extract_function_name(func_node: &Node, source: &str) -> Option<String> {
let declarator = func_node.child_by_field_name("declarator")?;
let name = extract_leaf_identifier(&declarator, source);
if name.is_empty() {
None
} else {
Some(name)
}
}
fn extract_leaf_identifier(node: &Node, source: &str) -> String {
match node.kind() {
"identifier" => node.utf8_text(source.as_bytes()).unwrap_or("").to_string(),
"function_declarator" | "pointer_declarator" | "array_declarator" => {
if let Some(inner) = node.child_by_field_name("declarator") {
extract_leaf_identifier(&inner, source)
} else {
String::new()
}
}
_ => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
return child.utf8_text(source.as_bytes()).unwrap_or("").to_string();
}
}
}
String::new()
}
}
}
pub fn infer_arg_null_state(arg: &Node, source: &str) -> NullState {
match arg.kind() {
"null" | "nullptr" => NullState::DefinitelyNull,
"number_literal" => {
let text = arg.utf8_text(source.as_bytes()).unwrap_or("").trim();
if text == "0" {
NullState::DefinitelyNull
} else {
NullState::NotNull
}
}
"string_literal" | "concatenated_string" | "char_literal" => NullState::NotNull,
"unary_expression" => {
if let Some(op) = arg.child_by_field_name("operator") {
if op.utf8_text(source.as_bytes()).unwrap_or("") == "&" {
return NullState::NotNull;
}
}
NullState::Unknown
}
"cast_expression" => {
if let Some(value) = arg.child_by_field_name("value") {
let inner = infer_arg_null_state(&value, source);
if inner == NullState::DefinitelyNull {
return NullState::DefinitelyNull;
}
}
NullState::Unknown
}
"parenthesized_expression" => {
if let Some(inner) = arg.child(1) {
return infer_arg_null_state(&inner, source);
}
NullState::Unknown
}
"identifier" => {
let text = arg.utf8_text(source.as_bytes()).unwrap_or("");
if text == "NULL" {
NullState::DefinitelyNull
} else if matches!(text, "stdout" | "stderr" | "stdin") {
NullState::NotNull
} else {
NullState::Unknown
}
}
_ => NullState::Unknown,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_and_summarize(code: &str) -> HashMap<String, FunctionSummary> {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_c::language()).unwrap();
let tree = parser.parse(code, None).unwrap();
let macros = const_eval::collect_macro_constants(&tree.root_node(), code);
compute_summaries(&tree.root_node(), code, ¯os, true, &[], &HashMap::new())
}
#[test]
fn test_never_returns() {
let code = r#"
void die(const char *msg) {
fprintf(stderr, "%s\n", msg);
abort();
}
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("die").unwrap();
assert!(summary.never_returns);
}
#[test]
fn test_frees_params() {
let code = r#"
void cleanup(void *ptr) {
free(ptr);
}
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("cleanup").unwrap();
assert!(summary.frees_params.contains(&0));
}
#[test]
fn test_can_return_null() {
let code = r#"
char *find_match(const char *haystack, const char *needle) {
char *result = strstr(haystack, needle);
if (!result) {
return NULL;
}
return result;
}
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("find_match").unwrap();
assert!(summary.can_return_null);
}
#[test]
fn test_checks_null_params() {
let code = r#"
int safe_strlen(const char *s) {
if (s == NULL) {
return 0;
}
return strlen(s);
}
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("safe_strlen").unwrap();
assert!(summary.checks_null_params.contains(&0));
}
#[test]
fn test_modifies_params() {
let code = r#"
void init_struct(struct config *cfg) {
cfg->value = 0;
cfg->name = "default";
}
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("init_struct").unwrap();
assert!(summary.modifies_params.contains(&0));
}
#[test]
fn test_returns_allocation() {
let code = r#"
char *create_buffer(size_t size) {
char *buf = malloc(size);
if (!buf) return NULL;
memset(buf, 0, size);
return buf;
}
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("create_buffer").unwrap();
assert!(summary.returns_allocation);
assert!(summary.can_return_null);
}
#[test]
fn test_return_range_constant() {
let code = r#"
int get_five(void) { return 5; }
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("get_five").unwrap();
assert_eq!(summary.return_range, Some(ValueRange::exact(5)));
}
#[test]
fn test_return_range_multiple_paths() {
let code = r#"
int get_bounded(int flag) {
if (flag) return 1;
return 10;
}
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("get_bounded").unwrap();
assert_eq!(summary.return_range, Some(ValueRange::new(1, 10)));
}
#[test]
fn test_return_range_void() {
let code = r#"
void do_nothing(void) { return; }
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("do_nothing").unwrap();
assert_eq!(summary.return_range, None);
}
#[test]
fn test_return_range_pointer() {
let code = r#"
int *get_ptr(void) { return 0; }
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("get_ptr").unwrap();
assert_eq!(summary.return_range, None);
}
#[test]
fn test_return_range_param_dependent() {
let code = r#"
int identity(int x) { return x; }
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("identity").unwrap();
assert_eq!(summary.return_range, None);
}
#[test]
fn test_return_range_macro() {
let code = r#"
#define MAX_COUNT 100
int get_max(void) { return MAX_COUNT; }
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("get_max").unwrap();
assert_eq!(summary.return_range, Some(ValueRange::exact(100)));
}
#[test]
fn test_return_range_zero() {
let code = r#"
int get_zero(void) { return 0; }
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("get_zero").unwrap();
assert_eq!(summary.return_range, Some(ValueRange::exact(0)));
}
#[test]
fn test_return_range_negative() {
let code = r#"
int get_error(void) { return -1; }
"#;
let summaries = parse_and_summarize(code);
let summary = summaries.get("get_error").unwrap();
assert_eq!(summary.return_range, Some(ValueRange::exact(-1)));
}
}