Add ANSI C quoting to export env parsing (#32404)

Follow up to #31799 to support ansi-c quoting. This is used by
nix/direnv

Release Notes:

- N/A
This commit is contained in:
Stanislav Alekseev 2025-06-11 05:15:35 +03:00 committed by GitHub
parent b4e558ce3d
commit 84eca53319
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -130,7 +130,10 @@ fn parse_name_and_terminator(input: &str, terminator: char) -> Option<(Cow<'_, s
}
fn parse_literal_and_terminator(input: &str, terminator: char) -> Option<(Cow<'_, str>, &str)> {
if let Some((literal, rest)) = parse_literal_single_quoted(input) {
if let Some((literal, rest)) = parse_literal_ansi_c_quoted(input) {
let rest = rest.strip_prefix(terminator)?;
Some((Cow::Owned(literal), rest))
} else if let Some((literal, rest)) = parse_literal_single_quoted(input) {
let rest = rest.strip_prefix(terminator)?;
Some((Cow::Borrowed(literal), rest))
} else if let Some((literal, rest)) = parse_literal_double_quoted(input) {
@ -143,6 +146,52 @@ fn parse_literal_and_terminator(input: &str, terminator: char) -> Option<(Cow<'_
}
}
/// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
fn parse_literal_ansi_c_quoted(input: &str) -> Option<(String, &str)> {
let rest = input.strip_prefix("$'")?;
let mut char_indices = rest.char_indices();
let mut escaping = false;
let (literal, rest) = loop {
let (index, char) = char_indices.next()?;
if char == '\'' && !escaping {
break (&rest[..index], &rest[index + 1..]);
} else {
escaping = !escaping && char == '\\';
}
};
let mut result = String::new();
let mut chars = literal.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('r') => result.push('\r'),
Some('\\') => result.push('\\'),
Some('\'') => result.push('\''),
Some('"') => result.push('"'),
Some('a') => result.push('\x07'), // bell
Some('b') => result.push('\x08'), // backspace
Some('f') => result.push('\x0C'), // form feed
Some('v') => result.push('\x0B'), // vertical tab
Some('0') => result.push('\0'), // null
Some(other) => {
// For unknown escape sequences, keep the backslash and character
result.push('\\');
result.push(other);
}
None => result.push('\\'), // trailing backslash
}
} else {
result.push(ch);
}
}
Some((result, rest))
}
/// https://www.gnu.org/software/bash/manual/html_node/Single-Quotes.html
fn parse_literal_single_quoted(input: &str) -> Option<(&str, &str)> {
input.strip_prefix('\'')?.split_once('\'')
@ -220,6 +269,7 @@ mod tests {
\"wo\
rld\"\n!\\
!"
export foo=$'hello\nworld'
"#};
let expected_values = [
@ -244,6 +294,7 @@ mod tests {
Some(indoc::indoc! {r#"
`Hello`
"world"\n!\!"#}),
Some("hello\nworld"),
];
let expected = expected_values
.into_iter()
@ -304,4 +355,43 @@ mod tests {
assert_eq!(expected, actual);
assert_eq!(rest, "\nrest");
}
#[test]
fn test_parse_literal_ansi_c_quoted() {
let (actual, rest) = parse_literal_ansi_c_quoted("$'hello\\nworld'\nrest").unwrap();
assert_eq!(actual, "hello\nworld");
assert_eq!(rest, "\nrest");
let (actual, rest) = parse_literal_ansi_c_quoted("$'tab\\there'\nrest").unwrap();
assert_eq!(actual, "tab\there");
assert_eq!(rest, "\nrest");
let (actual, rest) = parse_literal_ansi_c_quoted("$'quote\\'\\'end'\nrest").unwrap();
assert_eq!(actual, "quote''end");
assert_eq!(rest, "\nrest");
let (actual, rest) = parse_literal_ansi_c_quoted("$'backslash\\\\end'\nrest").unwrap();
assert_eq!(actual, "backslash\\end");
assert_eq!(rest, "\nrest");
}
#[test]
fn test_parse_buildphase_export() {
let input = r#"export buildPhase=$'{ echo "------------------------------------------------------------";\n echo " WARNING: the existence of this path is not guaranteed.";\n echo " It is an internal implementation detail for pkgs.mkShell.";\n echo "------------------------------------------------------------";\n echo;\n # Record all build inputs as runtime dependencies\n export;\n} >> "$out"\n'
"#;
let expected_value = r#"{ echo "------------------------------------------------------------";
echo " WARNING: the existence of this path is not guaranteed.";
echo " It is an internal implementation detail for pkgs.mkShell.";
echo "------------------------------------------------------------";
echo;
# Record all build inputs as runtime dependencies
export;
} >> "$out"
"#;
let ((name, value), _rest) = parse_declaration(input).unwrap();
assert_eq!(name, "buildPhase");
assert_eq!(value.as_deref(), Some(expected_value));
}
}