forge_lint/sol/high/
rtlo.rs1use 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 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}