use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use std::collections::{HashMap, HashSet};
use tree_sitter::Node;
pub struct Fio42C;
impl CertRule for Fio42C {
fn rule_id(&self) -> &'static str {
"FIO42-C"
}
fn description(&self) -> &'static str {
"Close files when they are no longer needed"
}
fn severity(&self) -> Severity {
Severity::High
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn cert_id(&self) -> &'static str {
"FIO42-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let mut tracker = FileResourceTracker::new();
tracker.analyze_node(node, source, &mut violations);
violations
}
}
struct FileResourceTracker {
file_pointers: HashMap<String, ResourceInfo>,
file_descriptors: HashMap<String, ResourceInfo>,
file_handles: HashMap<String, ResourceInfo>,
closed_resources: HashSet<String>,
}
#[derive(Clone)]
#[allow(dead_code)]
struct ResourceInfo {
var_name: String,
resource_type: ResourceType,
line: usize,
column: usize,
}
#[derive(Clone, PartialEq)]
#[allow(clippy::enum_variant_names)]
enum ResourceType {
FilePointer, FileDescriptor, FileHandle, }
impl FileResourceTracker {
fn new() -> Self {
Self {
file_pointers: HashMap::new(),
file_descriptors: HashMap::new(),
file_handles: HashMap::new(),
closed_resources: HashSet::new(),
}
}
fn analyze_node(&mut self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
if node.kind() == "function_definition" {
self.analyze_function(node, source, violations);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.analyze_node(&child, source, violations);
}
}
}
fn analyze_function(
&mut self,
func_node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
) {
self.file_pointers.clear();
self.file_descriptors.clear();
self.file_handles.clear();
self.closed_resources.clear();
if let Some(body) = func_node.child_by_field_name("body") {
self.collect_resources(&body, source);
self.collect_closes(&body, source);
self.check_unclosed_resources(violations);
self.check_temp_file_cleanup(&body, source, violations);
}
}
fn collect_resources(&mut self, node: &Node, source: &str) {
match node.kind() {
"declaration" => {
self.check_file_pointer_declaration(node, source);
}
"assignment_expression" => {
self.check_file_pointer_assignment(node, source);
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.collect_resources(&child, source);
}
}
}
fn check_file_pointer_declaration(&mut self, node: &Node, source: &str) {
let decl_text = get_node_text(node, source);
if decl_text.contains("FILE") && decl_text.contains("*") {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "init_declarator" {
if let Some(value) = child.child_by_field_name("value") {
let value_text = get_node_text(&value, source);
if value_text.contains("fopen") || value_text.contains("freopen") {
if let Some(var_name) = self.extract_declarator_name(&child, source)
{
self.file_pointers.insert(
var_name.clone(),
ResourceInfo {
var_name,
resource_type: ResourceType::FilePointer,
line: node.start_position().row + 1,
column: node.start_position().column + 1,
},
);
}
}
}
}
}
}
}
if decl_text.contains("int") && decl_text.contains("open(") {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "init_declarator" {
if let Some(value) = child.child_by_field_name("value") {
let value_text = get_node_text(&value, source);
if value_text.contains("open(") {
if let Some(var_name) = self.extract_declarator_name(&child, source)
{
self.file_descriptors.insert(
var_name.clone(),
ResourceInfo {
var_name,
resource_type: ResourceType::FileDescriptor,
line: node.start_position().row + 1,
column: node.start_position().column + 1,
},
);
}
}
}
}
}
}
}
if decl_text.contains("HANDLE") && decl_text.contains("CreateFile") {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "init_declarator" {
if let Some(value) = child.child_by_field_name("value") {
let value_text = get_node_text(&value, source);
if value_text.contains("CreateFile") {
if let Some(var_name) = self.extract_declarator_name(&child, source)
{
self.file_handles.insert(
var_name.clone(),
ResourceInfo {
var_name,
resource_type: ResourceType::FileHandle,
line: node.start_position().row + 1,
column: node.start_position().column + 1,
},
);
}
}
}
}
}
}
}
}
fn check_file_pointer_assignment(&mut self, node: &Node, source: &str) {
if let (Some(left), Some(right)) = (
node.child_by_field_name("left"),
node.child_by_field_name("right"),
) {
let right_text = get_node_text(&right, source);
let var_name = get_node_text(&left, source).to_string();
if right_text.contains("fopen") || right_text.contains("freopen") {
self.file_pointers.insert(
var_name.clone(),
ResourceInfo {
var_name: var_name.clone(),
resource_type: ResourceType::FilePointer,
line: node.start_position().row + 1,
column: node.start_position().column + 1,
},
);
}
if right_text.contains("open(") {
self.file_descriptors.insert(
var_name.clone(),
ResourceInfo {
var_name: var_name.clone(),
resource_type: ResourceType::FileDescriptor,
line: node.start_position().row + 1,
column: node.start_position().column + 1,
},
);
}
if right_text.contains("CreateFile") {
self.file_handles.insert(
var_name.clone(),
ResourceInfo {
var_name: var_name.clone(),
resource_type: ResourceType::FileHandle,
line: node.start_position().row + 1,
column: node.start_position().column + 1,
},
);
}
}
}
fn collect_closes(&mut self, node: &Node, source: &str) {
if node.kind() == "call_expression" {
if let Some(function) = node.child_by_field_name("function") {
let func_name = get_node_text(&function, source);
if func_name == "fclose" {
if let Some(args) = node.child_by_field_name("arguments") {
let args_text = get_node_text(&args, source);
let var_name = args_text.trim_matches(|c| c == '(' || c == ')').trim();
self.closed_resources.insert(var_name.to_string());
}
}
if func_name == "close" {
if let Some(args) = node.child_by_field_name("arguments") {
let args_text = get_node_text(&args, source);
let var_name = args_text.trim_matches(|c| c == '(' || c == ')').trim();
self.closed_resources.insert(var_name.to_string());
}
}
if func_name == "CloseHandle" {
if let Some(args) = node.child_by_field_name("arguments") {
let args_text = get_node_text(&args, source);
let var_name = args_text.trim_matches(|c| c == '(' || c == ')').trim();
self.closed_resources.insert(var_name.to_string());
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.collect_closes(&child, source);
}
}
}
fn check_unclosed_resources(&self, violations: &mut Vec<RuleViolation>) {
for (var_name, info) in &self.file_pointers {
if !self.closed_resources.contains(var_name) {
violations.push(RuleViolation {
rule_id: "FIO42-C".to_string(),
message: format!(
"FILE pointer '{}' opened with fopen/freopen but never closed with fclose()",
var_name
),
severity: Severity::High,
line: info.line,
column: info.column,
file_path: String::new(),
suggestion: Some(format!(
"Add fclose({}) before function returns or program exits",
var_name
)),
requires_manual_review: None,
});
}
}
for (var_name, info) in &self.file_descriptors {
if !self.closed_resources.contains(var_name) {
violations.push(RuleViolation {
rule_id: "FIO42-C".to_string(),
message: format!(
"File descriptor '{}' opened with open() but never closed with close()",
var_name
),
severity: Severity::High,
line: info.line,
column: info.column,
file_path: String::new(),
suggestion: Some(format!(
"Add close({}) before function returns or program exits",
var_name
)),
requires_manual_review: None,
});
}
}
for (var_name, info) in &self.file_handles {
if !self.closed_resources.contains(var_name) {
violations.push(RuleViolation {
rule_id: "FIO42-C".to_string(),
message: format!(
"File HANDLE '{}' opened with CreateFile() but never closed with CloseHandle()",
var_name
),
severity: Severity::High,
line: info.line,
column: info.column,
file_path: String::new(),
suggestion: Some(format!(
"Add CloseHandle({}) before function returns or program exits",
var_name
)),
requires_manual_review: None,
});
}
}
}
fn extract_declarator_name(&self, node: &Node, source: &str) -> Option<String> {
if let Some(declarator) = node.child_by_field_name("declarator") {
return self.find_identifier(&declarator, source);
}
None
}
fn find_identifier(&self, node: &Node, source: &str) -> Option<String> {
if node.kind() == "identifier" {
return Some(get_node_text(node, source).to_string());
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if let Some(name) = self.find_identifier(&child, source) {
return Some(name);
}
}
}
None
}
fn check_temp_file_cleanup(
&self,
body: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
) {
let mut temp_creations: Vec<(usize, usize)> = Vec::new();
let mut has_cleanup = false;
self.scan_temp_file_calls(body, source, &mut temp_creations, &mut has_cleanup);
if !has_cleanup {
for (line, col) in &temp_creations {
violations.push(RuleViolation {
rule_id: "FIO42-C".to_string(),
message:
"Temporary file created but never deleted (missing unlink/remove call)"
.to_string(),
severity: Severity::Medium,
line: *line,
column: *col,
file_path: String::new(),
suggestion: Some(
"Call unlink() or remove() on the temporary file before function returns"
.to_string(),
),
requires_manual_review: None,
});
}
}
}
fn scan_temp_file_calls(
&self,
node: &Node,
source: &str,
temp_creations: &mut Vec<(usize, usize)>,
has_cleanup: &mut bool,
) {
if node.kind() == "call_expression" {
if let Some(func) = node.child_by_field_name("function") {
let name = get_node_text(&func, source).trim().to_string();
match name.as_str() {
"mkstemp" | "MKSTEMP" | "_mkstemp" | "mktemp" | "MKTEMP" | "_wmktemp"
| "mkdtemp" | "tmpnam" => {
temp_creations.push((
node.start_position().row + 1,
node.start_position().column + 1,
));
}
"unlink" | "UNLINK" | "_unlink" | "_wunlink" | "remove" | "DeleteFile"
| "DeleteFileA" | "DeleteFileW" => {
*has_cleanup = true;
}
_ => {}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.scan_temp_file_calls(&child, source, temp_creations, has_cleanup);
}
}
}
}