use crate::manifest::{RuleCategory, Severity};
use crate::rules::{CertRule, RuleViolation};
use crate::utility::cert_c::ast_utils::get_node_text;
use lang_parsing_substrate::query;
use streaming_iterator::StreamingIterator;
use tree_sitter::{Node, Query, QueryCursor};
pub struct Exp42C;
impl CertRule for Exp42C {
fn rule_id(&self) -> &'static str {
"EXP42-C"
}
fn description(&self) -> &'static str {
"Do not compare padding data"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn cert_id(&self) -> &'static str {
"EXP42-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let query_str = r#"
(call_expression
function: (identifier) @func_name
arguments: (argument_list) @args
) @call
"#;
let language = crate::parser::c_language();
let query = Query::new(&language, query_str).expect("Invalid query");
let mut query_cursor = QueryCursor::new();
let mut matches = query_cursor.matches(&query, *node, source.as_bytes());
while let Some(m) = matches.next() {
let mut func_name_node = None;
let mut args_node = None;
let mut call_node = None;
for capture in m.captures {
let capture_name = &query.capture_names()[capture.index as usize];
match &**capture_name {
"func_name" => func_name_node = Some(capture.node),
"args" => args_node = Some(capture.node),
"call" => call_node = Some(capture.node),
_ => {}
}
}
if let (Some(func_node), Some(args), Some(call)) =
(func_name_node, args_node, call_node)
{
let func_name = get_node_text(&func_node, source);
if func_name == "memcmp" || func_name == "memcmp_s" {
if is_struct_comparison(&args, source)
&& !is_packed_struct_comparison(node, &args, source)
{
let start_pos = call.start_position();
violations.push(RuleViolation {
rule_id: "EXP42-C".to_string(),
severity: Severity::Medium,
message: format!(
"Comparing padding data using {}(). Padding bytes in structures have indeterminate values and should not be compared. Consider comparing struct members individually instead, or use #pragma pack to eliminate padding.",
func_name
),
file_path: String::new(), line: start_pos.row + 1,
column: start_pos.column + 1,
suggestion: Some(
"Compare struct members individually instead of using memcmp()".to_string()
),
requires_manual_review: None,
});
}
}
}
}
violations
}
}
fn is_struct_comparison(args_node: &Node, source: &str) -> bool {
let mut cursor = args_node.walk();
let mut arguments = Vec::new();
for child in args_node.children(&mut cursor) {
if child.kind() != "," && child.kind() != "(" && child.kind() != ")" {
arguments.push(child);
}
}
if arguments.len() < 3 {
return false;
}
let size_arg = arguments[2];
if has_sizeof_struct(&size_arg, source) {
return true;
}
for i in 0..2 {
if i < arguments.len() && looks_like_struct_pointer(&arguments[i], source) {
if looks_like_sizeof_usage(&size_arg, source) {
return true;
}
}
}
false
}
fn is_packed_struct_comparison(root: &Node, args_node: &Node, source: &str) -> bool {
let Some(struct_name) = extract_struct_type_name(args_node, source) else {
return false;
};
let Some(struct_def) = query::find_first_descendant(*root, |n| {
n.kind() == "struct_specifier"
&& n.child_by_field_name("body").is_some()
&& n.child_by_field_name("name")
.map(|name| get_node_text(&name, source) == struct_name)
.unwrap_or(false)
}) else {
return false;
};
is_pack_one_active_before(root, struct_def.start_byte(), source)
}
fn extract_struct_type_name(args_node: &Node, source: &str) -> Option<String> {
query::find_first_descendant(*args_node, |n| n.kind() == "struct_specifier").and_then(
|struct_ref| {
struct_ref
.child_by_field_name("name")
.map(|name| get_node_text(&name, source).to_string())
},
)
}
fn is_pack_one_active_before(root: &Node, byte_pos: usize, source: &str) -> bool {
let mut active = false;
for pc in query::find_descendants_of_kind(*root, "preproc_call") {
if pc.start_byte() >= byte_pos {
break;
}
let text = get_node_text(&pc, source);
if !text.contains("pack") {
continue;
}
if text.contains("pop") {
active = false;
} else if text.contains('1') {
active = true;
}
}
active
}
fn has_sizeof_struct(node: &Node, source: &str) -> bool {
let text = get_node_text(node, source);
if text.contains("sizeof") && text.contains("struct") {
return true;
}
query::find_first_descendant(*node, |n| {
if n.kind() != "sizeof_expression" {
return false;
}
let sizeof_text = get_node_text(&n, source);
if sizeof_text.contains("struct") {
return true;
}
let mut cursor = n.walk();
let has_struct_child = n
.children(&mut cursor)
.any(|c| c.kind() == "struct_specifier" || c.kind() == "type_identifier");
has_struct_child
})
.is_some()
}
fn looks_like_sizeof_usage(node: &Node, source: &str) -> bool {
let text = get_node_text(node, source);
text.contains("sizeof")
}
fn looks_like_struct_pointer(node: &Node, source: &str) -> bool {
query::find_first_descendant(*node, |n| {
if n.kind() == "cast_expression" {
let text = get_node_text(&n, source);
if text.contains("struct") {
return true;
}
}
if n.kind() == "unary_expression" {
let text = get_node_text(&n, source);
if text.starts_with('&') {
return true;
}
}
if n.kind() == "pointer_expression" {
return true;
}
false
})
.is_some()
}