Skip to content

Commit 778b20f

Browse files
committed
printf: fix %q shell quoting with control chars and quotes
should fix tests/printf/printf-quote.sh upstream change: ba3442f4fa0eec1adff9ff02b95f6b5250999aa3
1 parent 1f4a976 commit 778b20f

File tree

2 files changed

+96
-7
lines changed

2 files changed

+96
-7
lines changed

src/uucore/src/lib/features/quoting_style/shell_quoter.rs

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ impl<'a> NonEscapedShellQuoter<'a> {
4040
dirname: bool,
4141
size_hint: usize,
4242
) -> Self {
43-
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote);
43+
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote, false);
4444
Self {
4545
reference,
4646
quotes,
@@ -108,7 +108,7 @@ pub(super) struct EscapedShellQuoter<'a> {
108108

109109
impl<'a> EscapedShellQuoter<'a> {
110110
pub fn new(reference: &'a [u8], always_quote: bool, dirname: bool, size_hint: usize) -> Self {
111-
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote);
111+
let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote, true);
112112
Self {
113113
reference,
114114
quotes,
@@ -185,11 +185,17 @@ impl Quoter for EscapedShellQuoter<'_> {
185185
}
186186

187187
/// Deduce the initial quoting status from the provided information
188-
fn initial_quoting(input: &[u8], dirname: bool, always_quote: bool) -> (Quotes, bool) {
189-
if input
190-
.iter()
191-
.any(|c| shell_escaped_char_set(dirname).contains(c))
192-
{
188+
fn initial_quoting(
189+
input: &[u8],
190+
dirname: bool,
191+
always_quote: bool,
192+
check_control_chars: bool,
193+
) -> (Quotes, bool) {
194+
let has_special_chars = input.iter().any(|c| {
195+
shell_escaped_char_set(dirname).contains(c) || (check_control_chars && c.is_ascii_control())
196+
});
197+
198+
if has_special_chars {
193199
(Quotes::Single, true)
194200
} else if input.contains(&b'\'') {
195201
(Quotes::Double, true)
@@ -239,3 +245,73 @@ fn finalize_shell_quoter(
239245
buffer
240246
}
241247
}
248+
249+
#[cfg(test)]
250+
mod tests {
251+
use super::*;
252+
253+
#[test]
254+
fn test_initial_quoting() {
255+
// Control chars (0-31 and 0x7F) force single quotes in escape mode
256+
assert_eq!(
257+
initial_quoting(b"\x01", false, false, true),
258+
(Quotes::Single, true)
259+
);
260+
261+
// Control + quote uses single quotes (segmented) in escape mode
262+
assert_eq!(
263+
initial_quoting(b"\x01'\x01", false, false, true),
264+
(Quotes::Single, true)
265+
);
266+
267+
// Simple quote uses double quotes in escape mode
268+
assert_eq!(
269+
initial_quoting(b"a'b", false, false, true),
270+
(Quotes::Double, true)
271+
);
272+
273+
// Shell special chars force single quotes in escape mode
274+
assert_eq!(
275+
initial_quoting(b"test$var", false, false, true),
276+
(Quotes::Single, true)
277+
);
278+
assert_eq!(
279+
initial_quoting(b"test\nline", false, false, true),
280+
(Quotes::Single, true)
281+
);
282+
283+
// Empty string forces quotes in escape mode
284+
assert_eq!(
285+
initial_quoting(b"", false, false, true),
286+
(Quotes::Single, true)
287+
);
288+
289+
// Always quote flag works in escape mode
290+
assert_eq!(
291+
initial_quoting(b"normal", false, true, true),
292+
(Quotes::Single, true)
293+
);
294+
295+
// Normal text doesn't need quoting in escape mode
296+
assert_eq!(
297+
initial_quoting(b"hello", false, false, true),
298+
(Quotes::Single, false)
299+
);
300+
301+
// Dirname affects colon handling in escape mode
302+
assert_eq!(
303+
initial_quoting(b"dir:name", true, false, true),
304+
(Quotes::Single, true)
305+
);
306+
assert_eq!(
307+
initial_quoting(b"file:name", false, false, true),
308+
(Quotes::Single, false)
309+
);
310+
311+
// Control chars ignored in non-escape mode
312+
assert_eq!(
313+
initial_quoting(b"\x01", false, false, false),
314+
(Quotes::Single, false)
315+
);
316+
}
317+
}

tests/by-util/test_printf.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,3 +1492,16 @@ fn test_extreme_field_width_overflow() {
14921492
.fails_with_code(1)
14931493
.stderr_contains("printf: write error"); //could contains additional message like "formatting width too large" not in GNU, thats fine.
14941494
}
1495+
1496+
#[test]
1497+
fn test_q_string_control_chars_with_quotes() {
1498+
// Test %q with control characters and single quotes combined.
1499+
// This tests the fix for the GNU compatibility issue where control
1500+
// characters combined with single quotes should use the segmented
1501+
// quoting approach rather than double quotes.
1502+
let input = "\x01'\x01";
1503+
new_ucmd!()
1504+
.args(&["%q", input])
1505+
.succeeds()
1506+
.stdout_only("''$'\\001'\\'''$'\\001'");
1507+
}

0 commit comments

Comments
 (0)