printf: fix %q shell quoting with control chars and quotes (#10816)

This commit is contained in:
Sylvestre Ledru
2026-02-08 14:10:09 -05:00
committed by GitHub
parent 1f4a976a92
commit a9136fb863
2 changed files with 96 additions and 7 deletions
@@ -40,7 +40,7 @@ impl<'a> NonEscapedShellQuoter<'a> {
dirname: bool,
size_hint: usize,
) -> Self {
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote);
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote, false);
Self {
reference,
quotes,
@@ -108,7 +108,7 @@ pub(super) struct EscapedShellQuoter<'a> {
impl<'a> EscapedShellQuoter<'a> {
pub fn new(reference: &'a [u8], always_quote: bool, dirname: bool, size_hint: usize) -> Self {
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote);
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote, true);
Self {
reference,
quotes,
@@ -185,11 +185,17 @@ impl Quoter for EscapedShellQuoter<'_> {
}
/// Deduce the initial quoting status from the provided information
fn initial_quoting(input: &[u8], dirname: bool, always_quote: bool) -> (Quotes, bool) {
if input
.iter()
.any(|c| shell_escaped_char_set(dirname).contains(c))
{
fn initial_quoting(
input: &[u8],
dirname: bool,
always_quote: bool,
check_control_chars: bool,
) -> (Quotes, bool) {
let has_special_chars = input.iter().any(|c| {
shell_escaped_char_set(dirname).contains(c) || (check_control_chars && c.is_ascii_control())
});
if has_special_chars {
(Quotes::Single, true)
} else if input.contains(&b'\'') {
(Quotes::Double, true)
@@ -239,3 +245,73 @@ fn finalize_shell_quoter(
buffer
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_quoting() {
// Control chars (0-31 and 0x7F) force single quotes in escape mode
assert_eq!(
initial_quoting(b"\x01", false, false, true),
(Quotes::Single, true)
);
// Control + quote uses single quotes (segmented) in escape mode
assert_eq!(
initial_quoting(b"\x01'\x01", false, false, true),
(Quotes::Single, true)
);
// Simple quote uses double quotes in escape mode
assert_eq!(
initial_quoting(b"a'b", false, false, true),
(Quotes::Double, true)
);
// Shell special chars force single quotes in escape mode
assert_eq!(
initial_quoting(b"test$var", false, false, true),
(Quotes::Single, true)
);
assert_eq!(
initial_quoting(b"test\nline", false, false, true),
(Quotes::Single, true)
);
// Empty string forces quotes in escape mode
assert_eq!(
initial_quoting(b"", false, false, true),
(Quotes::Single, true)
);
// Always quote flag works in escape mode
assert_eq!(
initial_quoting(b"normal", false, true, true),
(Quotes::Single, true)
);
// Normal text doesn't need quoting in escape mode
assert_eq!(
initial_quoting(b"hello", false, false, true),
(Quotes::Single, false)
);
// Dirname affects colon handling in escape mode
assert_eq!(
initial_quoting(b"dir:name", true, false, true),
(Quotes::Single, true)
);
assert_eq!(
initial_quoting(b"file:name", false, false, true),
(Quotes::Single, false)
);
// Control chars ignored in non-escape mode
assert_eq!(
initial_quoting(b"\x01", false, false, false),
(Quotes::Single, false)
);
}
}
+13
View File
@@ -1492,3 +1492,16 @@ fn test_extreme_field_width_overflow() {
.fails_with_code(1)
.stderr_contains("printf: write error"); //could contains additional message like "formatting width too large" not in GNU, thats fine.
}
#[test]
fn test_q_string_control_chars_with_quotes() {
// Test %q with control characters and single quotes combined.
// This tests the fix for the GNU compatibility issue where control
// characters combined with single quotes should use the segmented
// quoting approach rather than double quotes.
let input = "\x01'\x01";
new_ucmd!()
.args(&["%q", input])
.succeeds()
.stdout_only("''$'\\001'\\'''$'\\001'");
}