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
8 changes: 6 additions & 2 deletions src/uu/echo/src/echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::env;
use std::ffi::{OsStr, OsString};
use std::io::{StdoutLock, Write, stdout};
use uucore::error::UResult;
use uucore::format::{FormatChar, OctalParsing, parse_escape_only};
use uucore::format::{EscapeSet, FormatChar, OctalParsing, parse_escape_only};
use uucore::{crate_version, format_usage, os_str_as_bytes};

use uucore::translate;
Expand Down Expand Up @@ -238,7 +238,11 @@ fn execute(
}

if options.escape {
for item in parse_escape_only(bytes, OctalParsing::ThreeDigits) {
for item in parse_escape_only(
bytes,
OctalParsing::ThreeDigits,
EscapeSet::WithoutUnicodeAndQuote,
) {
if item.write(&mut *stdout)?.is_break() {
return Ok(());
}
Expand Down
28 changes: 25 additions & 3 deletions src/uucore/src/lib/features/format/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ pub enum OctalParsing {
ThreeDigits = 3,
}

/// Which escape sequences `parse_escape_code` recognizes.
///
/// `printf` recognizes three sequences that `echo -e` does not: `\"`, and the
/// four- and eight-digit code point escapes `\u` and `\U`. Every other
/// sequence is shared by both, so this only selects whether those three are
/// interpreted or left exactly as written.
#[derive(Clone, Copy)]
pub enum EscapeSet {
/// Interpret `\"`, `\u` and `\U` on top of the shared sequences.
WithUnicodeAndQuote,
/// Only the shared sequences. `\"`, `\u` and `\U` are not escape sequences
/// and are emitted with their backslash, like any other unknown escape.
WithoutUnicodeAndQuote,
}

impl EscapeSet {
fn has_unicode_and_quote(self) -> bool {
matches!(self, Self::WithUnicodeAndQuote)
}
}

#[derive(Clone, Copy)]
enum Base {
Oct(OctalParsing),
Expand Down Expand Up @@ -121,6 +142,7 @@ pub enum EscapeError {
pub fn parse_escape_code(
rest: &mut &[u8],
zero_octal_parsing: OctalParsing,
escape_set: EscapeSet,
) -> Result<EscapedChar, FormatError> {
if let [c, new_rest @ ..] = rest {
// This is for the \NNN syntax for octal sequences.
Expand All @@ -135,7 +157,7 @@ pub fn parse_escape_code(
*rest = new_rest;
match c {
b'\\' => Ok(EscapedChar::Byte(b'\\')),
b'"' => Ok(EscapedChar::Byte(b'"')),
b'"' if escape_set.has_unicode_and_quote() => Ok(EscapedChar::Byte(b'"')),
b'a' => Ok(EscapedChar::Byte(b'\x07')),
b'b' => Ok(EscapedChar::Byte(b'\x08')),
b'c' => Ok(EscapedChar::End),
Expand All @@ -155,14 +177,14 @@ pub fn parse_escape_code(
b'0' => Ok(EscapedChar::Byte(
parse_code(rest, Base::Oct(zero_octal_parsing)).unwrap_or(b'\0'),
)),
b'u' => match parse_unicode(rest, 4) {
b'u' if escape_set.has_unicode_and_quote() => match parse_unicode(rest, 4) {
Ok(c) => Ok(EscapedChar::Char(c)),
Err(EscapeError::MissingHexadecimalNumber) => Err(FormatError::MissingHex(None)),
Err(EscapeError::InvalidCharacters(chars)) => {
Err(FormatError::InvalidCharacter('u', chars, None))
}
},
b'U' => match parse_unicode(rest, 8) {
b'U' if escape_set.has_unicode_and_quote() => match parse_unicode(rest, 8) {
Ok(c) => Ok(EscapedChar::Char(c)),
Err(EscapeError::MissingHexadecimalNumber) => Err(FormatError::MissingHex(None)),
Err(EscapeError::InvalidCharacters(chars)) => {
Expand Down
15 changes: 10 additions & 5 deletions src/uucore/src/lib/features/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub mod human;
pub mod num_format;
mod spec;

pub use self::escape::{EscapedChar, OctalParsing};
pub use self::escape::{EscapeSet, EscapedChar, OctalParsing};
use crate::extendedbigdecimal::ExtendedBigDecimal;
pub use argument::{FormatArgument, FormatArguments};

Expand Down Expand Up @@ -274,9 +274,13 @@ pub fn parse_spec_and_escape(
let start = fmt.len() - current.len();
current = rest;
Some(
parse_escape_code(&mut current, OctalParsing::default())
.map(FormatItem::Char)
.map_err(|e| e.spanned(start..fmt.len() - current.len())),
parse_escape_code(
&mut current,
OctalParsing::default(),
EscapeSet::WithUnicodeAndQuote,
)
.map(FormatItem::Char)
.map_err(|e| e.spanned(start..fmt.len() - current.len())),
)
}
[c, rest @ ..] => {
Expand Down Expand Up @@ -318,14 +322,15 @@ pub fn parse_spec_only(
pub fn parse_escape_only(
fmt: &[u8],
zero_octal_parsing: OctalParsing,
escape_set: EscapeSet,
) -> impl Iterator<Item = EscapedChar> + '_ {
let mut current = fmt;
std::iter::from_fn(move || match current {
[] => None,
[b'\\', rest @ ..] => {
current = rest;
Some(
parse_escape_code(&mut current, zero_octal_parsing)
parse_escape_code(&mut current, zero_octal_parsing, escape_set)
.unwrap_or(EscapedChar::Backslash(b'x')),
)
}
Expand Down
8 changes: 6 additions & 2 deletions src/uucore/src/lib/features/format/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// spell-checker:ignore (vars) intmax ptrdiff padlen

use super::{
ExtendedBigDecimal, FormatChar, FormatError, OctalParsing, check_precision,
EscapeSet, ExtendedBigDecimal, FormatChar, FormatError, OctalParsing, check_precision,
num_format::{
self, Case, FloatVariant, ForceDecimal, Formatter, NumberAlignment, PositiveSign, Prefix,
UnsignedIntVariant,
Expand Down Expand Up @@ -388,7 +388,11 @@ impl Spec {
let bytes = os_str_as_bytes(os_str)?;
let mut parsed = Vec::<u8>::new();

for c in parse_escape_only(bytes, OctalParsing::ThreeDigits) {
for c in parse_escape_only(
bytes,
OctalParsing::ThreeDigits,
EscapeSet::WithUnicodeAndQuote,
) {
match c.write(&mut parsed)? {
ControlFlow::Continue(()) => {}
ControlFlow::Break(()) => {
Expand Down
71 changes: 71 additions & 0 deletions tests/by-util/test_echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,77 @@ fn test_escape_vertical_tab() {
.stdout_only("\x0B\n");
}

#[test]
fn test_escape_unicode_and_quote_not_recognized() {
// `echo -e` has no `\u`, `\U` or `\"` escape, unlike `printf`: the
// backslash and the character following it are printed as given.
for arg in [
r"\u0041",
r"\U00000041",
r"\U0001F600",
r#"\""#,
r"a\u0041b",
r"a\U00000041b",
r#"a\"b"#,
r"\u\u",
] {
new_ucmd!()
.args(&["-e", arg])
.succeeds()
.stdout_only(format!("{arg}\n"));
}
}

#[test]
fn test_escape_malformed_unicode_not_recognized() {
// Truncated, non-hexadecimal, surrogate and out-of-range payloads are not
// special either: nothing is consumed and no error is reported.
for arg in [
r"\u",
r"\U",
r"\u00",
r"\U0000004",
r"\u00zz",
r"\U000000zz",
r"\uD800",
r"\U00110000",
] {
new_ucmd!()
.args(&["-e", arg])
.succeeds()
.stdout_only(format!("{arg}\n"));
}
}

#[test]
fn test_disable_escapes_unicode_and_quote() {
let input_str = r#"\u0041 \U00000041 \""#;
new_ucmd!()
.arg("-E")
.arg(input_str)
.succeeds()
.stdout_only(format!("{input_str}\n"));
}

#[test]
fn test_escape_recognized_sequences_still_expand() {
// The sequences the GNU manual does list for `echo -e` are unaffected.
new_ucmd!()
.args(&["-e", r"\a\b\e\f\n\r\t\v\\"])
.succeeds()
.stdout_only("\x07\x08\x1B\x0C\n\r\t\x0B\\\n");

new_ucmd!()
.args(&["-e", r"\0101\101\x41"])
.succeeds()
.stdout_only("AAA\n");

new_ucmd!()
.args(&["-e", r"a\cb", "c"])
.succeeds()
.stdout_only("a");
}

#[test]
fn test_disable_escapes() {
let input_str = "\\a \\\\ \\b \\r \\e \\f \\x41 \\n a\\cb \\u0100 \\t \\v";
Expand Down
Loading