use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::{get_identifier_from_declarator, get_node_text};
use lang_parsing_substrate::query;
use tree_sitter::Node;
const STREAM_READ_FNS: &[&str] = &["fgetc", "getc", "fgetwc", "getwc"];
#[derive(Debug)]
pub struct Msc23C;
impl Msc23C {
#[allow(dead_code)]
pub fn new() -> Self {
Msc23C
}
fn assigned_variable(&self, call: &Node, source: &str) -> Option<String> {
let parent = call.parent()?;
match parent.kind() {
"init_declarator" => {
let declarator = parent.child_by_field_name("declarator")?;
let name = get_identifier_from_declarator(&declarator, source);
(!name.is_empty()).then_some(name)
}
"assignment_expression" => {
let left = parent.child_by_field_name("left")?;
(left.kind() == "identifier").then(|| get_node_text(&left, source).to_string())
}
_ => None,
}
}
fn mode_literal<'a>(&self, call: &Node<'a>, source: &str) -> Option<(Node<'a>, String)> {
let args = call.child_by_field_name("arguments")?;
let mode_arg = args.named_child(1)?;
if mode_arg.kind() != "string_literal" {
return None;
}
let text = get_node_text(&mode_arg, source);
Some((mode_arg, text.trim_matches('"').to_string()))
}
fn call_references_var(&self, call: &Node, var_name: &str, source: &str) -> bool {
let Some(args) = call.child_by_field_name("arguments") else {
return false;
};
(0..args.named_child_count()).any(|i| {
args.named_child(i)
.is_some_and(|a| a.kind() == "identifier" && get_node_text(&a, source) == var_name)
})
}
fn is_stream_read_of(&self, node: &Node, var_name: &str, source: &str) -> bool {
node.kind() == "call_expression"
&& node
.child_by_field_name("function")
.is_some_and(|f| STREAM_READ_FNS.contains(&get_node_text(&f, source)))
&& self.call_references_var(node, var_name, source)
}
fn is_bare_counter_increment(&self, stmt: &Node, var_name: &str, source: &str) -> bool {
if stmt.kind() != "expression_statement" {
return false;
}
let Some(expr) = stmt.named_child(0) else {
return false;
};
expr.kind() == "update_expression"
&& expr.child_by_field_name("argument").is_some_and(|arg| {
arg.kind() == "identifier" && get_node_text(&arg, source) != var_name
})
}
fn top_level_statements<'a>(&self, loop_body: &Node<'a>) -> Vec<Node<'a>> {
if loop_body.kind() != "compound_statement" {
return vec![*loop_body];
}
(0..loop_body.child_count())
.filter_map(|i| loop_body.child(i))
.filter(|c| !matches!(c.kind(), "{" | "}"))
.collect()
}
fn has_byte_counting_loop(&self, body: &Node, var_name: &str, source: &str) -> bool {
let loop_kinds = ["while_statement", "for_statement", "do_statement"];
query::find_descendants_of_kinds(*body, &loop_kinds)
.into_iter()
.filter_map(|loop_node| loop_node.child_by_field_name("body"))
.any(|loop_body| {
let stmts = self.top_level_statements(&loop_body);
let reads = stmts.iter().any(|s| {
query::find_first_descendant(*s, |n| {
self.is_stream_read_of(&n, var_name, source)
})
.is_some()
});
let counts = stmts
.iter()
.any(|s| self.is_bare_counter_increment(s, var_name, source));
reads && counts
})
}
}
impl CertRule for Msc23C {
fn rule_id(&self) -> &'static str {
"MSC23-C"
}
fn description(&self) -> &'static str {
"Beware of vendor-specific library and language differences"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"MSC23-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
for func in query::find_descendants_of_kind(*node, "function_definition") {
let Some(body) = func.child_by_field_name("body") else {
continue;
};
for call in query::find_descendants_of_kind(body, "call_expression") {
let Some(func_name_node) = call.child_by_field_name("function") else {
continue;
};
let func_name = get_node_text(&func_name_node, source);
if func_name != "fopen" && func_name != "freopen" {
continue;
}
let Some((mode_node, mode)) = self.mode_literal(&call, source) else {
continue;
};
if mode.contains('b') || !mode.contains('r') {
continue;
}
let Some(var_name) = self.assigned_variable(&call, source) else {
continue;
};
if self.has_byte_counting_loop(&body, &var_name, source) {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: self.severity(),
message: format!(
"'{}' is opened in text mode (\"{}\") and then read byte-by-byte in a loop that counts bytes; vendor-specific newline translation (e.g. CRLF -> LF) can make the count differ from the file's actual size.",
var_name, mode
),
file_path: String::new(),
line: mode_node.start_position().row + 1,
column: mode_node.start_position().column + 1,
suggestion: Some(format!(
"Open '{}' in binary mode (\"{}b\") if an exact byte count is required.",
var_name, mode
)),
..Default::default()
});
}
}
}
violations
}
}