Skip to content

Commit bbe81b5

Browse files
authored
Merge pull request #640 from Jongy/handle-non-utf8-env
Handle non Unicode env keys/values
2 parents 370c939 + b5d342e commit bbe81b5

File tree

2 files changed

+111
-6
lines changed

2 files changed

+111
-6
lines changed

src/env.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::env;
2+
use std::ffi::OsString;
23

34
#[cfg(feature = "convert-case")]
45
use convert_case::{Case, Casing};
@@ -7,6 +8,7 @@ use crate::error::Result;
78
use crate::map::Map;
89
use crate::source::Source;
910
use crate::value::{Value, ValueKind};
11+
use crate::ConfigError;
1012

1113
/// An environment source collects a dictionary of environment variables values into a hierarchical
1214
/// config Value type. We have to be aware how the config tree is created from the environment
@@ -243,10 +245,16 @@ impl Source for Environment {
243245
.as_ref()
244246
.map(|prefix| format!("{prefix}{prefix_separator}").to_lowercase());
245247

246-
let collector = |(key, value): (String, String)| {
248+
let collector = |(key, value): (OsString, OsString)| {
249+
let key = match key.into_string() {
250+
Ok(key) => key,
251+
// Key is not valid unicode, skip it
252+
Err(_) => return Ok(()),
253+
};
254+
247255
// Treat empty environment variables as unset
248256
if self.ignore_empty && value.is_empty() {
249-
return;
257+
return Ok(());
250258
}
251259

252260
let mut key = key.to_lowercase();
@@ -260,10 +268,18 @@ impl Source for Environment {
260268
}
261269
} else {
262270
// Skip this key
263-
return;
271+
return Ok(());
264272
}
265273
}
266274

275+
// At this point, we don't know if the key is required or not.
276+
// Therefore if the value is not a valid unicode string, we error out.
277+
let value = value.into_string().map_err(|os_string| {
278+
ConfigError::Message(format!(
279+
"env variable {key:?} contains non-Unicode data: {os_string:?}"
280+
))
281+
})?;
282+
267283
// If separator is given replace with `.`
268284
if !separator.is_empty() {
269285
key = key.replace(separator, ".");
@@ -308,12 +324,18 @@ impl Source for Environment {
308324
};
309325

310326
m.insert(key, Value::new(Some(&uri), value));
327+
328+
Ok(())
311329
};
312330

313331
match &self.source {
314-
Some(source) => source.clone().into_iter().for_each(collector),
315-
None => env::vars().for_each(collector),
316-
}
332+
Some(source) => source
333+
.clone()
334+
.into_iter()
335+
.map(|(key, value)| (key.into(), value.into()))
336+
.try_for_each(collector),
337+
None => env::vars_os().try_for_each(collector),
338+
}?;
317339

318340
Ok(m)
319341
}

tests/testsuite/env.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,3 +721,86 @@ fn test_parse_uint_default() {
721721
let config: TestUint = config.try_deserialize().unwrap();
722722
assert_eq!(config.int_val, 42);
723723
}
724+
725+
#[cfg(any(unix, windows))]
726+
#[cfg(test)]
727+
mod unicode_tests {
728+
use std::ffi::OsString;
729+
730+
use super::*;
731+
732+
fn make_invalid_unicode_os_string() -> OsString {
733+
let string = {
734+
#[cfg(unix)]
735+
{
736+
use std::os::unix::ffi::OsStringExt;
737+
738+
OsString::from_vec(vec![0xff])
739+
}
740+
#[cfg(windows)]
741+
{
742+
use std::os::windows::ffi::OsStringExt;
743+
744+
OsString::from_wide(&[0xd800]) // unpaired high surrogate
745+
}
746+
};
747+
748+
assert!(string.to_str().is_none());
749+
750+
string
751+
}
752+
753+
#[test]
754+
fn test_invalid_unicode_key_ignored() {
755+
temp_env::with_vars(
756+
vec![
757+
(make_invalid_unicode_os_string(), Some("abc")),
758+
("A_B_C".into(), Some("abc")),
759+
],
760+
|| {
761+
let vars = Environment::default().collect().unwrap();
762+
763+
assert!(vars.contains_key("a_b_c"));
764+
},
765+
);
766+
}
767+
768+
#[test]
769+
fn test_invalid_unicode_value_filtered() {
770+
temp_env::with_vars(
771+
vec![
772+
("invalid_value1", Some(make_invalid_unicode_os_string())),
773+
("valid_value2", Some("valid".into())),
774+
],
775+
|| {
776+
let vars = Environment::with_prefix("valid")
777+
.keep_prefix(true)
778+
.collect()
779+
.unwrap();
780+
781+
assert!(!vars.contains_key("invalid_value1"));
782+
assert!(vars.contains_key("valid_value2"));
783+
},
784+
);
785+
}
786+
787+
#[test]
788+
fn test_invalid_unicode_value_not_filtered() {
789+
temp_env::with_vars(
790+
vec![("invalid_value1", Some(make_invalid_unicode_os_string()))],
791+
|| {
792+
let result = Environment::default().collect();
793+
794+
#[cfg(unix)]
795+
let expected =
796+
str![[r#"env variable "invalid_value1" contains non-Unicode data: "/xFF""#]];
797+
#[cfg(windows)]
798+
let expected = str![[
799+
r#"env variable "invalid_value1" contains non-Unicode data: "/u{d800}""#
800+
]];
801+
802+
assert_data_eq!(result.unwrap_err().to_string(), expected);
803+
},
804+
);
805+
}
806+
}

0 commit comments

Comments
 (0)