Skip to main content

forge_lint/sol/high/
rtlo.rs

1use super::Rtlo;
2use crate::{
3    linter::{EarlyLintPass, Lint, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast,
8    interface::{BytePos, Span},
9};
10
11declare_forge_lint!(
12    RTLO,
13    Severity::High,
14    "rtlo",
15    "unicode bidirectional override character can hide malicious code"
16);
17
18impl<'ast> EarlyLintPass<'ast> for Rtlo {
19    fn check_full_source_unit(
20        &mut self,
21        ctx: &LintContext<'ast, '_>,
22        _unit: &'ast ast::SourceUnit<'ast>,
23    ) {
24        if !ctx.is_lint_enabled(RTLO.id()) {
25            return;
26        }
27
28        // Scan the raw source so bidi chars in comments are also caught.
29        let Some(file) = ctx.source_file() else { return };
30
31        for (offset, ch) in file.src.char_indices() {
32            let Some(name) = bidi_char_name(ch) else { continue };
33
34            let lo = file.start_pos + BytePos::from_usize(offset);
35            let hi = lo + BytePos::from_usize(ch.len_utf8());
36            let span = Span::new(lo, hi);
37
38            ctx.emit_with_msg(&RTLO, span, format!("U+{:04X} ({name}) detected", ch as u32));
39        }
40    }
41}
42
43const fn bidi_char_name(ch: char) -> Option<&'static str> {
44    Some(match ch {
45        '\u{200E}' => "Left-to-Right Mark",
46        '\u{200F}' => "Right-to-Left Mark",
47        '\u{202A}' => "Left-to-Right Embedding",
48        '\u{202B}' => "Right-to-Left Embedding",
49        '\u{202C}' => "Pop Directional Formatting",
50        '\u{202D}' => "Left-to-Right Override",
51        '\u{202E}' => "Right-to-Left Override",
52        '\u{2066}' => "Left-to-Right Isolate",
53        '\u{2067}' => "Right-to-Left Isolate",
54        '\u{2068}' => "First Strong Isolate",
55        '\u{2069}' => "Pop Directional Isolate",
56        _ => return None,
57    })
58}