From b7fc62e7aa9d487cbece02644a8fe3ef0cde8a7e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 7 Dec 2024 10:10:49 +0100 Subject: [PATCH] stat: handle byte as a format for better display --- src/uu/stat/src/stat.rs | 73 +++++++++++++++++++++++++------------- tests/by-util/test_stat.rs | 39 ++++++++++++++++++++ util/build-gnu.sh | 3 ++ 3 files changed, 91 insertions(+), 24 deletions(-) diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index cef94169026..0a6a4a6c4ef 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -22,6 +22,7 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; use std::fs::{FileType, Metadata}; +use std::io::Write; use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::unix::prelude::OsStrExt; use std::path::Path; @@ -119,6 +120,7 @@ impl std::str::FromStr for QuotingStyle { #[derive(Debug, PartialEq, Eq)] enum Token { Char(char), + Byte(u8), Directive { flag: Flags, width: usize, @@ -362,6 +364,7 @@ fn get_quoted_file_name( fn process_token_fs(t: &Token, meta: StatFs, display_name: &str) { match *t { + Token::Byte(byte) => write_raw_byte(byte), Token::Char(c) => print!("{c}"), Token::Directive { flag, @@ -512,6 +515,10 @@ fn print_unsigned_hex( pad_and_print(&s, flags.left, width, padding_char); } +fn write_raw_byte(byte: u8) { + std::io::stdout().write_all(&[byte]).unwrap(); +} + impl Stater { fn process_flags(chars: &[char], i: &mut usize, bound: usize, flag: &mut Flags) { while *i < bound { @@ -614,33 +621,49 @@ impl Stater { return Token::Char('\\'); } match chars[*i] { - 'x' if *i + 1 < bound => { - if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) { - *i += offset; - Token::Char(c) - } else { - show_warning!("unrecognized escape '\\x'"); - Token::Char('x') + 'a' => Token::Byte(0x07), // BEL + 'b' => Token::Byte(0x08), // Backspace + 'f' => Token::Byte(0x0C), // Form feed + 'n' => Token::Byte(0x0A), // Line feed + 'r' => Token::Byte(0x0D), // Carriage return + 't' => Token::Byte(0x09), // Horizontal tab + '\\' => Token::Byte(b'\\'), // Backslash + '\'' => Token::Byte(b'\''), // Single quote + '"' => Token::Byte(b'"'), // Double quote + '0'..='7' => { + // Parse octal escape sequence (up to 3 digits) + let mut value = 0u8; + let mut count = 0; + while *i < bound && count < 3 { + if let Some(digit) = chars[*i].to_digit(8) { + value = value * 8 + digit as u8; + *i += 1; + count += 1; + } else { + break; + } } + *i -= 1; // Adjust index to account for the outer loop increment + Token::Byte(value) } - '0'..='7' => { - let (c, offset) = format_str[*i..].scan_char(8).unwrap(); - *i += offset - 1; - Token::Char(c) + 'x' => { + // Parse hexadecimal escape sequence + if *i + 1 < bound { + if let Some((c, offset)) = format_str[*i + 1..].scan_char(16) { + *i += offset; + Token::Byte(c as u8) + } else { + show_warning!("unrecognized escape '\\x'"); + Token::Byte(b'x') + } + } else { + show_warning!("incomplete hex escape '\\x'"); + Token::Byte(b'x') + } } - '"' => Token::Char('"'), - '\\' => Token::Char('\\'), - 'a' => Token::Char('\x07'), - 'b' => Token::Char('\x08'), - 'e' => Token::Char('\x1B'), - 'f' => Token::Char('\x0C'), - 'n' => Token::Char('\n'), - 'r' => Token::Char('\r'), - 't' => Token::Char('\t'), - 'v' => Token::Char('\x0B'), - c => { - show_warning!("unrecognized escape '\\{}'", c); - Token::Char(c) + other => { + show_warning!("unrecognized escape '\\{}'", other); + Token::Byte(other as u8) } } } @@ -773,7 +796,9 @@ impl Stater { from_user: bool, ) -> Result<(), i32> { match *t { + Token::Byte(byte) => write_raw_byte(byte), Token::Char(c) => print!("{c}"), + Token::Directive { flag, width, diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 002f9ee2293..cb52f19e055 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -373,3 +373,42 @@ fn test_quoting_style_locale() { .stdout_is("\"'\"\n") .succeeded(); } + +#[test] +fn test_printf_octal_1() { + let ts = TestScenario::new(util_name!()); + let expected_stdout = vec![0x0A, 0xFF]; // Newline + byte 255 + ts.ucmd() + .args(&["--printf=\\012\\377", "."]) + .succeeds() + .stdout_is_bytes(expected_stdout); +} + +#[test] +fn test_printf_octal_2() { + let ts = TestScenario::new(util_name!()); + let expected_stdout = vec![b'.', 0x0A, b'a', 0xFF, b'b']; // ".\naxÿb" + ts.ucmd() + .args(&["--printf=.\\012a\\377b", "."]) + .succeeds() + .stdout_is_bytes(expected_stdout); +} + +#[test] +fn test_printf_hex_3() { + let ts = TestScenario::new(util_name!()); + ts.ucmd() + .args(&["--printf=\\x", "."]) + .run() + .stderr_contains("warning: incomplete hex escape"); +} + +#[test] +fn test_printf_bel_etc() { + let ts = TestScenario::new(util_name!()); + let expected_stdout = vec![0x07, 0x08, 0x0C, 0x0A, 0x0D, 0x09]; // BEL, BS, FF, LF, CR, TAB + ts.ucmd() + .args(&["--printf=\\a\\b\\f\\n\\r\\t", "."]) + .succeeds() + .stdout_is_bytes(expected_stdout); +} diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 684187733de..e33e64429bf 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -204,6 +204,9 @@ sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not # Our message is a bit better sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh +# Our message is better +sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl + sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh