Skip to content

Commit 6ca1aa7

Browse files
committed
[nextest-runner] enforce that --config only accepts key-value pairs
Port over rust-lang/cargo#10176 to nextest.
1 parent 7e11f1e commit 6ca1aa7

File tree

3 files changed

+219
-10
lines changed

3 files changed

+219
-10
lines changed

cargo-nextest/src/cargo_cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ pub(crate) struct CargoOptions {
127127

128128
// NOTE: this does not conflict with reuse build opts since we let target.runner be specified
129129
// this way
130-
/// Override a configuration value (unstable)
130+
/// Override a configuration value
131131
#[clap(long, value_name = "KEY=VALUE")]
132132
pub(crate) config: Vec<String>,
133133

nextest-runner/src/cargo_config.rs

Lines changed: 166 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
//! Since `cargo config get` is not stable as of Rust 1.61, nextest must do its own config file
77
//! search.
88
9-
use crate::errors::{CargoConfigSearchError, CargoConfigsConstructError, TargetTripleError};
9+
use crate::errors::{
10+
CargoConfigSearchError, CargoConfigsConstructError, InvalidCargoCliConfigReason,
11+
TargetTripleError,
12+
};
1013
use camino::{Utf8Path, Utf8PathBuf};
1114
use once_cell::sync::OnceCell;
1215
use serde::Deserialize;
1316
use std::{collections::BTreeMap, fmt};
17+
use toml_edit::Item;
1418

1519
/// Represents a target triple that's being cross-compiled against.
1620
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -221,17 +225,103 @@ fn parse_cli_configs(
221225
.map(|config_str| {
222226
// Each cargo config is expected to be a valid TOML file.
223227
let config_str = config_str.as_ref();
224-
let config = toml_edit::easy::from_str(config_str).map_err(|error| {
225-
CargoConfigsConstructError::CliConfigParseError {
226-
config_str: config_str.to_owned(),
227-
error,
228-
}
229-
})?;
228+
let config = parse_cli_config(config_str)?;
230229
Ok((CargoConfigSource::CliOption, config))
231230
})
232231
.collect()
233232
}
234233

234+
fn parse_cli_config(config_str: &str) -> Result<CargoConfig, CargoConfigsConstructError> {
235+
// This implementation is copied over from https://github.com/rust-lang/cargo/pull/10176.
236+
237+
// We only want to allow "dotted key" (see https://toml.io/en/v1.0.0#keys)
238+
// expressions followed by a value that's not an "inline table"
239+
// (https://toml.io/en/v1.0.0#inline-table). Easiest way to check for that is to
240+
// parse the value as a toml_edit::Document, and check that the (single)
241+
// inner-most table is set via dotted keys.
242+
let doc: toml_edit::Document =
243+
config_str
244+
.parse()
245+
.map_err(|error| CargoConfigsConstructError::CliConfigParseError {
246+
config_str: config_str.to_owned(),
247+
error,
248+
})?;
249+
250+
fn non_empty_decor(d: &toml_edit::Decor) -> bool {
251+
d.prefix().map_or(false, |p| !p.trim().is_empty())
252+
|| d.suffix().map_or(false, |s| !s.trim().is_empty())
253+
}
254+
255+
let ok = {
256+
let mut got_to_value = false;
257+
let mut table = doc.as_table();
258+
let mut is_root = true;
259+
while table.is_dotted() || is_root {
260+
is_root = false;
261+
if table.len() != 1 {
262+
break;
263+
}
264+
let (k, n) = table.iter().next().expect("len() == 1 above");
265+
match n {
266+
Item::Table(nt) => {
267+
if table.key_decor(k).map_or(false, non_empty_decor)
268+
|| non_empty_decor(nt.decor())
269+
{
270+
return Err(CargoConfigsConstructError::InvalidCliConfig {
271+
config_str: config_str.to_owned(),
272+
reason: InvalidCargoCliConfigReason::IncludesNonWhitespaceDecoration,
273+
})?;
274+
}
275+
table = nt;
276+
}
277+
Item::Value(v) if v.is_inline_table() => {
278+
return Err(CargoConfigsConstructError::InvalidCliConfig {
279+
config_str: config_str.to_owned(),
280+
reason: InvalidCargoCliConfigReason::SetsValueToInlineTable,
281+
})?;
282+
}
283+
Item::Value(v) => {
284+
if non_empty_decor(v.decor()) {
285+
return Err(CargoConfigsConstructError::InvalidCliConfig {
286+
config_str: config_str.to_owned(),
287+
reason: InvalidCargoCliConfigReason::IncludesNonWhitespaceDecoration,
288+
})?;
289+
}
290+
got_to_value = true;
291+
break;
292+
}
293+
Item::ArrayOfTables(_) => {
294+
return Err(CargoConfigsConstructError::InvalidCliConfig {
295+
config_str: config_str.to_owned(),
296+
reason: InvalidCargoCliConfigReason::SetsValueToArrayOfTables,
297+
})?;
298+
}
299+
Item::None => {
300+
return Err(CargoConfigsConstructError::InvalidCliConfig {
301+
config_str: config_str.to_owned(),
302+
reason: InvalidCargoCliConfigReason::DoesntProvideValue,
303+
})?;
304+
}
305+
}
306+
}
307+
got_to_value
308+
};
309+
if !ok {
310+
return Err(CargoConfigsConstructError::InvalidCliConfig {
311+
config_str: config_str.to_owned(),
312+
reason: InvalidCargoCliConfigReason::NotDottedKv,
313+
})?;
314+
}
315+
316+
let cargo_config: CargoConfig = toml_edit::easy::from_document(doc).map_err(|error| {
317+
CargoConfigsConstructError::CliConfigDeError {
318+
config_str: config_str.to_owned(),
319+
error,
320+
}
321+
})?;
322+
Ok(cargo_config)
323+
}
324+
235325
fn discover_impl(
236326
start_search_at: &Utf8Path,
237327
terminate_search_at: Option<&Utf8Path>,
@@ -342,7 +432,7 @@ pub(crate) struct CargoConfigRunner {
342432
pub(crate) runner: Option<Runner>,
343433
}
344434

345-
#[derive(Clone, Deserialize, Debug)]
435+
#[derive(Clone, Deserialize, Debug, Eq, PartialEq)]
346436
#[serde(untagged)]
347437
pub(crate) enum Runner {
348438
Simple(String),
@@ -355,6 +445,74 @@ mod tests {
355445
use camino::Utf8Path;
356446
use color_eyre::eyre::{Context, Result};
357447
use tempfile::TempDir;
448+
use test_case::test_case;
449+
450+
#[test]
451+
fn test_cli_kv_accepted() {
452+
// These dotted key expressions should all be fine.
453+
let config = parse_cli_config("build.target=\"aarch64-unknown-linux-gnu\"")
454+
.expect("dotted config should parse correctly");
455+
assert_eq!(
456+
config.build.target.as_deref(),
457+
Some("aarch64-unknown-linux-gnu")
458+
);
459+
460+
let config = parse_cli_config(" target.\"aarch64-unknown-linux-gnu\".runner = 'test' ")
461+
.expect("dotted config should parse correctly");
462+
assert_eq!(
463+
config.target.as_ref().unwrap()["aarch64-unknown-linux-gnu"].runner,
464+
Some(Runner::Simple("test".to_owned()))
465+
);
466+
467+
// But anything that's not a dotted key expression should be disallowed.
468+
let _ = parse_cli_config("[a] foo=true").unwrap_err();
469+
let _ = parse_cli_config("a = true\nb = true").unwrap_err();
470+
471+
// We also disallow overwriting with tables since it makes merging unclear.
472+
let _ = parse_cli_config("a = { first = true, second = false }").unwrap_err();
473+
let _ = parse_cli_config("a = { first = true }").unwrap_err();
474+
}
475+
476+
#[test_case(
477+
"",
478+
InvalidCargoCliConfigReason::NotDottedKv
479+
480+
; "empty input")]
481+
#[test_case(
482+
"a.b={c = \"d\"}",
483+
InvalidCargoCliConfigReason::SetsValueToInlineTable
484+
485+
; "no inline table value")]
486+
#[test_case(
487+
"[[a.b]]\nc = \"d\"",
488+
InvalidCargoCliConfigReason::NotDottedKv
489+
490+
; "no array of tables")]
491+
#[test_case(
492+
"a.b = \"c\" # exactly",
493+
InvalidCargoCliConfigReason::IncludesNonWhitespaceDecoration
494+
495+
; "no comments after")]
496+
#[test_case(
497+
"# exactly\na.b = \"c\"",
498+
InvalidCargoCliConfigReason::IncludesNonWhitespaceDecoration
499+
500+
; "no comments before")]
501+
fn test_invalid_cli_config_reason(arg: &str, expected_reason: InvalidCargoCliConfigReason) {
502+
// Disallow inline tables
503+
let err = parse_cli_config(arg).unwrap_err();
504+
let actual_reason = match err {
505+
CargoConfigsConstructError::InvalidCliConfig { reason, .. } => reason,
506+
other => panic!(
507+
"expected input {arg} to fail with InvalidCliConfig, actual failure: {other}"
508+
),
509+
};
510+
511+
assert_eq!(
512+
expected_reason, actual_reason,
513+
"expected reason for failure doesn't match actual reason"
514+
);
515+
}
358516

359517
#[test]
360518
fn test_find_target_triple() {

nextest-runner/src/errors.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,15 +797,66 @@ pub enum CargoConfigsConstructError {
797797
CurrentDirInvalidUtf8(#[source] FromPathBufError),
798798

799799
/// Parsing a CLI config option failed.
800-
#[error("failed to parse --config option `{config_str}` as TOML")]
800+
#[error("failed to parse --config argument `{config_str}` as TOML")]
801801
CliConfigParseError {
802802
/// The CLI config option.
803803
config_str: String,
804804

805+
/// The error that occurred trying to parse the config.
806+
#[source]
807+
error: toml_edit::TomlError,
808+
},
809+
810+
/// Deserializing a CLI config option into domain types failed.
811+
#[error("failed to deserialize --config argument `{config_str}` as TOML")]
812+
CliConfigDeError {
813+
/// The CLI config option.
814+
config_str: String,
815+
805816
/// The error that occurred trying to deserialize the config.
806817
#[source]
807818
error: toml_edit::easy::de::Error,
808819
},
820+
821+
/// A CLI config option is not in the dotted key format.
822+
#[error(
823+
"invalid format for --config argument `{config_str}` (should be a dotted key expression)"
824+
)]
825+
InvalidCliConfig {
826+
/// The CLI config option.
827+
config_str: String,
828+
829+
/// The reason why this Cargo CLI config is invalid.
830+
#[source]
831+
reason: InvalidCargoCliConfigReason,
832+
},
833+
}
834+
835+
/// The reason an invalid CLI config failed.
836+
///
837+
/// Part of [`CargoConfigsConstructError::InvalidCliConfig`].
838+
#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
839+
#[non_exhaustive]
840+
pub enum InvalidCargoCliConfigReason {
841+
/// The argument is not a TOML dotted key expression.
842+
#[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
843+
NotDottedKv,
844+
845+
/// The argument includes non-whitespace decoration.
846+
#[error("includes non-whitespace decoration")]
847+
IncludesNonWhitespaceDecoration,
848+
849+
/// The argument sets a value to an inline table.
850+
#[error("sets a value to an inline table, which is not accepted")]
851+
SetsValueToInlineTable,
852+
853+
/// The argument sets a value to an array of tables.
854+
#[error("sets a value to an array of tables, which is not accepted")]
855+
SetsValueToArrayOfTables,
856+
857+
/// The argument doesn't provide a value.
858+
#[error("doesn't provide a value")]
859+
DoesntProvideValue,
809860
}
810861

811862
/// An error occurred while looking for Cargo configuration files.

0 commit comments

Comments
 (0)