use super::super::{CertRule, RuleViolation};
use crate::analyze::cfg;
use crate::analyze::const_eval::{self, MacroConstantMap};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use lang_parsing_substrate::query;
use std::collections::HashSet;
use tree_sitter::Node;
const UTF8_LEAD_MASKS: [i64; 5] = [0xc0, 0xe0, 0xf0, 0xf8, 0xfc];
const MIN_DISTINCT_LEAD_MASKS: usize = 3;
const OVERLONG_CHECK_CONSTANTS: [i64; 9] = [
0x800, 0x10000, 0x110000, 0xc1, 0xa0, 0x90, 0xd800, 0xdfff, 0xffff, ];
const OVERLONG_CHECK_WORDS: [&str; 6] = [
"overlong",
"shortest",
"non-minimal",
"nonminimal",
"minimal form",
"canonical",
];
#[derive(Debug)]
pub struct Msc10C;
impl Msc10C {
#[allow(dead_code)]
pub fn new() -> Self {
Msc10C
}
fn collect_constants(
&self,
body: &Node,
source: &str,
macros: &MacroConstantMap,
) -> (HashSet<i64>, HashSet<i64>) {
let mut all_constants: HashSet<i64> = HashSet::new();
let mut mask_constants: HashSet<i64> = HashSet::new();
for expr in query::find_descendants_of_kind(*body, "binary_expression") {
let (Some(left), Some(right)) = (
expr.child_by_field_name("left"),
expr.child_by_field_name("right"),
) else {
continue;
};
let left_val =
const_eval::try_evaluate_text_public(get_node_text(&left, source), macros);
let right_val =
const_eval::try_evaluate_text_public(get_node_text(&right, source), macros);
if self.operator_is_bitand(&expr, source) {
for val in [left_val, right_val].into_iter().flatten() {
mask_constants.insert(val);
}
}
for val in [left_val, right_val].into_iter().flatten() {
all_constants.insert(val);
}
}
for lit in query::find_descendants_of_kind(*body, "number_literal") {
if let Some(val) =
const_eval::try_evaluate_text_public(get_node_text(&lit, source), macros)
{
all_constants.insert(val);
}
}
(all_constants, mask_constants)
}
fn operator_is_bitand(&self, expr: &Node, source: &str) -> bool {
for i in 0..expr.child_count() {
if let Some(child) = expr.child(i) {
if !child.is_named() && get_node_text(&child, source).trim() == "&" {
return true;
}
}
}
false
}
fn looks_like_utf8_decoder(
&self,
all_constants: &HashSet<i64>,
mask_constants: &HashSet<i64>,
) -> bool {
let distinct_lead_masks = UTF8_LEAD_MASKS
.iter()
.filter(|m| mask_constants.contains(m))
.count();
distinct_lead_masks >= MIN_DISTINCT_LEAD_MASKS && all_constants.contains(&0x80)
}
fn has_overlong_rejection(&self, all_constants: &HashSet<i64>, body_text: &str) -> bool {
if OVERLONG_CHECK_CONSTANTS
.iter()
.any(|c| all_constants.contains(c))
{
return true;
}
let lowered = body_text.to_lowercase();
OVERLONG_CHECK_WORDS.iter().any(|w| lowered.contains(w))
}
}
impl CertRule for Msc10C {
fn rule_id(&self) -> &'static str {
"MSC10-C"
}
fn description(&self) -> &'static str {
"Character encoding: UTF8-related issues"
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn cert_id(&self) -> &'static str {
"MSC10-C"
}
fn scan(&self, root: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
let macros = const_eval::collect_macro_constants(root, source);
for func in query::find_descendants_of_kind(*root, "function_definition") {
let Some(body) = func.child_by_field_name("body") else {
continue;
};
let (all_constants, mask_constants) = self.collect_constants(&body, source, ¯os);
if !self.looks_like_utf8_decoder(&all_constants, &mask_constants) {
continue;
}
if self.has_overlong_rejection(&all_constants, get_node_text(&func, source)) {
continue;
}
let name = cfg::get_function_name(&func, source).unwrap_or("<anonymous>");
let pos = func.start_position();
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: Severity::Medium,
message: format!(
"'{}' validates UTF-8 sequence structure but never rejects non-shortest (overlong) encodings -- 'C0 80' would be accepted as U+0000",
name
),
file_path: String::new(),
line: pos.row + 1,
column: pos.column + 1,
suggestion: Some(
"Reject non-shortest forms: verify the decoded code point meets the minimum for its byte length (>= 0x80 for 2 bytes, >= 0x800 for 3, >= 0x10000 for 4), or use a vetted UTF-8 decoder"
.to_string(),
),
requires_manual_review: Some(false),
});
}
}
}