Skip to content
Merged
Show file tree
Hide file tree
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
90 changes: 83 additions & 7 deletions src/uucore/src/lib/features/quoting_style/shell_quoter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 changes: 13 additions & 0 deletions tests/by-util/test_printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'");
}
Loading