Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions dotenvy/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,28 @@ impl<'a> LineParser<'a> {
Ok(Some((key, parsed_value)))
}

#[inline(always)]
fn valid_env_var_character(c: char) -> bool {
// POSIX 2018 § 8.1 tells us that environment variables can be any ASCII portable
// character other than `=` and `\0`. Because we're loading them from a file, we also
// rule out whitespace and control characters
// See
// <https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/basedefs/V1_chap06.html#tagtcjh_3>
// for the range of portable ASCII; it's essentially 0x21 through 0x7e, with some
// whitespace that we can ignore
c.is_ascii() && c >= '\u{0021}' && c <= '\u{007e}' && c != '='
}

fn parse_key(&mut self) -> Result<String, ParseBufError> {
// POSIX: "Other applications may have difficulty dealing with environment variable names that start with a digit.
// For this reason, use of such names is not recommended anywhere."
if !self
.line
.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
.starts_with(|c| Self::valid_env_var_character(c) && !c.is_numeric())
{
return Err(self.err());
}
let index = match self
.line
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.'))
{
let index = match self.line.find(|c| !Self::valid_env_var_character(c)) {
Some(index) => index,
None => self.line.len(),
};
Expand Down Expand Up @@ -289,6 +300,8 @@ KEY10 ="whitespace before ="
KEY11= "whitespace after ="
export="export as key"
export SHELL_LOVER=1
export KEYS:CAN:HAVE_COLONS=1
%TEMP%=/tmp
"#
.as_bytes(),
);
Expand All @@ -307,6 +320,8 @@ export SHELL_LOVER=1
("KEY11", "whitespace after ="),
("export", "export as key"),
("SHELL_LOVER", "1"),
("KEYS:CAN:HAVE_COLONS", "1"),
("%TEMP%", "/tmp"),
]
.into_iter()
.map(|(key, value)| (key.to_owned(), value.to_owned()));
Expand All @@ -316,7 +331,7 @@ export SHELL_LOVER=1
assert_eq!(expected, actual?);
count += 1;
}
assert_eq!(count, 13);
assert_eq!(count, 15);
Ok(())
}

Expand Down Expand Up @@ -554,6 +569,17 @@ mod substitution_tests {
vec![("KEY2", "_2"), ("KEY", "><>_2<")],
)
}

#[test]
fn nonalphanumeric_substitutions() -> Result<(), ParseBufError> {
assert_str(
r"
KEY:ONE=one
KEY:TWO=${KEY:ONE}11
",
vec![("KEY:ONE", "one"), ("KEY:TWO", "one11")],
)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -588,8 +614,8 @@ mod error_tests {
}

#[test]
fn should_not_allow_dot_as_first_char_of_key() {
let invalid_key = ".KEY=value";
fn should_not_allow_number_as_first_char_of_key() {
let invalid_key = "1KEY=value";

let iter = Iter::new(invalid_key.as_bytes()).collect::<Vec<_>>();

Expand All @@ -601,7 +627,7 @@ mod error_tests {

#[test]
fn should_not_parse_invalid_format() {
let invalid_fmt = r"<><><>";
let invalid_fmt = r"======";
let iter = Iter::new(invalid_fmt.as_bytes()).collect::<Vec<_>>();

assert!(matches!(
Expand All @@ -610,6 +636,29 @@ mod error_tests {
));
}

#[test]
fn should_not_parse_invalid_keys() {
for (key, bad_index) in [
("0foo", 0), // starts with a digit, discouraged by posix
("foö", 2), // trailing unicode
("Πoo", 0), // leading unicode
("foo\x07", 3), // contains control code
("bar\x00", 3), // contains embedded nul byte
("baz zed", 4), // whitespace
] {
let invalid_fmt = format!("{key}=1");
let iter = Iter::new(invalid_fmt.as_bytes()).collect::<Vec<_>>();

assert!(
matches!(
iter[0],
Err(ParseBufError::LineParse(ref v, idx)) if *v == invalid_fmt && idx == bad_index
),
"should fail to parse {key:?}"
);
}
}

#[test]
fn should_not_parse_invalid_escape() {
let invalid_esc = r">\f<";
Expand Down