From 80fbfed287236c8d25f009062807e77dcf67393d Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 6 Dec 2021 12:43:46 -0800 Subject: [PATCH 1/7] Check --config for dotted keys only This addresses the remaining unresolved issue for `config-cli` (#7722). --- src/cargo/util/config/mod.rs | 103 +++++++++++++++++++++++++++++++--- tests/testsuite/config_cli.rs | 61 ++++++++++++++++++-- 2 files changed, 151 insertions(+), 13 deletions(-) diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index bc0d5709807..2cdfe1d38d8 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -1176,17 +1176,102 @@ impl Config { map.insert("include".to_string(), value); CV::Table(map, Definition::Cli) } else { - // TODO: This should probably use a more narrow parser, reject - // comments, blank lines, [headers], etc. + // We only want to allow "dotted keys" (see https://toml.io/en/v1.0.0#keys), which + // are defined as .-separated "simple" keys, where a simple key is either a quoted + // or unquoted key, as defined in the ANBF: + // + // unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _ + // quoted-key = basic-string / literal-string + // + // https://github.com/toml-lang/toml/blob/1.0.0/toml.abnf#L50-L51 + // + // We don't want to bring in a full parser, but luckily since we're just verifying + // a subset of the format here, the code isn't too hairy. The big thing we need to + // deal with is quoted strings and escaped characters. + let mut in_quoted_string = false; + let mut in_literal_string = false; + let mut chars = arg.chars(); + let mut depth = 1; + while let Some(c) = chars.next() { + match c { + '\'' if !in_quoted_string => { + in_literal_string = !in_literal_string; + } + _ if in_literal_string => { + // The spec only allows certain characters here, but it doesn't matter + // for the purposes of checking if this is indeed a dotted key. If the + // user gave an invalid expression, it'll be detected when we parse as + // TOML later. + // + // Note that escapes are not permitted in literal strings. + } + '"' => { + in_quoted_string = !in_quoted_string; + } + '\\' => { + // Whatever the next char is, it's not a double quote or an escape. + // Technically escape can capture more than one char, but that's only + // possible if uXXXX and UXXXXXXXX Unicode specfiers, which are all hex + // characters anyway, and therefore won't cause a problem. + let _ = chars.next(); + } + _ if in_quoted_string => { + // Anything goes within quotes as far as we're concerned + } + 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' => { + // These are fine as part of a dotted key + } + '.' => { + // This is a dotted key separator -- dots are okay + depth += 1; + } + ' ' | '\t' => { + // This kind of whitespace is acceptable in dotted keys. + // Technically it's only allowed around the dots and =, + // but there's no need for us to be picky about that here. + } + '=' => { + // We didn't hit anything questionable before hitting the first = + // (that is not within a quoted string), so this is a dotted key + // expression. + break; + } + _ => { + // We hit some character before the = that isn't permitted in a dotted + // key expression, so the user is trying to pass something more + // involved. + bail!( + "--config argument `{}` was not a TOML dotted key expression (a.b.c = _)", + arg + ); + } + } + } let toml_v: toml::Value = toml::de::from_str(arg) .with_context(|| format!("failed to parse --config argument `{}`", arg))?; - let toml_table = toml_v.as_table().unwrap(); - if toml_table.len() != 1 { - bail!( - "--config argument `{}` expected exactly one key=value pair, got {} keys", - arg, - toml_table.len() - ); + + // To avoid questions around nested table merging, we disallow tables with more + // than one value -- such changes should instead take the form of multiple dotted + // key expressions passed as separate --config arguments. + { + let mut table = toml_v.as_table(); + while let Some(t) = table { + if t.len() != 1 { + bail!( + "--config argument `{}` expected exactly one key=value pair, got {} keys", + arg, + t.len() + ); + } + if depth == 0 { + bail!( + "--config argument `{}` uses inline table values, which are not accepted", + arg, + ); + } + depth -= 1; + table = t.values().next().unwrap().as_table(); + } } CV::from_toml(Definition::Cli, toml_v) .with_context(|| format!("failed to convert --config argument `{}`", arg))? diff --git a/tests/testsuite/config_cli.rs b/tests/testsuite/config_cli.rs index e8a78e7c462..055f66cf5dc 100644 --- a/tests/testsuite/config_cli.rs +++ b/tests/testsuite/config_cli.rs @@ -3,7 +3,7 @@ use super::config::{assert_error, assert_match, read_output, write_config, ConfigBuilder}; use cargo::util::config::Definition; use cargo_test_support::{paths, project}; -use std::fs; +use std::{collections::HashMap, fs}; #[cargo_test] fn config_gated() { @@ -224,11 +224,64 @@ fn merge_array_mixed_def_paths() { } #[cargo_test] -fn unused_key() { - // Unused key passed on command line. +fn enforces_format() { + // These dotted key expressions should all be fine. let config = ConfigBuilder::new() - .config_arg("build={jobs=1, unused=2}") + .config_arg("a=true") + .config_arg(" b.a = true ") + .config_arg("c.\"b\".'a'=true") + .config_arg("d.\"=\".'='=true") + .config_arg("e.\"'\".'\"'=true") .build(); + assert_eq!(config.get::("a").unwrap(), true); + assert_eq!( + config.get::>("b").unwrap(), + HashMap::from([("a".to_string(), true)]) + ); + assert_eq!( + config + .get::>>("c") + .unwrap(), + HashMap::from([("b".to_string(), HashMap::from([("a".to_string(), true)]))]) + ); + assert_eq!( + config + .get::>>("d") + .unwrap(), + HashMap::from([("=".to_string(), HashMap::from([("=".to_string(), true)]))]) + ); + assert_eq!( + config + .get::>>("e") + .unwrap(), + HashMap::from([("'".to_string(), HashMap::from([("\"".to_string(), true)]))]) + ); + + // But anything that's not a dotted key expression should be disallowed. + let _ = ConfigBuilder::new() + .config_arg("[a] foo=true") + .build_err() + .unwrap_err(); + let _ = ConfigBuilder::new() + .config_arg("a = true\nb = true") + .build_err() + .unwrap_err(); + + // We also disallow overwriting with tables since it makes merging unclear. + let _ = ConfigBuilder::new() + .config_arg("a = { first = true, second = false }") + .build_err() + .unwrap_err(); + let _ = ConfigBuilder::new() + .config_arg("a = { first = true }") + .build_err() + .unwrap_err(); +} + +#[cargo_test] +fn unused_key() { + // Unused key passed on command line. + let config = ConfigBuilder::new().config_arg("build.unused = 2").build(); config.build_config().unwrap(); let output = read_output(config); From 934d2d63f58923fb077d5c2c543f4a0e5df2dc3a Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Tue, 14 Dec 2021 14:25:30 -0800 Subject: [PATCH 2/7] Use toml_edit to check dotted key for --config --- Cargo.toml | 1 + src/cargo/util/config/mod.rs | 126 ++++++++++------------------------ tests/testsuite/config_cli.rs | 40 +++++++++-- 3 files changed, 73 insertions(+), 94 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ee553f7c808..88eae57c1a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ tar = { version = "0.4.35", default-features = false } tempfile = "3.0" termcolor = "1.1" toml = "0.5.7" +toml_edit = "0.10.1" unicode-xid = "0.2.0" url = "2.2.2" walkdir = "2.2" diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index 62b182ffb5a..a1efe2cc8cd 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -1175,103 +1175,51 @@ impl Config { map.insert("include".to_string(), value); CV::Table(map, Definition::Cli) } else { - // We only want to allow "dotted keys" (see https://toml.io/en/v1.0.0#keys), which - // are defined as .-separated "simple" keys, where a simple key is either a quoted - // or unquoted key, as defined in the ANBF: - // - // unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _ - // quoted-key = basic-string / literal-string - // - // https://github.com/toml-lang/toml/blob/1.0.0/toml.abnf#L50-L51 - // - // We don't want to bring in a full parser, but luckily since we're just verifying - // a subset of the format here, the code isn't too hairy. The big thing we need to - // deal with is quoted strings and escaped characters. - let mut in_quoted_string = false; - let mut in_literal_string = false; - let mut chars = arg.chars(); - let mut depth = 1; - while let Some(c) = chars.next() { - match c { - '\'' if !in_quoted_string => { - in_literal_string = !in_literal_string; - } - _ if in_literal_string => { - // The spec only allows certain characters here, but it doesn't matter - // for the purposes of checking if this is indeed a dotted key. If the - // user gave an invalid expression, it'll be detected when we parse as - // TOML later. - // - // Note that escapes are not permitted in literal strings. - } - '"' => { - in_quoted_string = !in_quoted_string; - } - '\\' => { - // Whatever the next char is, it's not a double quote or an escape. - // Technically escape can capture more than one char, but that's only - // possible if uXXXX and UXXXXXXXX Unicode specfiers, which are all hex - // characters anyway, and therefore won't cause a problem. - let _ = chars.next(); - } - _ if in_quoted_string => { - // Anything goes within quotes as far as we're concerned - } - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' => { - // These are fine as part of a dotted key - } - '.' => { - // This is a dotted key separator -- dots are okay - depth += 1; - } - ' ' | '\t' => { - // This kind of whitespace is acceptable in dotted keys. - // Technically it's only allowed around the dots and =, - // but there's no need for us to be picky about that here. - } - '=' => { - // We didn't hit anything questionable before hitting the first = - // (that is not within a quoted string), so this is a dotted key - // expression. + // We only want to allow "dotted key" (see https://toml.io/en/v1.0.0#keys) + // expressions followed by a value that's not an "inline table" + // (https://toml.io/en/v1.0.0#inline-table). Easiest way to check for that is to + // parse the value as a toml_edit::Document, and check that the (single) + // inner-most table is set via dotted keys. + let doc: toml_edit::Document = arg.parse().with_context(|| { + format!("failed to parse value from --config argument `{}` as a dotted key expression", arg) + })?; + let ok = { + let mut got_to_value = false; + let mut table = doc.as_table(); + let mut is_root = true; + while table.is_dotted() || is_root { + is_root = false; + if table.len() != 1 { break; } - _ => { - // We hit some character before the = that isn't permitted in a dotted - // key expression, so the user is trying to pass something more - // involved. - bail!( - "--config argument `{}` was not a TOML dotted key expression (a.b.c = _)", - arg - ); - } - } - } - let toml_v: toml::Value = toml::de::from_str(arg) - .with_context(|| format!("failed to parse --config argument `{}`", arg))?; - - // To avoid questions around nested table merging, we disallow tables with more - // than one value -- such changes should instead take the form of multiple dotted - // key expressions passed as separate --config arguments. - { - let mut table = toml_v.as_table(); - while let Some(t) = table { - if t.len() != 1 { - bail!( - "--config argument `{}` expected exactly one key=value pair, got {} keys", - arg, - t.len() - ); - } - if depth == 0 { + let (_, n) = table.iter().next().expect("len() == 1 above"); + if let Some(nt) = n.as_table() { + table = nt; + } else if n.is_inline_table() { bail!( - "--config argument `{}` uses inline table values, which are not accepted", + "--config argument `{}` sets a value to an inline table, which is not accepted", arg, ); + } else { + got_to_value = true; + break; } - depth -= 1; - table = t.values().next().unwrap().as_table(); } + got_to_value + }; + if !ok { + bail!( + "--config argument `{}` was not a TOML dotted key expression (a.b.c = _)", + arg + ); } + + // Rather than trying to bridge between toml_edit::Document and toml::Value, we + // just parse the whole argument again now that we know it has the right format. + let toml_v = toml::from_str(arg).with_context(|| { + format!("failed to parse value from --config argument `{}`", arg) + })?; + CV::from_toml(Definition::Cli, toml_v) .with_context(|| format!("failed to convert --config argument `{}`", arg))? }; diff --git a/tests/testsuite/config_cli.rs b/tests/testsuite/config_cli.rs index be0f3fec387..ee37ab16126 100644 --- a/tests/testsuite/config_cli.rs +++ b/tests/testsuite/config_cli.rs @@ -337,10 +337,21 @@ fn bad_parse() { assert_error( config.unwrap_err(), "\ -failed to parse --config argument `abc` +failed to parse value from --config argument `abc` as a dotted key expression Caused by: - expected an equals, found eof at line 1 column 4", + TOML parse error at line 1, column 4 + | +1 | abc + | ^ +Unexpected `end of input` +Expected `.` or `=`", + ); + + let config = ConfigBuilder::new().config_arg("").build_err(); + assert_error( + config.unwrap_err(), + "--config argument `` was not a TOML dotted key expression (a.b.c = _)", ); } @@ -352,14 +363,33 @@ fn too_many_values() { config.unwrap_err(), "\ --config argument `a=1 -b=2` expected exactly one key=value pair, got 2 keys", +b=2` was not a TOML dotted key expression (a.b.c = _)", ); +} - let config = ConfigBuilder::new().config_arg("").build_err(); +#[cargo_test] +fn no_inline_table_value() { + // Disallow inline tables + let config = ConfigBuilder::new() + .config_arg("a.b={c = \"d\"}") + .build_err(); + assert_error( + config.unwrap_err(), + "--config argument `a.b={c = \"d\"}` sets a value to an inline table, which is not accepted" + ); +} + +#[cargo_test] +fn no_array_of_tables_values() { + // Disallow array-of-tables when not in dotted form + let config = ConfigBuilder::new() + .config_arg("[[a.b]]\nc = \"d\"") + .build_err(); assert_error( config.unwrap_err(), "\ - --config argument `` expected exactly one key=value pair, got 0 keys", +--config argument `[[a.b]] +c = \"d\"` was not a TOML dotted key expression (a.b.c = _)", ); } From 1372afc6bdf0ee52fa08e4b7f07300f70fc1b334 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Tue, 14 Dec 2021 19:39:03 -0800 Subject: [PATCH 3/7] Fix up config_include test --- tests/testsuite/config_include.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/testsuite/config_include.rs b/tests/testsuite/config_include.rs index aff2a78afd8..ff734b3625e 100644 --- a/tests/testsuite/config_include.rs +++ b/tests/testsuite/config_include.rs @@ -274,9 +274,14 @@ fn cli_path() { assert_error( config.unwrap_err(), "\ -failed to parse --config argument `missing.toml` +failed to parse value from --config argument `missing.toml` as a dotted key expression Caused by: - expected an equals, found eof at line 1 column 13", + TOML parse error at line 1, column 13 + | +1 | missing.toml + | ^ +Unexpected `end of input` +Expected `.` or `=`", ); } From ea6d9f106ca5945b7dee2814b9039223657b5c09 Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Thu, 20 Jan 2022 13:28:29 -0800 Subject: [PATCH 4/7] Avoid re-parsing config-cli toml --- src/cargo/util/config/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index 3b20ea65eef..65df89939ba 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -1215,9 +1215,7 @@ impl Config { ); } - // Rather than trying to bridge between toml_edit::Document and toml::Value, we - // just parse the whole argument again now that we know it has the right format. - let toml_v = toml::from_str(arg).with_context(|| { + let toml_v = toml::from_document(doc).with_context(|| { format!("failed to parse value from --config argument `{}`", arg) })?; From c4f4e20f243f94d3e3f786dc1c9e72c5d37b146b Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 24 Jan 2022 10:08:19 -0800 Subject: [PATCH 5/7] Better example in error --- src/cargo/util/config/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index 65df89939ba..8437f7a6a74 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -1210,7 +1210,7 @@ impl Config { }; if !ok { bail!( - "--config argument `{}` was not a TOML dotted key expression (a.b.c = _)", + "--config argument `{}` was not a TOML dotted key expression (such as `build.jobs = 2`)", arg ); } From 51917b450d8367c8e5afa59f4506e21763595edb Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Mon, 24 Jan 2022 12:05:09 -0800 Subject: [PATCH 6/7] Enforce no decorators in --config values --- src/cargo/util/config/mod.rs | 60 ++++++++++++++++++++++++++++------- tests/testsuite/config_cli.rs | 28 ++++++++++++++-- 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index 8437f7a6a74..c0cd213495c 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -79,7 +79,7 @@ use cargo_util::paths; use curl::easy::Easy; use lazycell::LazyCell; use serde::Deserialize; -use toml_edit::easy as toml; +use toml_edit::{easy as toml, Item}; use url::Url; mod de; @@ -1184,6 +1184,10 @@ impl Config { let doc: toml_edit::Document = arg.parse().with_context(|| { format!("failed to parse value from --config argument `{}` as a dotted key expression", arg) })?; + fn non_empty_decor(d: &toml_edit::Decor) -> bool { + d.prefix().map_or(false, |p| !p.trim().is_empty()) + || d.suffix().map_or(false, |s| !s.trim().is_empty()) + } let ok = { let mut got_to_value = false; let mut table = doc.as_table(); @@ -1193,17 +1197,49 @@ impl Config { if table.len() != 1 { break; } - let (_, n) = table.iter().next().expect("len() == 1 above"); - if let Some(nt) = n.as_table() { - table = nt; - } else if n.is_inline_table() { - bail!( - "--config argument `{}` sets a value to an inline table, which is not accepted", - arg, - ); - } else { - got_to_value = true; - break; + let (k, n) = table.iter().next().expect("len() == 1 above"); + match n { + Item::Table(nt) => { + if table.key_decor(k).map_or(false, non_empty_decor) + || non_empty_decor(nt.decor()) + { + bail!( + "--config argument `{}` \ + includes non-whitespace decoration", + arg + ) + } + table = nt; + } + Item::Value(v) if v.is_inline_table() => { + bail!( + "--config argument `{}` \ + sets a value to an inline table, which is not accepted", + arg, + ); + } + Item::Value(v) => { + if non_empty_decor(v.decor()) { + bail!( + "--config argument `{}` \ + includes non-whitespace decoration", + arg + ) + } + got_to_value = true; + break; + } + Item::ArrayOfTables(_) => { + bail!( + "--config argument `{}` \ + sets a value to an array of tables, which is not accepted", + arg, + ); + } + + Item::None => { + bail!("--config argument `{}` doesn't provide a value", arg) + } } } got_to_value diff --git a/tests/testsuite/config_cli.rs b/tests/testsuite/config_cli.rs index bd0a305f4e7..db4c8600fc6 100644 --- a/tests/testsuite/config_cli.rs +++ b/tests/testsuite/config_cli.rs @@ -352,7 +352,7 @@ Expected `.` or `=` let config = ConfigBuilder::new().config_arg("").build_err(); assert_error( config.unwrap_err(), - "--config argument `` was not a TOML dotted key expression (a.b.c = _)", + "--config argument `` was not a TOML dotted key expression (such as `build.jobs = 2`)", ); } @@ -364,7 +364,7 @@ fn too_many_values() { config.unwrap_err(), "\ --config argument `a=1 -b=2` was not a TOML dotted key expression (a.b.c = _)", +b=2` was not a TOML dotted key expression (such as `build.jobs = 2`)", ); } @@ -390,7 +390,29 @@ fn no_array_of_tables_values() { config.unwrap_err(), "\ --config argument `[[a.b]] -c = \"d\"` was not a TOML dotted key expression (a.b.c = _)", +c = \"d\"` was not a TOML dotted key expression (such as `build.jobs = 2`)", + ); +} + +#[cargo_test] +fn no_comments() { + // Disallow comments in dotted form. + let config = ConfigBuilder::new() + .config_arg("a.b = \"c\" # exactly") + .build_err(); + assert_error( + config.unwrap_err(), + "\ +--config argument `a.b = \"c\" # exactly` includes non-whitespace decoration", + ); + + let config = ConfigBuilder::new() + .config_arg("# exactly\na.b = \"c\"") + .build_err(); + assert_error( + config.unwrap_err(), + "\ +--config argument `# exactly\na.b = \"c\"` includes non-whitespace decoration", ); } From 02e071c6303c1dff422ad9d8cde84d02b6f4dfba Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Tue, 25 Jan 2022 09:37:05 -0800 Subject: [PATCH 7/7] Use implicit captured format args --- src/cargo/util/config/mod.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index c0cd213495c..0f183cdf563 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -1182,7 +1182,7 @@ impl Config { // parse the value as a toml_edit::Document, and check that the (single) // inner-most table is set via dotted keys. let doc: toml_edit::Document = arg.parse().with_context(|| { - format!("failed to parse value from --config argument `{}` as a dotted key expression", arg) + format!("failed to parse value from --config argument `{arg}` as a dotted key expression") })?; fn non_empty_decor(d: &toml_edit::Decor) -> bool { d.prefix().map_or(false, |p| !p.trim().is_empty()) @@ -1204,26 +1204,23 @@ impl Config { || non_empty_decor(nt.decor()) { bail!( - "--config argument `{}` \ - includes non-whitespace decoration", - arg + "--config argument `{arg}` \ + includes non-whitespace decoration" ) } table = nt; } Item::Value(v) if v.is_inline_table() => { bail!( - "--config argument `{}` \ - sets a value to an inline table, which is not accepted", - arg, + "--config argument `{arg}` \ + sets a value to an inline table, which is not accepted" ); } Item::Value(v) => { if non_empty_decor(v.decor()) { bail!( - "--config argument `{}` \ - includes non-whitespace decoration", - arg + "--config argument `{arg}` \ + includes non-whitespace decoration" ) } got_to_value = true; @@ -1231,14 +1228,13 @@ impl Config { } Item::ArrayOfTables(_) => { bail!( - "--config argument `{}` \ - sets a value to an array of tables, which is not accepted", - arg, + "--config argument `{arg}` \ + sets a value to an array of tables, which is not accepted" ); } Item::None => { - bail!("--config argument `{}` doesn't provide a value", arg) + bail!("--config argument `{arg}` doesn't provide a value") } } } @@ -1246,17 +1242,16 @@ impl Config { }; if !ok { bail!( - "--config argument `{}` was not a TOML dotted key expression (such as `build.jobs = 2`)", - arg + "--config argument `{arg}` was not a TOML dotted key expression (such as `build.jobs = 2`)" ); } let toml_v = toml::from_document(doc).with_context(|| { - format!("failed to parse value from --config argument `{}`", arg) + format!("failed to parse value from --config argument `{arg}`") })?; CV::from_toml(Definition::Cli, toml_v) - .with_context(|| format!("failed to convert --config argument `{}`", arg))? + .with_context(|| format!("failed to convert --config argument `{arg}`"))? }; let mut seen = HashSet::new(); let tmp_table = self @@ -1264,7 +1259,7 @@ impl Config { .with_context(|| "failed to load --config include".to_string())?; loaded_args .merge(tmp_table, true) - .with_context(|| format!("failed to merge --config argument `{}`", arg))?; + .with_context(|| format!("failed to merge --config argument `{arg}`"))?; } Ok(loaded_args) }