diff --git a/Cargo.lock b/Cargo.lock index f7727eca4d0..e5282778ab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,6 +393,8 @@ dependencies = [ "time", "toml", "toml_edit", + "toml_parser", + "toml_writer", "tracing", "tracing-chrome", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 392758a17db..b10a448689d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,8 @@ thiserror = "2.0.17" time = { version = "0.3.44", features = ["parsing", "formatting", "serde"] } toml = { version = "0.9.10", default-features = false } toml_edit = { version = "0.24.0", features = ["serde"] } +toml_parser = "1.0.6" +toml_writer = "1.0.0" tracing = { version = "0.1.44", default-features = false, features = ["std"] } # be compatible with rustc_log: https://github.com/rust-lang/rust/blob/e51e98dde6a/compiler/rustc_log/Cargo.toml#L9 tracing-chrome = "0.7.2" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } @@ -214,6 +216,8 @@ thiserror.workspace = true time.workspace = true toml = { workspace = true, features = ["std", "serde", "parse", "display", "preserve_order"] } toml_edit.workspace = true +toml_parser.workspace = true +toml_writer.workspace = true tracing = { workspace = true, features = ["attributes"] } tracing-subscriber.workspace = true unicase.workspace = true diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index a9b325e9d94..28453996922 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -25,6 +25,7 @@ use crate::lints::analyze_cargo_lints_table; use crate::lints::rules::blanket_hint_mostly_unused; use crate::lints::rules::check_im_a_teapot; use crate::lints::rules::implicit_minimum_version_req; +use crate::lints::rules::text_direction_codepoint; use crate::ops; use crate::sources::{CRATES_IO_INDEX, CRATES_IO_REGISTRY, PathSource, SourceConfigMap}; use crate::util::context::{FeatureUnification, Value}; @@ -1313,6 +1314,13 @@ impl<'gctx> Workspace<'gctx> { &mut run_error_count, self.gctx, )?; + text_direction_codepoint( + pkg.into(), + &path, + &cargo_lints, + &mut run_error_count, + self.gctx, + )?; if run_error_count > 0 { let plural = if run_error_count == 1 { "" } else { "s" }; @@ -1370,6 +1378,13 @@ impl<'gctx> Workspace<'gctx> { &mut run_error_count, self.gctx, )?; + text_direction_codepoint( + self.root_maybe().into(), + self.root_manifest(), + &cargo_lints, + &mut run_error_count, + self.gctx, + )?; } // This is a short term hack to allow `blanket_hint_mostly_unused` diff --git a/src/cargo/lints/rules/mod.rs b/src/cargo/lints/rules/mod.rs index da6975a8c5c..b49e2bdea27 100644 --- a/src/cargo/lints/rules/mod.rs +++ b/src/cargo/lints/rules/mod.rs @@ -1,16 +1,19 @@ mod blanket_hint_mostly_unused; mod im_a_teapot; mod implicit_minimum_version_req; +mod text_direction_codepoint; mod unknown_lints; pub use blanket_hint_mostly_unused::blanket_hint_mostly_unused; pub use im_a_teapot::check_im_a_teapot; pub use implicit_minimum_version_req::implicit_minimum_version_req; +pub use text_direction_codepoint::text_direction_codepoint; pub use unknown_lints::output_unknown_lints; pub const LINTS: &[crate::lints::Lint] = &[ blanket_hint_mostly_unused::LINT, implicit_minimum_version_req::LINT, im_a_teapot::LINT, + text_direction_codepoint::LINT, unknown_lints::LINT, ]; diff --git a/src/cargo/lints/rules/text_direction_codepoint.rs b/src/cargo/lints/rules/text_direction_codepoint.rs new file mode 100644 index 00000000000..15717684590 --- /dev/null +++ b/src/cargo/lints/rules/text_direction_codepoint.rs @@ -0,0 +1,364 @@ +use std::path::Path; + +use annotate_snippets::AnnotationKind; +use annotate_snippets::Group; +use annotate_snippets::Level; +use annotate_snippets::Patch; +use annotate_snippets::Snippet; +use cargo_util_schemas::manifest::TomlToolLints; +use toml_parser::Source; +use toml_parser::Span; +use toml_parser::decoder::Encoding; +use toml_parser::parser::EventReceiver; +use toml_writer::{ToTomlKey, ToTomlValue}; + +use crate::CargoResult; +use crate::GlobalContext; +use crate::core::MaybePackage; +use crate::lints::Lint; +use crate::lints::LintLevel; +use crate::lints::ManifestFor; +use crate::lints::rel_cwd_manifest_path; + +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"), +]; + +pub const LINT: Lint = Lint { + name: "text_direction_codepoint", + desc: "unicode codepoint changing visible direction of text present in manifest", + groups: &[], + default_level: LintLevel::Deny, + edition_lint_opts: None, + feature_gate: None, + docs: None, +}; + +/// Paths where BiDi codepoints are allowed (legitimate RTL content). +const ALLOWED_BIDI_PATHS: &[&[&str]] = &[ + &["package", "description"], + &["package", "metadata"], + &["workspace", "metadata"], +]; + +fn is_allowed_bidi_path(path: &[String]) -> bool { + ALLOWED_BIDI_PATHS.iter().any(|allowed| { + if path.len() < allowed.len() { + return false; + } + path.iter() + .zip(allowed.iter()) + .all(|(a, b)| a.as_str() == *b) + }) +} + +/// Generate a suggestion for replacing a literal string or bare key with a basic string +/// that has escaped BiDi codepoints. +fn generate_suggestion( + contents: &str, + span: Span, + finding_type: &FindingType, + encoding: Option<&Encoding>, +) -> Option { + let needs_suggestion = match (finding_type, encoding) { + (FindingType::Key, Some(Encoding::LiteralString)) => true, + (FindingType::Key, None) => true, // bare key + (FindingType::Scalar, Some(Encoding::LiteralString)) => true, + _ => false, + }; + + if !needs_suggestion { + return None; + } + + let text = &contents[span.start()..span.end()]; + + let decoded = if text.starts_with('"') && text.ends_with('"') { + return None; + } else if text.starts_with('\'') && text.ends_with('\'') { + text[1..text.len() - 1].to_string() + } else { + text.to_string() + }; + + match finding_type { + FindingType::Key => { + let builder = toml_writer::TomlKeyBuilder::new(&decoded); + if let Some(key) = builder.as_unquoted() { + Some(key.to_toml_key()) + } else { + Some(builder.as_basic().to_toml_key()) + } + } + FindingType::Scalar => { + let builder = toml_writer::TomlStringBuilder::new(&decoded); + Some(builder.as_basic().to_toml_value()) + } + _ => None, + } +} + +#[derive(Clone)] +enum FindingType { + Key, + Scalar, + Comment, +} + +struct Finding { + byte_idx: usize, + ch: char, + name: &'static str, + in_allowed_value: bool, + event_span: (usize, usize), + finding_type: FindingType, + encoding: Option, + span: Span, +} + +struct BiDiCollector<'a> { + contents: &'a str, + findings: &'a mut Vec, + key_path: Vec, + table_stack: Vec>, +} + +impl<'a> BiDiCollector<'a> { + fn new(contents: &'a str, findings: &'a mut Vec) -> Self { + Self { + contents, + findings, + key_path: Vec::new(), + table_stack: Vec::new(), + } + } + + fn check_span_for_bidi( + &mut self, + span: Span, + in_value: bool, + finding_type: FindingType, + encoding: Option, + ) { + let text = &self.contents[span.start()..span.end()]; + let event_span = (span.start(), span.end()); + for (offset, ch) in text.char_indices() { + if let Some((_, name)) = UNICODE_BIDI_CODEPOINTS.iter().find(|(c, _)| *c == ch) { + let in_allowed_value = in_value && is_allowed_bidi_path(&self.key_path); + self.findings.push(Finding { + byte_idx: span.start() + offset, + ch, + name, + in_allowed_value, + event_span, + finding_type: finding_type.clone(), + encoding: encoding.clone(), + span, + }); + } + } + } + + fn current_path(&self) -> Vec { + let mut path = self.table_stack.last().cloned().unwrap_or_default(); + path.extend(self.key_path.clone()); + path + } +} + +impl EventReceiver for BiDiCollector<'_> { + fn std_table_open(&mut self, _span: Span, _error: &mut dyn toml_parser::ErrorSink) { + self.table_stack.push(self.key_path.clone()); + self.key_path.clear(); + } + + fn std_table_close(&mut self, _span: Span, _error: &mut dyn toml_parser::ErrorSink) { + if let Some(last) = self.table_stack.last_mut() { + *last = self.key_path.clone(); + } + self.key_path.clear(); + } + + fn array_table_open(&mut self, _span: Span, _error: &mut dyn toml_parser::ErrorSink) { + self.table_stack.push(self.key_path.clone()); + self.key_path.clear(); + } + + fn array_table_close(&mut self, _span: Span, _error: &mut dyn toml_parser::ErrorSink) { + if let Some(last) = self.table_stack.last_mut() { + *last = self.key_path.clone(); + } + self.key_path.clear(); + } + + fn simple_key( + &mut self, + span: Span, + kind: Option, + _error: &mut dyn toml_parser::ErrorSink, + ) { + self.check_span_for_bidi(span, false, FindingType::Key, kind); + + let key_text = &self.contents[span.start()..span.end()]; + let key = if (key_text.starts_with('"') && key_text.ends_with('"')) + || (key_text.starts_with('\'') && key_text.ends_with('\'')) + { + key_text[1..key_text.len() - 1].to_string() + } else { + key_text.to_string() + }; + self.key_path.push(key); + } + + fn key_sep(&mut self, _span: Span, _error: &mut dyn toml_parser::ErrorSink) {} + + fn key_val_sep(&mut self, _span: Span, _error: &mut dyn toml_parser::ErrorSink) {} + + fn scalar( + &mut self, + span: Span, + kind: Option, + _error: &mut dyn toml_parser::ErrorSink, + ) { + let full_path = self.current_path(); + let saved_key_path = std::mem::replace(&mut self.key_path, full_path); + self.check_span_for_bidi(span, true, FindingType::Scalar, kind); + self.key_path = saved_key_path; + self.key_path.clear(); + } + + fn comment(&mut self, span: Span, _error: &mut dyn toml_parser::ErrorSink) { + self.check_span_for_bidi(span, false, FindingType::Comment, None); + } +} + +pub fn text_direction_codepoint( + manifest: ManifestFor<'_>, + manifest_path: &Path, + cargo_lints: &TomlToolLints, + error_count: &mut usize, + gctx: &GlobalContext, +) -> CargoResult<()> { + let (lint_level, reason) = manifest.lint_level(cargo_lints, LINT); + + if lint_level == LintLevel::Allow { + return Ok(()); + } + + if matches!(&manifest, ManifestFor::Workspace(MaybePackage::Package(_))) { + return Ok(()); + } + + let contents = manifest.contents(); + + let has_bidi = contents.chars().any(|ch| { + UNICODE_BIDI_CODEPOINTS + .iter() + .any(|(bidi_ch, _)| *bidi_ch == ch) + }); + + if !has_bidi { + return Ok(()); + } + + let mut findings = Vec::new(); + { + let source = Source::new(contents); + let tokens = source.lex().into_vec(); + let mut collector = BiDiCollector::new(contents, &mut findings); + let mut errors = Vec::new(); + toml_parser::parser::parse_document(&tokens, &mut collector, &mut errors); + } + + let disallowed_findings: Vec<_> = findings + .into_iter() + .filter(|f| !f.in_allowed_value) + .collect(); + + if disallowed_findings.is_empty() { + return Ok(()); + } + + let manifest_path_str = rel_cwd_manifest_path(manifest_path, gctx); + + let mut findings_by_event: std::collections::BTreeMap<(usize, usize), Vec<_>> = + std::collections::BTreeMap::new(); + for finding in disallowed_findings { + findings_by_event + .entry(finding.event_span) + .or_insert_with(Vec::new) + .push(finding); + } + + let level = lint_level.to_diagnostic_level(); + let emitted_source = LINT.emitted_source(lint_level, reason); + + for (event_idx, event_findings) in findings_by_event.values().enumerate() { + if lint_level.is_error() { + *error_count += 1; + } + + let title = LINT.desc.to_string(); + + let labels: Vec = event_findings + .iter() + .map(|f| format!(r#"`\u{{{:04X}}}` ({})"#, f.ch as u32, f.name)) + .collect(); + + let first_finding = &event_findings[0]; + let suggestion = generate_suggestion( + contents, + first_finding.span, + &first_finding.finding_type, + first_finding.encoding.as_ref(), + ); + + let mut snippet = Snippet::source(contents).path(&manifest_path_str); + for (finding, label) in event_findings.iter().zip(labels.iter()) { + let span = finding.byte_idx..(finding.byte_idx + finding.ch.len_utf8()); + snippet = snippet.annotation(AnnotationKind::Primary.span(span).label(label)); + } + + let mut primary_group = + Group::with_title(level.clone().primary_title(&title)).element(snippet); + + if event_idx == 0 { + primary_group = primary_group.element(Level::NOTE.message(&emitted_source)); + primary_group = primary_group.element(Level::NOTE.message( + "these kinds of unicode codepoints change the way text flows on screen, \ + but can cause confusion because they change the order of characters", + )); + } + + let mut report = vec![primary_group]; + + if let Some(sugg) = &suggestion { + let event_span = first_finding.event_span; + let sugg_str = sugg.as_str(); + report.push( + Level::HELP.secondary_title("suggested fix").element( + Snippet::source(contents) + .path(&manifest_path_str) + .patch(Patch::new(event_span.0..event_span.1, sugg_str)), + ), + ); + } else { + report[0] = report[0].clone().element(Level::HELP.message( + "if their presence wasn't intentional, you can remove them, \ + or use their escape sequence (e.g., \\u{202E}) in double-quoted strings", + )); + } + + gctx.shell().print_report(&report, lint_level.force())?; + } + + Ok(()) +} diff --git a/tests/testsuite/lints/mod.rs b/tests/testsuite/lints/mod.rs index ea2ddeb7137..f743676b03f 100644 --- a/tests/testsuite/lints/mod.rs +++ b/tests/testsuite/lints/mod.rs @@ -7,6 +7,7 @@ mod blanket_hint_mostly_unused; mod error; mod implicit_minimum_version_req; mod inherited; +mod text_direction_codepoint; mod unknown_lints; mod warning; diff --git a/tests/testsuite/lints/text_direction_codepoint.rs b/tests/testsuite/lints/text_direction_codepoint.rs new file mode 100644 index 00000000000..684e89c95f0 --- /dev/null +++ b/tests/testsuite/lints/text_direction_codepoint.rs @@ -0,0 +1,354 @@ +use crate::prelude::*; +use cargo_test_support::project; +use cargo_test_support::str; + +#[cargo_test] +fn bidi_in_description_allowed() { + // BiDi in description is allowed (legitimate RTL language use case) + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" +description = \"A \u{202E}test package\" +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn bidi_in_comment_denied() { + // BiDi in comments is denied - can be used to hide malicious content + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" +# This is a \u{202E}tricky comment +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] unicode codepoint changing visible direction of text present in manifest + --> Cargo.toml:6:13 + | +6 | # This is a �tricky comment + | ^ `/u{202E}` (RIGHT-TO-LEFT OVERRIDE) + | + = [NOTE] `cargo::text_direction_codepoint` is set to `deny` by default + = [NOTE] these kinds of unicode codepoints change the way text flows on screen, but can cause confusion because they change the order of characters + = [HELP] if their presence wasn't intentional, you can remove them, or use their escape sequence (e.g., /u{202E}) in double-quoted strings +[ERROR] encountered 1 error while running lints + +"#]]) + .run(); +} + +#[cargo_test] +fn bidi_in_non_description_value_denied() { + // BiDi in regular values (not description/metadata) is denied + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" +authors = [\"Test \u{202E}Author\"] +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] unicode codepoint changing visible direction of text present in manifest + --> Cargo.toml:6:18 + | +6 | authors = ["Test �Author"] + | ^ `/u{202E}` (RIGHT-TO-LEFT OVERRIDE) + | + = [NOTE] `cargo::text_direction_codepoint` is set to `deny` by default + = [NOTE] these kinds of unicode codepoints change the way text flows on screen, but can cause confusion because they change the order of characters + = [HELP] if their presence wasn't intentional, you can remove them, or use their escape sequence (e.g., /u{202E}) in double-quoted strings +[ERROR] encountered 1 error while running lints + +"#]]) + .run(); +} + +#[cargo_test] +fn multiple_bidi_same_line() { + // Multiple BiDi codepoints on the same line in a denied location + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" +authors = [\"A \u{202E}test\u{202D} author\"] +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] unicode codepoint changing visible direction of text present in manifest + --> Cargo.toml:6:15 + | +6 | authors = ["A �test� author"] + | ^ ^ `/u{202D}` (LEFT-TO-RIGHT OVERRIDE) + | | + | `/u{202E}` (RIGHT-TO-LEFT OVERRIDE) + | + = [NOTE] `cargo::text_direction_codepoint` is set to `deny` by default + = [NOTE] these kinds of unicode codepoints change the way text flows on screen, but can cause confusion because they change the order of characters + = [HELP] if their presence wasn't intentional, you can remove them, or use their escape sequence (e.g., /u{202E}) in double-quoted strings +[ERROR] encountered 1 error while running lints + +"#]]) + .run(); +} + +#[cargo_test] +fn allow_lint() { + // Test that the lint can be allowed even for denied locations + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" +authors = [\"Test \u{202E}Author\"] + +[lints.cargo] +text_direction_codepoint = \"allow\" +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn warn_lint() { + // Test that the lint can be set to warn + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" +authors = [\"Test \u{202E}Author\"] + +[lints.cargo] +text_direction_codepoint = \"warn\" +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[WARNING] unicode codepoint changing visible direction of text present in manifest + --> Cargo.toml:6:18 + | +6 | authors = ["Test �Author"] + | ^ `/u{202E}` (RIGHT-TO-LEFT OVERRIDE) + | + = [NOTE] `cargo::text_direction_codepoint` is set to `warn` in `[lints]` + = [NOTE] these kinds of unicode codepoints change the way text flows on screen, but can cause confusion because they change the order of characters + = [HELP] if their presence wasn't intentional, you can remove them, or use their escape sequence (e.g., /u{202E}) in double-quoted strings +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn no_bidi_clean() { + // Clean manifest without BiDi + let p = project() + .file( + "Cargo.toml", + r#" +[package] +name = "foo" +version = "0.0.1" +edition = "2015" +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn workspace_metadata_allowed() { + // BiDi in workspace.metadata is allowed (legitimate use case) + let manifest = " +[workspace] +members = [\"foo\"] + +[workspace.metadata] +info = \"test \u{202E}info\" +"; + let foo_manifest = r#" +[package] +name = "foo" +version = "0.0.1" +edition = "2015" +"#; + let p = project() + .file("Cargo.toml", manifest) + .file("foo/Cargo.toml", foo_manifest) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn package_metadata_allowed() { + // BiDi in package.metadata values is allowed + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" + +[package.metadata] +info = \"test \u{202E}info\" +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn workspace_inherited_allow() { + // Workspace-level lint configuration with member package + let manifest = " +[workspace] +members = [\"foo\"] + +[workspace.lints.cargo] +text_direction_codepoint = \"allow\" +"; + let foo_manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" +authors = [\"Test \u{202E}Author\"] + +[lints] +workspace = true +"; + let p = project() + .file("Cargo.toml", manifest) + .file("foo/Cargo.toml", foo_manifest) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn bidi_in_key_denied() { + // BiDi in keys (inside quoted key) is denied - security concern + let manifest = " +[package] +name = \"foo\" +version = \"0.0.1\" +edition = \"2015\" + +[package.metadata] +\"\u{202E}evil\" = \"value\" +"; + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Zcargo-lints") + .masquerade_as_nightly_cargo(&["cargo-lints"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] unicode codepoint changing visible direction of text present in manifest + --> Cargo.toml:8:2 + | +8 | "�evil" = "value" + | ^ `/u{202E}` (RIGHT-TO-LEFT OVERRIDE) + | + = [NOTE] `cargo::text_direction_codepoint` is set to `deny` by default + = [NOTE] these kinds of unicode codepoints change the way text flows on screen, but can cause confusion because they change the order of characters + = [HELP] if their presence wasn't intentional, you can remove them, or use their escape sequence (e.g., /u{202E}) in double-quoted strings +[ERROR] encountered 1 error while running lints + +"#]]) + .run(); +}