Skip to content

Commit 018acfd

Browse files
committed
Auto merge of #10176 - jonhoo:config-cli-simple, r=ehuss
Check --config for dotted keys only This addresses the remaining unresolved issue for `config-cli` (#7722).
2 parents 5145bd2 + 02e071c commit 018acfd

File tree

3 files changed

+183
-21
lines changed

3 files changed

+183
-21
lines changed

src/cargo/util/config/mod.rs

+74-12
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ use cargo_util::paths;
7979
use curl::easy::Easy;
8080
use lazycell::LazyCell;
8181
use serde::Deserialize;
82-
use toml_edit::easy as toml;
82+
use toml_edit::{easy as toml, Item};
8383
use url::Url;
8484

8585
mod de;
@@ -1176,28 +1176,90 @@ impl Config {
11761176
map.insert("include".to_string(), value);
11771177
CV::Table(map, Definition::Cli)
11781178
} else {
1179-
// TODO: This should probably use a more narrow parser, reject
1180-
// comments, blank lines, [headers], etc.
1181-
let toml_v: toml::Value = toml::de::from_str(arg)
1182-
.with_context(|| format!("failed to parse --config argument `{}`", arg))?;
1183-
let toml_table = toml_v.as_table().unwrap();
1184-
if toml_table.len() != 1 {
1179+
// We only want to allow "dotted key" (see https://toml.io/en/v1.0.0#keys)
1180+
// expressions followed by a value that's not an "inline table"
1181+
// (https://toml.io/en/v1.0.0#inline-table). Easiest way to check for that is to
1182+
// parse the value as a toml_edit::Document, and check that the (single)
1183+
// inner-most table is set via dotted keys.
1184+
let doc: toml_edit::Document = arg.parse().with_context(|| {
1185+
format!("failed to parse value from --config argument `{arg}` as a dotted key expression")
1186+
})?;
1187+
fn non_empty_decor(d: &toml_edit::Decor) -> bool {
1188+
d.prefix().map_or(false, |p| !p.trim().is_empty())
1189+
|| d.suffix().map_or(false, |s| !s.trim().is_empty())
1190+
}
1191+
let ok = {
1192+
let mut got_to_value = false;
1193+
let mut table = doc.as_table();
1194+
let mut is_root = true;
1195+
while table.is_dotted() || is_root {
1196+
is_root = false;
1197+
if table.len() != 1 {
1198+
break;
1199+
}
1200+
let (k, n) = table.iter().next().expect("len() == 1 above");
1201+
match n {
1202+
Item::Table(nt) => {
1203+
if table.key_decor(k).map_or(false, non_empty_decor)
1204+
|| non_empty_decor(nt.decor())
1205+
{
1206+
bail!(
1207+
"--config argument `{arg}` \
1208+
includes non-whitespace decoration"
1209+
)
1210+
}
1211+
table = nt;
1212+
}
1213+
Item::Value(v) if v.is_inline_table() => {
1214+
bail!(
1215+
"--config argument `{arg}` \
1216+
sets a value to an inline table, which is not accepted"
1217+
);
1218+
}
1219+
Item::Value(v) => {
1220+
if non_empty_decor(v.decor()) {
1221+
bail!(
1222+
"--config argument `{arg}` \
1223+
includes non-whitespace decoration"
1224+
)
1225+
}
1226+
got_to_value = true;
1227+
break;
1228+
}
1229+
Item::ArrayOfTables(_) => {
1230+
bail!(
1231+
"--config argument `{arg}` \
1232+
sets a value to an array of tables, which is not accepted"
1233+
);
1234+
}
1235+
1236+
Item::None => {
1237+
bail!("--config argument `{arg}` doesn't provide a value")
1238+
}
1239+
}
1240+
}
1241+
got_to_value
1242+
};
1243+
if !ok {
11851244
bail!(
1186-
"--config argument `{}` expected exactly one key=value pair, got {} keys",
1187-
arg,
1188-
toml_table.len()
1245+
"--config argument `{arg}` was not a TOML dotted key expression (such as `build.jobs = 2`)"
11891246
);
11901247
}
1248+
1249+
let toml_v = toml::from_document(doc).with_context(|| {
1250+
format!("failed to parse value from --config argument `{arg}`")
1251+
})?;
1252+
11911253
CV::from_toml(Definition::Cli, toml_v)
1192-
.with_context(|| format!("failed to convert --config argument `{}`", arg))?
1254+
.with_context(|| format!("failed to convert --config argument `{arg}`"))?
11931255
};
11941256
let mut seen = HashSet::new();
11951257
let tmp_table = self
11961258
.load_includes(tmp_table, &mut seen)
11971259
.with_context(|| "failed to load --config include".to_string())?;
11981260
loaded_args
11991261
.merge(tmp_table, true)
1200-
.with_context(|| format!("failed to merge --config argument `{}`", arg))?;
1262+
.with_context(|| format!("failed to merge --config argument `{arg}`"))?;
12011263
}
12021264
Ok(loaded_args)
12031265
}

tests/testsuite/config_cli.rs

+108-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use super::config::{assert_error, assert_match, read_output, write_config, ConfigBuilder};
44
use cargo::util::config::Definition;
55
use cargo_test_support::{paths, project};
6-
use std::fs;
6+
use std::{collections::HashMap, fs};
77

88
#[cargo_test]
99
fn config_gated() {
@@ -234,11 +234,64 @@ fn merge_array_mixed_def_paths() {
234234
}
235235

236236
#[cargo_test]
237-
fn unused_key() {
238-
// Unused key passed on command line.
237+
fn enforces_format() {
238+
// These dotted key expressions should all be fine.
239239
let config = ConfigBuilder::new()
240-
.config_arg("build={jobs=1, unused=2}")
240+
.config_arg("a=true")
241+
.config_arg(" b.a = true ")
242+
.config_arg("c.\"b\".'a'=true")
243+
.config_arg("d.\"=\".'='=true")
244+
.config_arg("e.\"'\".'\"'=true")
241245
.build();
246+
assert_eq!(config.get::<bool>("a").unwrap(), true);
247+
assert_eq!(
248+
config.get::<HashMap<String, bool>>("b").unwrap(),
249+
HashMap::from([("a".to_string(), true)])
250+
);
251+
assert_eq!(
252+
config
253+
.get::<HashMap<String, HashMap<String, bool>>>("c")
254+
.unwrap(),
255+
HashMap::from([("b".to_string(), HashMap::from([("a".to_string(), true)]))])
256+
);
257+
assert_eq!(
258+
config
259+
.get::<HashMap<String, HashMap<String, bool>>>("d")
260+
.unwrap(),
261+
HashMap::from([("=".to_string(), HashMap::from([("=".to_string(), true)]))])
262+
);
263+
assert_eq!(
264+
config
265+
.get::<HashMap<String, HashMap<String, bool>>>("e")
266+
.unwrap(),
267+
HashMap::from([("'".to_string(), HashMap::from([("\"".to_string(), true)]))])
268+
);
269+
270+
// But anything that's not a dotted key expression should be disallowed.
271+
let _ = ConfigBuilder::new()
272+
.config_arg("[a] foo=true")
273+
.build_err()
274+
.unwrap_err();
275+
let _ = ConfigBuilder::new()
276+
.config_arg("a = true\nb = true")
277+
.build_err()
278+
.unwrap_err();
279+
280+
// We also disallow overwriting with tables since it makes merging unclear.
281+
let _ = ConfigBuilder::new()
282+
.config_arg("a = { first = true, second = false }")
283+
.build_err()
284+
.unwrap_err();
285+
let _ = ConfigBuilder::new()
286+
.config_arg("a = { first = true }")
287+
.build_err()
288+
.unwrap_err();
289+
}
290+
291+
#[cargo_test]
292+
fn unused_key() {
293+
// Unused key passed on command line.
294+
let config = ConfigBuilder::new().config_arg("build.unused = 2").build();
242295

243296
config.build_config().unwrap();
244297
let output = read_output(config);
@@ -284,7 +337,7 @@ fn bad_parse() {
284337
assert_error(
285338
config.unwrap_err(),
286339
"\
287-
failed to parse --config argument `abc`
340+
failed to parse value from --config argument `abc` as a dotted key expression
288341
289342
Caused by:
290343
TOML parse error at line 1, column 4
@@ -295,6 +348,12 @@ Unexpected end of input
295348
Expected `.` or `=`
296349
",
297350
);
351+
352+
let config = ConfigBuilder::new().config_arg("").build_err();
353+
assert_error(
354+
config.unwrap_err(),
355+
"--config argument `` was not a TOML dotted key expression (such as `build.jobs = 2`)",
356+
);
298357
}
299358

300359
#[cargo_test]
@@ -305,14 +364,55 @@ fn too_many_values() {
305364
config.unwrap_err(),
306365
"\
307366
--config argument `a=1
308-
b=2` expected exactly one key=value pair, got 2 keys",
367+
b=2` was not a TOML dotted key expression (such as `build.jobs = 2`)",
309368
);
369+
}
310370

311-
let config = ConfigBuilder::new().config_arg("").build_err();
371+
#[cargo_test]
372+
fn no_inline_table_value() {
373+
// Disallow inline tables
374+
let config = ConfigBuilder::new()
375+
.config_arg("a.b={c = \"d\"}")
376+
.build_err();
377+
assert_error(
378+
config.unwrap_err(),
379+
"--config argument `a.b={c = \"d\"}` sets a value to an inline table, which is not accepted"
380+
);
381+
}
382+
383+
#[cargo_test]
384+
fn no_array_of_tables_values() {
385+
// Disallow array-of-tables when not in dotted form
386+
let config = ConfigBuilder::new()
387+
.config_arg("[[a.b]]\nc = \"d\"")
388+
.build_err();
389+
assert_error(
390+
config.unwrap_err(),
391+
"\
392+
--config argument `[[a.b]]
393+
c = \"d\"` was not a TOML dotted key expression (such as `build.jobs = 2`)",
394+
);
395+
}
396+
397+
#[cargo_test]
398+
fn no_comments() {
399+
// Disallow comments in dotted form.
400+
let config = ConfigBuilder::new()
401+
.config_arg("a.b = \"c\" # exactly")
402+
.build_err();
403+
assert_error(
404+
config.unwrap_err(),
405+
"\
406+
--config argument `a.b = \"c\" # exactly` includes non-whitespace decoration",
407+
);
408+
409+
let config = ConfigBuilder::new()
410+
.config_arg("# exactly\na.b = \"c\"")
411+
.build_err();
312412
assert_error(
313413
config.unwrap_err(),
314414
"\
315-
--config argument `` expected exactly one key=value pair, got 0 keys",
415+
--config argument `# exactly\na.b = \"c\"` includes non-whitespace decoration",
316416
);
317417
}
318418

tests/testsuite/config_include.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ fn cli_path() {
274274
assert_error(
275275
config.unwrap_err(),
276276
"\
277-
failed to parse --config argument `missing.toml`
277+
failed to parse value from --config argument `missing.toml` as a dotted key expression
278278
279279
Caused by:
280280
TOML parse error at line 1, column 13

0 commit comments

Comments
 (0)