Skip to content

Commit

Permalink
stat: handle byte as a format for better display
Browse files Browse the repository at this point in the history
  • Loading branch information
sylvestre committed Dec 7, 2024
1 parent a945717 commit b7fc62e
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 24 deletions.
73 changes: 49 additions & 24 deletions src/uu/stat/src/stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 39 additions & 0 deletions tests/by-util/test_stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
3 changes: 3 additions & 0 deletions util/build-gnu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b7fc62e

Please sign in to comment.