Skip to content

Commit 80fbfed

Browse files
author
Jon Gjengset
committed
Check --config for dotted keys only
This addresses the remaining unresolved issue for `config-cli` (#7722).
1 parent 263b169 commit 80fbfed

File tree

2 files changed

+151
-13
lines changed

2 files changed

+151
-13
lines changed

src/cargo/util/config/mod.rs

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,17 +1176,102 @@ 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.
1179+
// We only want to allow "dotted keys" (see https://toml.io/en/v1.0.0#keys), which
1180+
// are defined as .-separated "simple" keys, where a simple key is either a quoted
1181+
// or unquoted key, as defined in the ANBF:
1182+
//
1183+
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
1184+
// quoted-key = basic-string / literal-string
1185+
//
1186+
// https://github.com/toml-lang/toml/blob/1.0.0/toml.abnf#L50-L51
1187+
//
1188+
// We don't want to bring in a full parser, but luckily since we're just verifying
1189+
// a subset of the format here, the code isn't too hairy. The big thing we need to
1190+
// deal with is quoted strings and escaped characters.
1191+
let mut in_quoted_string = false;
1192+
let mut in_literal_string = false;
1193+
let mut chars = arg.chars();
1194+
let mut depth = 1;
1195+
while let Some(c) = chars.next() {
1196+
match c {
1197+
'\'' if !in_quoted_string => {
1198+
in_literal_string = !in_literal_string;
1199+
}
1200+
_ if in_literal_string => {
1201+
// The spec only allows certain characters here, but it doesn't matter
1202+
// for the purposes of checking if this is indeed a dotted key. If the
1203+
// user gave an invalid expression, it'll be detected when we parse as
1204+
// TOML later.
1205+
//
1206+
// Note that escapes are not permitted in literal strings.
1207+
}
1208+
'"' => {
1209+
in_quoted_string = !in_quoted_string;
1210+
}
1211+
'\\' => {
1212+
// Whatever the next char is, it's not a double quote or an escape.
1213+
// Technically escape can capture more than one char, but that's only
1214+
// possible if uXXXX and UXXXXXXXX Unicode specfiers, which are all hex
1215+
// characters anyway, and therefore won't cause a problem.
1216+
let _ = chars.next();
1217+
}
1218+
_ if in_quoted_string => {
1219+
// Anything goes within quotes as far as we're concerned
1220+
}
1221+
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' => {
1222+
// These are fine as part of a dotted key
1223+
}
1224+
'.' => {
1225+
// This is a dotted key separator -- dots are okay
1226+
depth += 1;
1227+
}
1228+
' ' | '\t' => {
1229+
// This kind of whitespace is acceptable in dotted keys.
1230+
// Technically it's only allowed around the dots and =,
1231+
// but there's no need for us to be picky about that here.
1232+
}
1233+
'=' => {
1234+
// We didn't hit anything questionable before hitting the first =
1235+
// (that is not within a quoted string), so this is a dotted key
1236+
// expression.
1237+
break;
1238+
}
1239+
_ => {
1240+
// We hit some character before the = that isn't permitted in a dotted
1241+
// key expression, so the user is trying to pass something more
1242+
// involved.
1243+
bail!(
1244+
"--config argument `{}` was not a TOML dotted key expression (a.b.c = _)",
1245+
arg
1246+
);
1247+
}
1248+
}
1249+
}
11811250
let toml_v: toml::Value = toml::de::from_str(arg)
11821251
.with_context(|| format!("failed to parse --config argument `{}`", arg))?;
1183-
let toml_table = toml_v.as_table().unwrap();
1184-
if toml_table.len() != 1 {
1185-
bail!(
1186-
"--config argument `{}` expected exactly one key=value pair, got {} keys",
1187-
arg,
1188-
toml_table.len()
1189-
);
1252+
1253+
// To avoid questions around nested table merging, we disallow tables with more
1254+
// than one value -- such changes should instead take the form of multiple dotted
1255+
// key expressions passed as separate --config arguments.
1256+
{
1257+
let mut table = toml_v.as_table();
1258+
while let Some(t) = table {
1259+
if t.len() != 1 {
1260+
bail!(
1261+
"--config argument `{}` expected exactly one key=value pair, got {} keys",
1262+
arg,
1263+
t.len()
1264+
);
1265+
}
1266+
if depth == 0 {
1267+
bail!(
1268+
"--config argument `{}` uses inline table values, which are not accepted",
1269+
arg,
1270+
);
1271+
}
1272+
depth -= 1;
1273+
table = t.values().next().unwrap().as_table();
1274+
}
11901275
}
11911276
CV::from_toml(Definition::Cli, toml_v)
11921277
.with_context(|| format!("failed to convert --config argument `{}`", arg))?

tests/testsuite/config_cli.rs

Lines changed: 57 additions & 4 deletions
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() {
@@ -224,11 +224,64 @@ fn merge_array_mixed_def_paths() {
224224
}
225225

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

233286
config.build_config().unwrap();
234287
let output = read_output(config);

0 commit comments

Comments
 (0)