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::Patch;
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_literal",
primary_group: &CORRECTNESS,
msrv: Some(super::CARGO_LINTS_MSRV),
feature_gate: None,
docs: Some(
r#"
### What it does
Detects Unicode codepoints in literals in manifests 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 literal can make code that will be compiled appear to be part of a literal,
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 { .. },
..
}
) {
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 toml_source = Source::new(contents);
let events = bidi_events(&toml_source, &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.clone())
.label("this literal 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 mut help_snippet = Snippet::source(contents).path(&manifest_path);
if let Some(original_raw) = toml_source.get(&event.token) {
let mut decoded = String::new();
let replacement = match event.token.kind() {
toml_parser::parser::EventKind::SimpleKey => {
use toml_writer::ToTomlKey as _;
original_raw.decode_key(&mut decoded, &mut ());
let builder = toml_writer::TomlKeyBuilder::new(&decoded);
let replacement = builder.as_basic();
Some(replacement.to_toml_key())
}
toml_parser::parser::EventKind::Scalar => {
use toml_writer::ToTomlValue as _;
let kind = original_raw.decode_scalar(&mut decoded, &mut ());
if matches!(kind, toml_parser::decoder::ScalarKind::String) {
let builder = toml_writer::TomlStringBuilder::new(&decoded);
let replacement = match event.token.encoding() {
Some(toml_parser::decoder::Encoding::BasicString)
| Some(toml_parser::decoder::Encoding::LiteralString)
| None => builder.as_basic(),
Some(toml_parser::decoder::Encoding::MlBasicString)
| Some(toml_parser::decoder::Encoding::MlLiteralString) => {
builder.as_ml_basic()
}
};
Some(replacement.to_toml_value())
} else {
None
}
}
_ => None,
};
if let Some(mut replacement) = replacement {
for (bidi, escaped, _) in UNICODE_BIDI_CODEPOINTS {
replacement = replacement.replace(*bidi, escaped);
}
help_snippet = help_snippet.patch(Patch::new(token_span.clone(), replacement));
}
}
let level = lint_level.to_diagnostic_level();
let mut primary = Group::with_title(level.primary_title(
"unicode codepoint changing visible direction of text present in literal",
))
.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 help = Group::with_title(Level::HELP.secondary_title("if you want to keep them but make them visible in your source code, you can escape them")).element(help_snippet);
let report = [primary, help];
pkg_stats.record_lint(lint_level);
gctx.shell().print_report(&report, lint_level.force())?;
}
Ok(())
}
const UNICODE_BIDI_CODEPOINTS: &[(char, &str, &str)] = &[
('\u{202A}', r"\u{202A}", "LEFT-TO-RIGHT EMBEDDING"),
('\u{202B}', r"\u{202B}", "RIGHT-TO-LEFT EMBEDDING"),
('\u{202C}', r"\u{202C}", "POP DIRECTIONAL FORMATTING"),
('\u{202D}', r"\u{202D}", "LEFT-TO-RIGHT OVERRIDE"),
('\u{202E}', r"\u{202E}", "RIGHT-TO-LEFT OVERRIDE"),
('\u{2066}', r"\u{2066}", "LEFT-TO-RIGHT ISOLATE"),
('\u{2067}', r"\u{2067}", "RIGHT-TO-LEFT ISOLATE"),
('\u{2068}', r"\u{2068}", "FIRST STRONG ISOLATE"),
('\u{2069}', r"\u{2069}", "POP DIRECTIONAL ISOLATE"),
];
struct BiDiEvent {
token: Event,
bidi_spans: Vec<(usize, usize)>,
}
fn bidi_events(source: &Source<'_>, bidi_spans: &[(usize, usize)]) -> Vec<BiDiEvent> {
let mut bidi_spans = bidi_spans.iter();
let bidi_span = bidi_spans.next().copied();
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 simple_key(
&mut self,
span: Span,
encoding: Option<Encoding>,
_error: &mut dyn toml_parser::ErrorSink,
) {
self.process(EventKind::SimpleKey, encoding, span)
}
fn scalar(
&mut self,
span: Span,
encoding: Option<Encoding>,
_error: &mut dyn toml_parser::ErrorSink,
) {
self.process(EventKind::Scalar, encoding, span)
}
}