Skip to content

Commit eb2ea58

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 eb2ea58

File tree

2 files changed

+99
-7
lines changed

2 files changed

+99
-7
lines changed

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

Lines changed: 86 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,20 @@ 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+
const ASCII_CONTROL_LIMIT: u8 = 32;
195+
196+
let has_special_chars = input.iter().any(|c| {
197+
shell_escaped_char_set(dirname).contains(c)
198+
|| (check_control_chars && *c < ASCII_CONTROL_LIMIT)
199+
});
200+
201+
if has_special_chars {
193202
(Quotes::Single, true)
194203
} else if input.contains(&b'\'') {
195204
(Quotes::Double, true)
@@ -239,3 +248,73 @@ fn finalize_shell_quoter(
239248
buffer
240249
}
241250
}
251+
252+
#[cfg(test)]
253+
mod tests {
254+
use super::*;
255+
256+
#[test]
257+
fn test_initial_quoting() {
258+
// Control chars (< 32) force single quotes in escape mode
259+
assert_eq!(
260+
initial_quoting(b"\x01", false, false, true),
261+
(Quotes::Single, true)
262+
);
263+
264+
// Control + quote uses single quotes (segmented) in escape mode
265+
assert_eq!(
266+
initial_quoting(b"\x01'\x01", false, false, true),
267+
(Quotes::Single, true)
268+
);
269+
270+
// Simple quote uses double quotes in escape mode
271+
assert_eq!(
272+
initial_quoting(b"a'b", false, false, true),
273+
(Quotes::Double, true)
274+
);
275+
276+
// Shell special chars force single quotes in escape mode
277+
assert_eq!(
278+
initial_quoting(b"test$var", false, false, true),
279+
(Quotes::Single, true)
280+
);
281+
assert_eq!(
282+
initial_quoting(b"test\nline", false, false, true),
283+
(Quotes::Single, true)
284+
);
285+
286+
// Empty string forces quotes in escape mode
287+
assert_eq!(
288+
initial_quoting(b"", false, false, true),
289+
(Quotes::Single, true)
290+
);
291+
292+
// Always quote flag works in escape mode
293+
assert_eq!(
294+
initial_quoting(b"normal", false, true, true),
295+
(Quotes::Single, true)
296+
);
297+
298+
// Normal text doesn't need quoting in escape mode
299+
assert_eq!(
300+
initial_quoting(b"hello", false, false, true),
301+
(Quotes::Single, false)
302+
);
303+
304+
// Dirname affects colon handling in escape mode
305+
assert_eq!(
306+
initial_quoting(b"dir:name", true, false, true),
307+
(Quotes::Single, true)
308+
);
309+
assert_eq!(
310+
initial_quoting(b"file:name", false, false, true),
311+
(Quotes::Single, false)
312+
);
313+
314+
// Control chars ignored in non-escape mode
315+
assert_eq!(
316+
initial_quoting(b"\x01", false, false, false),
317+
(Quotes::Single, false)
318+
);
319+
}
320+
}

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)