| use std::path::Path; |
| |
| use cargo_util_terminal::report::AnnotationKind; |
| use cargo_util_terminal::report::Group; |
| use cargo_util_terminal::report::Level; |
| use cargo_util_terminal::report::Snippet; |
| use toml_parser::Source; |
| use toml_parser::Span; |
| use toml_parser::decoder::Encoding; |
| use toml_parser::parser::Event; |
| use toml_parser::parser::EventKind; |
| use toml_parser::parser::EventReceiver; |
| use tracing::instrument; |
| |
| use super::CORRECTNESS; |
| use crate::CargoResult; |
| use crate::GlobalContext; |
| use crate::diagnostics::Lint; |
| use crate::diagnostics::LintLevelProduct; |
| use crate::diagnostics::ManifestFor; |
| use crate::diagnostics::ScopedDiagnosticStats; |
| use crate::diagnostics::workspace_rel_path; |
| use crate::workspace::MaybePackage; |
| use crate::workspace::Workspace; |
| |
| pub static LINT: &Lint = &Lint { |
| name: "text_direction_codepoint_in_comment", |
| desc: "unicode codepoint changing visible direction of text present in comment", |
| primary_group: &CORRECTNESS, |
| msrv: Some(super::CARGO_LINTS_MSRV), |
| feature_gate: None, |
| docs: Some( |
| r#" |
| ### What it does |
| Detects Unicode codepoints in manifest comments that change the visual representation of text on screen |
| in a way that does not correspond to their on memory representation. |
| |
| ### Why is this bad? |
| Unicode allows changing the visual flow of text on screen |
| in order to support scripts that are written right-to-left, |
| but a specially crafted comment can make code that will be compiled appear to be part of a comment, |
| depending on the software used to read the code. |
| To avoid potential problems or confusion, |
| such as in CVE-2021-42574, |
| by default we deny their use. |
| "#, |
| ), |
| }; |
| |
| #[instrument(skip_all)] |
| pub(crate) fn lint_manifest( |
| ws: &Workspace<'_>, |
| manifest: ManifestFor<'_>, |
| manifest_path: &Path, |
| level: LintLevelProduct, |
| pkg_stats: &mut ScopedDiagnosticStats<'_>, |
| gctx: &GlobalContext, |
| ) -> CargoResult<()> { |
| let LintLevelProduct { |
| level: lint_level, |
| source, |
| } = level; |
| |
| if matches!( |
| &manifest, |
| ManifestFor::Workspace { |
| maybe_pkg: MaybePackage::Package { .. }, |
| .. |
| } |
| ) { |
| // For real manifests, lint as a package, rather than a workspace |
| return Ok(()); |
| } |
| |
| let Some(contents) = manifest.contents() else { |
| return Ok(()); |
| }; |
| |
| let bidi_spans = contents |
| .char_indices() |
| .filter(|(_i, c)| { |
| UNICODE_BIDI_CODEPOINTS |
| .iter() |
| .any(|(bidi, _name)| c == bidi) |
| }) |
| .map(|(i, c)| (i, i + c.len_utf8())) |
| .collect::<Vec<_>>(); |
| if bidi_spans.is_empty() { |
| return Ok(()); |
| } |
| |
| let events = bidi_events(contents, &bidi_spans); |
| let manifest_path = workspace_rel_path(ws, manifest_path); |
| let mut emitted_source = None; |
| for event in events { |
| let token_span = event.token.span(); |
| let token_span = token_span.start()..token_span.end(); |
| let mut snippet = Snippet::source(contents).path(&manifest_path).annotation( |
| AnnotationKind::Context |
| .span(token_span) |
| .label("this comment contains an invisible unicode text flow control codepoint"), |
| ); |
| for bidi_span in event.bidi_spans { |
| let bidi_span = bidi_span.0..bidi_span.1; |
| let escaped = format!("{:?}", &contents[bidi_span.clone()]); |
| snippet = snippet.annotation(AnnotationKind::Primary.span(bidi_span).label(escaped)); |
| } |
| |
| let level = lint_level.to_diagnostic_level(); |
| let mut primary = Group::with_title(level.primary_title(LINT.desc)).element(snippet); |
| if emitted_source.is_none() { |
| emitted_source = Some(LINT.emitted_source(lint_level, source)); |
| primary = primary.element(Level::NOTE.message(emitted_source.as_ref().unwrap())); |
| } |
| |
| let report = [primary]; |
| |
| pkg_stats.record_lint(lint_level); |
| gctx.shell().print_report(&report, lint_level.force())?; |
| } |
| |
| Ok(()) |
| } |
| |
| const UNICODE_BIDI_CODEPOINTS: &[(char, &str)] = &[ |
| ('\u{202A}', "LEFT-TO-RIGHT EMBEDDING"), |
| ('\u{202B}', "RIGHT-TO-LEFT EMBEDDING"), |
| ('\u{202C}', "POP DIRECTIONAL FORMATTING"), |
| ('\u{202D}', "LEFT-TO-RIGHT OVERRIDE"), |
| ('\u{202E}', "RIGHT-TO-LEFT OVERRIDE"), |
| ('\u{2066}', "LEFT-TO-RIGHT ISOLATE"), |
| ('\u{2067}', "RIGHT-TO-LEFT ISOLATE"), |
| ('\u{2068}', "FIRST STRONG ISOLATE"), |
| ('\u{2069}', "POP DIRECTIONAL ISOLATE"), |
| ]; |
| |
| struct BiDiEvent { |
| token: Event, |
| bidi_spans: Vec<(usize, usize)>, |
| } |
| |
| fn bidi_events(contents: &str, bidi_spans: &[(usize, usize)]) -> Vec<BiDiEvent> { |
| let mut bidi_spans = bidi_spans.iter(); |
| let bidi_span = bidi_spans.next().copied(); |
| |
| let source = Source::new(contents); |
| let tokens = source.lex().into_vec(); |
| let mut collector = BiDiCollector { |
| bidi_span, |
| bidi_spans, |
| events: Vec::new(), |
| }; |
| let mut errors = (); |
| toml_parser::parser::parse_document(&tokens, &mut collector, &mut errors); |
| |
| collector.events |
| } |
| |
| struct BiDiCollector<'b> { |
| bidi_span: Option<(usize, usize)>, |
| bidi_spans: std::slice::Iter<'b, (usize, usize)>, |
| events: Vec<BiDiEvent>, |
| } |
| |
| impl BiDiCollector<'_> { |
| fn process(&mut self, kind: EventKind, encoding: Option<Encoding>, span: Span) { |
| let mut event_bidi_spans = Vec::new(); |
| while let Some(bidi_span) = self.bidi_span { |
| if bidi_span.0 < span.start() { |
| self.bidi_span = self.bidi_spans.next().copied(); |
| continue; |
| } else if span.end() <= bidi_span.0 { |
| break; |
| } |
| |
| event_bidi_spans.push(bidi_span); |
| self.bidi_span = self.bidi_spans.next().copied(); |
| } |
| |
| if !event_bidi_spans.is_empty() { |
| let token = Event::new_unchecked(kind, encoding, span); |
| self.events.push(BiDiEvent { |
| token, |
| bidi_spans: event_bidi_spans, |
| }); |
| } |
| } |
| } |
| |
| impl EventReceiver for BiDiCollector<'_> { |
| fn comment(&mut self, span: Span, _error: &mut dyn toml_parser::ErrorSink) { |
| self.process(EventKind::Comment, None, span) |
| } |
| } |