Skip to main content

cargo/diagnostics/rules/
text_direction_codepoint_in_comment.rs

1use std::path::Path;
2
3use cargo_util_terminal::report::AnnotationKind;
4use cargo_util_terminal::report::Group;
5use cargo_util_terminal::report::Level;
6use cargo_util_terminal::report::Snippet;
7use toml_parser::Source;
8use toml_parser::Span;
9use toml_parser::decoder::Encoding;
10use toml_parser::parser::Event;
11use toml_parser::parser::EventKind;
12use toml_parser::parser::EventReceiver;
13use tracing::instrument;
14
15use super::CORRECTNESS;
16use crate::CargoResult;
17use crate::GlobalContext;
18use crate::diagnostics::Lint;
19use crate::diagnostics::LintLevelProduct;
20use crate::diagnostics::ManifestFor;
21use crate::diagnostics::ScopedDiagnosticStats;
22use crate::diagnostics::workspace_rel_path;
23use crate::workspace::MaybePackage;
24use crate::workspace::Workspace;
25
26pub static LINT: &Lint = &Lint {
27    name: "text_direction_codepoint_in_comment",
28    primary_group: &CORRECTNESS,
29    msrv: Some(super::CARGO_LINTS_MSRV),
30    feature_gate: None,
31    docs: Some(
32        r#"
33### What it does
34Detects Unicode codepoints in manifest comments that change the visual representation of text on screen
35in a way that does not correspond to their on memory representation.
36
37### Why is this bad?
38Unicode allows changing the visual flow of text on screen
39in order to support scripts that are written right-to-left,
40but a specially crafted comment can make code that will be compiled appear to be part of a comment,
41depending on the software used to read the code.
42To avoid potential problems or confusion,
43such as in CVE-2021-42574,
44by default we deny their use.
45"#,
46    ),
47};
48
49#[instrument(skip_all)]
50pub(crate) fn lint_manifest(
51    ws: &Workspace<'_>,
52    manifest: ManifestFor<'_>,
53    manifest_path: &Path,
54    level: LintLevelProduct,
55    pkg_stats: &mut ScopedDiagnosticStats<'_>,
56    gctx: &GlobalContext,
57) -> CargoResult<()> {
58    let LintLevelProduct {
59        level: lint_level,
60        source,
61    } = level;
62
63    if matches!(
64        &manifest,
65        ManifestFor::Workspace {
66            maybe_pkg: MaybePackage::Package { .. },
67            ..
68        }
69    ) {
70        // For real manifests, lint as a package, rather than a workspace
71        return Ok(());
72    }
73
74    let Some(contents) = manifest.contents() else {
75        return Ok(());
76    };
77
78    let bidi_spans = contents
79        .char_indices()
80        .filter(|(_i, c)| {
81            UNICODE_BIDI_CODEPOINTS
82                .iter()
83                .any(|(bidi, _name)| c == bidi)
84        })
85        .map(|(i, c)| (i, i + c.len_utf8()))
86        .collect::<Vec<_>>();
87    if bidi_spans.is_empty() {
88        return Ok(());
89    }
90
91    let events = bidi_events(contents, &bidi_spans);
92    let manifest_path = workspace_rel_path(ws, manifest_path);
93    let mut emitted_source = None;
94    for event in events {
95        let token_span = event.token.span();
96        let token_span = token_span.start()..token_span.end();
97        let mut snippet = Snippet::source(contents).path(&manifest_path).annotation(
98            AnnotationKind::Context
99                .span(token_span)
100                .label("this comment contains an invisible unicode text flow control codepoint"),
101        );
102        for bidi_span in event.bidi_spans {
103            let bidi_span = bidi_span.0..bidi_span.1;
104            let escaped = format!("{:?}", &contents[bidi_span.clone()]);
105            snippet = snippet.annotation(AnnotationKind::Primary.span(bidi_span).label(escaped));
106        }
107
108        let level = lint_level.to_diagnostic_level();
109        let mut primary = Group::with_title(level.primary_title(
110            "unicode codepoint changing visible direction of text present in comment",
111        ))
112        .element(snippet);
113        if emitted_source.is_none() {
114            emitted_source = Some(LINT.emitted_source(lint_level, source));
115            primary = primary.element(Level::NOTE.message(emitted_source.as_ref().unwrap()));
116        }
117
118        let report = [primary];
119
120        pkg_stats.record_lint(lint_level);
121        gctx.shell().print_report(&report, lint_level.force())?;
122    }
123
124    Ok(())
125}
126
127const UNICODE_BIDI_CODEPOINTS: &[(char, &str)] = &[
128    ('\u{202A}', "LEFT-TO-RIGHT EMBEDDING"),
129    ('\u{202B}', "RIGHT-TO-LEFT EMBEDDING"),
130    ('\u{202C}', "POP DIRECTIONAL FORMATTING"),
131    ('\u{202D}', "LEFT-TO-RIGHT OVERRIDE"),
132    ('\u{202E}', "RIGHT-TO-LEFT OVERRIDE"),
133    ('\u{2066}', "LEFT-TO-RIGHT ISOLATE"),
134    ('\u{2067}', "RIGHT-TO-LEFT ISOLATE"),
135    ('\u{2068}', "FIRST STRONG ISOLATE"),
136    ('\u{2069}', "POP DIRECTIONAL ISOLATE"),
137];
138
139struct BiDiEvent {
140    token: Event,
141    bidi_spans: Vec<(usize, usize)>,
142}
143
144fn bidi_events(contents: &str, bidi_spans: &[(usize, usize)]) -> Vec<BiDiEvent> {
145    let mut bidi_spans = bidi_spans.iter();
146    let bidi_span = bidi_spans.next().copied();
147
148    let source = Source::new(contents);
149    let tokens = source.lex().into_vec();
150    let mut collector = BiDiCollector {
151        bidi_span,
152        bidi_spans,
153        events: Vec::new(),
154    };
155    let mut errors = ();
156    toml_parser::parser::parse_document(&tokens, &mut collector, &mut errors);
157
158    collector.events
159}
160
161struct BiDiCollector<'b> {
162    bidi_span: Option<(usize, usize)>,
163    bidi_spans: std::slice::Iter<'b, (usize, usize)>,
164    events: Vec<BiDiEvent>,
165}
166
167impl BiDiCollector<'_> {
168    fn process(&mut self, kind: EventKind, encoding: Option<Encoding>, span: Span) {
169        let mut event_bidi_spans = Vec::new();
170        while let Some(bidi_span) = self.bidi_span {
171            if bidi_span.0 < span.start() {
172                self.bidi_span = self.bidi_spans.next().copied();
173                continue;
174            } else if span.end() <= bidi_span.0 {
175                break;
176            }
177
178            event_bidi_spans.push(bidi_span);
179            self.bidi_span = self.bidi_spans.next().copied();
180        }
181
182        if !event_bidi_spans.is_empty() {
183            let token = Event::new_unchecked(kind, encoding, span);
184            self.events.push(BiDiEvent {
185                token,
186                bidi_spans: event_bidi_spans,
187            });
188        }
189    }
190}
191
192impl EventReceiver for BiDiCollector<'_> {
193    fn comment(&mut self, span: Span, _error: &mut dyn toml_parser::ErrorSink) {
194        self.process(EventKind::Comment, None, span)
195    }
196}