use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use std::collections::HashMap;
use tree_sitter::Node;
pub struct Fio13C;
impl CertRule for Fio13C {
fn rule_id(&self) -> &'static str {
"FIO13-C"
}
fn description(&self) -> &'static str {
"Never attempt to pushback more than one character via successive calls to ungetc()"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn cert_id(&self) -> &'static str {
self.rule_id()
}
fn check(&self, root: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let mut file_operations: HashMap<String, Vec<FileOp>> = HashMap::new();
self.collect_operations(root, source, &mut file_operations);
for (file_ptr, ops) in file_operations {
let mut last_ungetc: Option<usize> = None;
for op in ops {
match op {
FileOp::Ungetc(line) => {
if let Some(prev_line) = last_ungetc {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: self.severity(),
message: format!(
"Multiple successive ungetc() calls on '{}' without intervening read (previous ungetc at line {})",
file_ptr, prev_line + 1
),
file_path: String::new(),
line: line + 1,
column: 0,
suggestion: Some("Insert a read operation (fgetc, fgets, etc.) between ungetc() calls".to_string()),
..Default::default()
});
}
last_ungetc = Some(line);
}
FileOp::Read(_) => {
last_ungetc = None;
}
}
}
}
violations
}
}
impl Fio13C {
fn collect_operations(
&self,
node: &Node,
source: &str,
file_operations: &mut HashMap<String, Vec<FileOp>>,
) {
if node.kind() == "call_expression" {
if let Some(func_node) = node.child_by_field_name("function") {
let func_name = get_node_text(&func_node, source);
if func_name == "ungetc" {
if let Some(args) = node.child_by_field_name("arguments") {
if let Some(file_arg) = self.get_nth_argument(&args, 1, source) {
let entry = file_operations.entry(file_arg.clone()).or_default();
entry.push(FileOp::Ungetc(node.start_position().row));
}
}
} else if self.is_read_function(&func_name) {
if let Some(args) = node.child_by_field_name("arguments") {
if let Some(file_arg) = self.get_file_argument(&args, &func_name, source) {
let entry = file_operations.entry(file_arg.clone()).or_default();
entry.push(FileOp::Read(node.start_position().row));
}
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.collect_operations(&child, source, file_operations);
}
}
fn get_nth_argument(&self, args_node: &Node, n: usize, source: &str) -> Option<String> {
let mut count = 0;
let mut cursor = args_node.walk();
for child in args_node.children(&mut cursor) {
if child.kind() != "(" && child.kind() != ")" && child.kind() != "," {
if count == n {
return Some(get_node_text(&child, source).trim().to_string());
}
count += 1;
}
}
None
}
fn get_file_argument(&self, args_node: &Node, func_name: &str, source: &str) -> Option<String> {
match func_name {
"fgetc" | "fgets" | "fread" | "fscanf" | "fgetpos" => {
let mut last_arg = None;
let mut cursor = args_node.walk();
for child in args_node.children(&mut cursor) {
if child.kind() != "(" && child.kind() != ")" && child.kind() != "," {
last_arg = Some(get_node_text(&child, source).trim().to_string());
}
}
last_arg
}
_ => self.get_nth_argument(args_node, 0, source),
}
}
fn is_read_function(&self, func_name: &str) -> bool {
matches!(
func_name,
"fgetc" | "fgets" | "fread" | "fscanf" | "getc" | "fgetpos" | "fsetpos"
)
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum FileOp {
Ungetc(usize), Read(usize), }