blob: 3f25452bdd87e792b7052954f8473730ed2e1964 [file] [edit]
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)
}
}