Skip to content

Commit 829ddf0

Browse files
committed
Fix config env vars that are prefix of another with underscore.
1 parent 2f52929 commit 829ddf0

File tree

5 files changed

+179
-41
lines changed

5 files changed

+179
-41
lines changed

src/cargo/util/config/de.rs

+79-36
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@ use crate::util::config::value;
44
use crate::util::config::{Config, ConfigError, ConfigKey};
55
use crate::util::config::{ConfigValue as CV, Definition, Value};
66
use serde::{de, de::IntoDeserializer};
7-
use std::collections::HashSet;
87
use std::vec;
98

109
/// Serde deserializer used to convert config values to a target type using
1110
/// `Config::get`.
1211
#[derive(Clone)]
13-
pub(crate) struct Deserializer<'config> {
14-
pub(crate) config: &'config Config,
15-
pub(crate) key: ConfigKey,
12+
pub(super) struct Deserializer<'config> {
13+
pub(super) config: &'config Config,
14+
/// The current key being deserialized.
15+
pub(super) key: ConfigKey,
16+
/// Whether or not this key part is allowed to be an inner table. For
17+
/// example, `profile.dev.build-override` needs to check if
18+
/// CARGO_PROFILE_DEV_BUILD_OVERRIDE_ prefixes exist. But
19+
/// CARGO_BUILD_TARGET should not check for prefixes because it would
20+
/// collide with CARGO_BUILD_TARGET_DIR. See `ConfigMapAccess` for
21+
/// details.
22+
pub(super) env_prefix_ok: bool,
1623
}
1724

1825
macro_rules! deserialize_method {
@@ -109,7 +116,7 @@ impl<'de, 'config> de::Deserializer<'de> for Deserializer<'config> {
109116
where
110117
V: de::Visitor<'de>,
111118
{
112-
if self.config.has_key(&self.key) {
119+
if self.config.has_key(&self.key, self.env_prefix_ok) {
113120
visitor.visit_some(self)
114121
} else {
115122
// Treat missing values as `None`.
@@ -179,8 +186,10 @@ impl<'de, 'config> de::Deserializer<'de> for Deserializer<'config> {
179186

180187
struct ConfigMapAccess<'config> {
181188
de: Deserializer<'config>,
182-
set_iter: <HashSet<KeyKind> as IntoIterator>::IntoIter,
183-
next: Option<KeyKind>,
189+
/// The fields that this map should deserialize.
190+
fields: Vec<KeyKind>,
191+
/// Current field being deserialized.
192+
field_index: usize,
184193
}
185194

186195
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -191,50 +200,53 @@ enum KeyKind {
191200

192201
impl<'config> ConfigMapAccess<'config> {
193202
fn new_map(de: Deserializer<'config>) -> Result<ConfigMapAccess<'config>, ConfigError> {
194-
let mut set = HashSet::new();
203+
let mut fields = Vec::new();
195204
if let Some(mut v) = de.config.get_table(&de.key)? {
196205
// `v: Value<HashMap<String, CV>>`
197206
for (key, _value) in v.val.drain() {
198-
set.insert(KeyKind::CaseSensitive(key));
207+
fields.push(KeyKind::CaseSensitive(key));
199208
}
200209
}
201210
if de.config.cli_unstable().advanced_env {
202211
// `CARGO_PROFILE_DEV_PACKAGE_`
203-
let env_pattern = format!("{}_", de.key.as_env_key());
212+
let env_prefix = format!("{}_", de.key.as_env_key());
204213
for env_key in de.config.env.keys() {
205-
if env_key.starts_with(&env_pattern) {
214+
if env_key.starts_with(&env_prefix) {
206215
// `CARGO_PROFILE_DEV_PACKAGE_bar_OPT_LEVEL = 3`
207-
let rest = &env_key[env_pattern.len()..];
216+
let rest = &env_key[env_prefix.len()..];
208217
// `rest = bar_OPT_LEVEL`
209218
let part = rest.splitn(2, '_').next().unwrap();
210219
// `part = "bar"`
211-
set.insert(KeyKind::CaseSensitive(part.to_string()));
220+
fields.push(KeyKind::CaseSensitive(part.to_string()));
212221
}
213222
}
214223
}
215224
Ok(ConfigMapAccess {
216225
de,
217-
set_iter: set.into_iter(),
218-
next: None,
226+
fields,
227+
field_index: 0,
219228
})
220229
}
221230

222231
fn new_struct(
223232
de: Deserializer<'config>,
224233
fields: &'static [&'static str],
225234
) -> Result<ConfigMapAccess<'config>, ConfigError> {
226-
let mut set = HashSet::new();
227-
for field in fields {
228-
set.insert(KeyKind::Normal(field.to_string()));
229-
}
235+
let fields: Vec<KeyKind> = fields
236+
.iter()
237+
.map(|field| KeyKind::Normal(field.to_string()))
238+
.collect();
230239

231240
// Assume that if we're deserializing a struct it exhaustively lists all
232241
// possible fields on this key that we're *supposed* to use, so take
233242
// this opportunity to warn about any keys that aren't recognized as
234243
// fields and warn about them.
235244
if let Some(mut v) = de.config.get_table(&de.key)? {
236245
for (t_key, value) in v.val.drain() {
237-
if set.contains(&KeyKind::Normal(t_key.to_string())) {
246+
if fields.iter().any(|k| match k {
247+
KeyKind::Normal(s) => s == &t_key,
248+
KeyKind::CaseSensitive(s) => s == &t_key,
249+
}) {
238250
continue;
239251
}
240252
de.config.shell().warn(format!(
@@ -248,8 +260,8 @@ impl<'config> ConfigMapAccess<'config> {
248260

249261
Ok(ConfigMapAccess {
250262
de,
251-
set_iter: set.into_iter(),
252-
next: None,
263+
fields,
264+
field_index: 0,
253265
})
254266
}
255267
}
@@ -261,30 +273,61 @@ impl<'de, 'config> de::MapAccess<'de> for ConfigMapAccess<'config> {
261273
where
262274
K: de::DeserializeSeed<'de>,
263275
{
264-
match self.set_iter.next() {
265-
Some(key) => {
266-
let name = match &key {
267-
KeyKind::Normal(s) | KeyKind::CaseSensitive(s) => s.as_str(),
268-
};
269-
let result = seed.deserialize(name.into_deserializer()).map(Some);
270-
self.next = Some(key);
271-
result
272-
}
273-
None => Ok(None),
276+
if self.field_index >= self.fields.len() {
277+
return Ok(None);
274278
}
279+
let field = match &self.fields[self.field_index] {
280+
KeyKind::Normal(s) | KeyKind::CaseSensitive(s) => s.as_str(),
281+
};
282+
seed.deserialize(field.into_deserializer()).map(Some)
275283
}
276284

277285
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
278286
where
279287
V: de::DeserializeSeed<'de>,
280288
{
281-
match self.next.take().expect("next field missing") {
282-
KeyKind::Normal(key) => self.de.key.push(&key),
283-
KeyKind::CaseSensitive(key) => self.de.key.push_sensitive(&key),
284-
}
289+
let field = &self.fields[self.field_index];
290+
self.field_index += 1;
291+
// Set this as the current key in the deserializer.
292+
let field = match field {
293+
KeyKind::Normal(field) => {
294+
self.de.key.push(&field);
295+
field
296+
}
297+
KeyKind::CaseSensitive(field) => {
298+
self.de.key.push_sensitive(&field);
299+
field
300+
}
301+
};
302+
// Env vars that are a prefix of another with a dash/underscore cannot
303+
// be supported by our serde implementation, so check for them here.
304+
// Example:
305+
// CARGO_BUILD_TARGET
306+
// CARGO_BUILD_TARGET_DIR
307+
// or
308+
// CARGO_PROFILE_DEV_DEBUG
309+
// CARGO_PROFILE_DEV_DEBUG_ASSERTIONS
310+
// The `deserialize_option` method does not know the type of the field.
311+
// If the type is an Option<struct> (like
312+
// `profile.dev.build-override`), then it needs to check for env vars
313+
// starting with CARGO_FOO_BAR_. This is a problem for keys like
314+
// CARGO_BUILD_TARGET because checking for a prefix would incorrectly
315+
// match CARGO_BUILD_TARGET_DIR. `deserialize_option` would have no
316+
// choice but to call `visit_some()` which would then fail if
317+
// CARGO_BUILD_TARGET isn't set. So we check for these prefixes and
318+
// disallow them here.
319+
let env_prefix = format!("{}_", field).replace('-', "_");
320+
let env_prefix_ok = !self.fields.iter().any(|field| {
321+
let field = match field {
322+
KeyKind::Normal(s) | KeyKind::CaseSensitive(s) => s.as_str(),
323+
};
324+
field.replace('-', "_").starts_with(&env_prefix)
325+
});
326+
285327
let result = seed.deserialize(Deserializer {
286328
config: self.de.config,
287329
key: self.de.key.clone(),
330+
env_prefix_ok,
288331
});
289332
self.de.key.pop();
290333
result

src/cargo/util/config/mod.rs

+15-5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
//! tables (because Cargo must fetch all of them), so those do not support
3636
//! environment variables.
3737
//!
38+
//! Try to avoid keys that are a prefix of another with a dash/underscore. For
39+
//! example `build.target` and `build.target-dir`. This is OK if these are not
40+
//! structs/maps, but if it is a struct or map, then it will not be able to
41+
//! read the environment variable due to ambiguity. (See `ConfigMapAccess` for
42+
//! more details.)
43+
//!
3844
//! ## Internal API
3945
//!
4046
//! Internally config values are stored with the `ConfigValue` type after they
@@ -520,13 +526,16 @@ impl Config {
520526
}
521527
}
522528

523-
fn has_key(&self, key: &ConfigKey) -> bool {
524-
if self.env.get(key.as_env_key()).is_some() {
529+
fn has_key(&self, key: &ConfigKey, env_prefix_ok: bool) -> bool {
530+
if self.env.contains_key(key.as_env_key()) {
525531
return true;
526532
}
527-
let env_pattern = format!("{}_", key.as_env_key());
528-
if self.env.keys().any(|k| k.starts_with(&env_pattern)) {
529-
return true;
533+
// See ConfigMapAccess for a description of this.
534+
if env_prefix_ok {
535+
let env_prefix = format!("{}_", key.as_env_key());
536+
if self.env.keys().any(|k| k.starts_with(&env_prefix)) {
537+
return true;
538+
}
530539
}
531540
if let Ok(o_cv) = self.get_cv(key) {
532541
if o_cv.is_some() {
@@ -1101,6 +1110,7 @@ impl Config {
11011110
let d = Deserializer {
11021111
config: self,
11031112
key: ConfigKey::from_str(key),
1113+
env_prefix_ok: true,
11041114
};
11051115
T::deserialize(d).map_err(|e| e.into())
11061116
}

src/cargo/util/config/value.rs

+4
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ pub(crate) const DEFINITION_FIELD: &str = "$__cargo_private_definition";
5151
pub(crate) const NAME: &str = "$__cargo_private_Value";
5252
pub(crate) static FIELDS: [&str; 2] = [VALUE_FIELD, DEFINITION_FIELD];
5353

54+
/// Location where a config value is defined.
5455
#[derive(Clone, Debug, Eq)]
5556
pub enum Definition {
57+
/// Defined in a `.cargo/config`, includes the path to the file.
5658
Path(PathBuf),
59+
/// Defined in an environment variable, includes the environment key.
5760
Environment(String),
61+
/// Passed in on the command line.
5862
Cli,
5963
}
6064

tests/testsuite/build.rs

+5
Original file line numberDiff line numberDiff line change
@@ -2806,6 +2806,11 @@ fn custom_target_dir_env() {
28062806
assert!(p.root().join("foo/target/debug").join(&exe_name).is_file());
28072807
assert!(p.root().join("target/debug").join(&exe_name).is_file());
28082808

2809+
p.cargo("build")
2810+
.env("CARGO_BUILD_TARGET_DIR", "foo2/target")
2811+
.run();
2812+
assert!(p.root().join("foo2/target/debug").join(&exe_name).is_file());
2813+
28092814
fs::create_dir(p.root().join(".cargo")).unwrap();
28102815
File::create(p.root().join(".cargo/config"))
28112816
.unwrap()

tests/testsuite/config.rs

+76
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,32 @@ lto = false
424424
);
425425
}
426426

427+
#[cargo_test]
428+
fn profile_env_var_prefix() {
429+
// Check for a bug with collision on DEBUG vs DEBUG_ASSERTIONS.
430+
let config = ConfigBuilder::new()
431+
.env("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false")
432+
.build();
433+
let p: toml::TomlProfile = config.get("profile.dev").unwrap();
434+
assert_eq!(p.debug_assertions, Some(false));
435+
assert_eq!(p.debug, None);
436+
437+
let config = ConfigBuilder::new()
438+
.env("CARGO_PROFILE_DEV_DEBUG", "1")
439+
.build();
440+
let p: toml::TomlProfile = config.get("profile.dev").unwrap();
441+
assert_eq!(p.debug_assertions, None);
442+
assert_eq!(p.debug, Some(toml::U32OrBool::U32(1)));
443+
444+
let config = ConfigBuilder::new()
445+
.env("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false")
446+
.env("CARGO_PROFILE_DEV_DEBUG", "1")
447+
.build();
448+
let p: toml::TomlProfile = config.get("profile.dev").unwrap();
449+
assert_eq!(p.debug_assertions, Some(false));
450+
assert_eq!(p.debug, Some(toml::U32OrBool::U32(1)));
451+
}
452+
427453
#[cargo_test]
428454
fn config_deserialize_any() {
429455
// Some tests to exercise deserialize_any for deserializers that need to
@@ -1103,3 +1129,53 @@ Caused by:
11031129
expected string but found integer in list",
11041130
);
11051131
}
1132+
1133+
#[cargo_test]
1134+
fn struct_with_opt_inner_struct() {
1135+
// Struct with a key that is Option of another struct.
1136+
// Check that can be defined with environment variable.
1137+
#[derive(Deserialize)]
1138+
struct Inner {
1139+
value: Option<i32>,
1140+
}
1141+
#[derive(Deserialize)]
1142+
struct Foo {
1143+
inner: Option<Inner>,
1144+
}
1145+
let config = ConfigBuilder::new()
1146+
.env("CARGO_FOO_INNER_VALUE", "12")
1147+
.build();
1148+
let f: Foo = config.get("foo").unwrap();
1149+
assert_eq!(f.inner.unwrap().value.unwrap(), 12);
1150+
}
1151+
1152+
#[cargo_test]
1153+
fn overlapping_env_config() {
1154+
// Issue where one key is a prefix of another.
1155+
#[derive(Deserialize)]
1156+
#[serde(rename_all = "kebab-case")]
1157+
struct Ambig {
1158+
debug: Option<u32>,
1159+
debug_assertions: Option<bool>,
1160+
}
1161+
let config = ConfigBuilder::new()
1162+
.env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
1163+
.build();
1164+
1165+
let s: Ambig = config.get("ambig").unwrap();
1166+
assert_eq!(s.debug_assertions, Some(true));
1167+
assert_eq!(s.debug, None);
1168+
1169+
let config = ConfigBuilder::new().env("CARGO_AMBIG_DEBUG", "0").build();
1170+
let s: Ambig = config.get("ambig").unwrap();
1171+
assert_eq!(s.debug_assertions, None);
1172+
assert_eq!(s.debug, Some(0));
1173+
1174+
let config = ConfigBuilder::new()
1175+
.env("CARGO_AMBIG_DEBUG", "1")
1176+
.env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
1177+
.build();
1178+
let s: Ambig = config.get("ambig").unwrap();
1179+
assert_eq!(s.debug_assertions, Some(true));
1180+
assert_eq!(s.debug, Some(1));
1181+
}

0 commit comments

Comments
 (0)