use clap::builder::ValueParser;
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError};
use uucore::fs::display_permissions;
use uucore::fsext::{
pretty_filetype, pretty_fstype, pretty_time, read_fs_list, statfs, BirthTime, FsMeta,
};
use uucore::libc::mode_t;
use uucore::{entries, format_usage, show_error, show_warning};
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use std::borrow::Cow;
use std::convert::AsRef;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::os::unix::prelude::OsStrExt;
use std::path::Path;
const ABOUT: &str = "Display file or file system status.";
const USAGE: &str = "{} [OPTION]... FILE...";
mod options {
pub const DEREFERENCE: &str = "dereference";
pub const FILE_SYSTEM: &str = "file-system";
pub const FORMAT: &str = "format";
pub const PRINTF: &str = "printf";
pub const TERSE: &str = "terse";
pub const FILES: &str = "files";
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
struct Flags {
alter: bool,
zero: bool,
left: bool,
space: bool,
sign: bool,
group: bool,
}
fn check_bound(slice: &str, bound: usize, beg: usize, end: usize) -> UResult<()> {
if end >= bound {
return Err(USimpleError::new(
1,
format!("{}: invalid directive", slice[beg..end].quote()),
));
}
Ok(())
}
enum Padding {
Zero,
Space,
}
fn pad_and_print(result: &str, left: bool, width: usize, padding: Padding) {
match (left, padding) {
(false, Padding::Zero) => print!("{result:0>width$}"),
(false, Padding::Space) => print!("{result:>width$}"),
(true, Padding::Zero) => print!("{result:0<width$}"),
(true, Padding::Space) => print!("{result:<width$}"),
};
}
#[derive(Debug)]
pub enum OutputType {
Str(String),
Integer(i64),
Unsigned(u64),
UnsignedHex(u64),
UnsignedOct(u32),
Unknown,
}
#[derive(Debug, PartialEq, Eq)]
enum Token {
Char(char),
Directive {
flag: Flags,
width: usize,
precision: Option<usize>,
format: char,
},
}
trait ScanUtil {
fn scan_num<F>(&self) -> Option<(F, usize)>
where
F: std::str::FromStr;
fn scan_char(&self, radix: u32) -> Option<(char, usize)>;
}
impl ScanUtil for str {
fn scan_num<F>(&self) -> Option<(F, usize)>
where
F: std::str::FromStr,
{
let mut chars = self.chars();
let mut i = 0;
match chars.next() {
Some('-' | '+' | '0'..='9') => i += 1,
_ => return None,
}
for c in chars {
match c {
'0'..='9' => i += 1,
_ => break,
}
}
if i > 0 {
F::from_str(&self[..i]).ok().map(|x| (x, i))
} else {
None
}
}
fn scan_char(&self, radix: u32) -> Option<(char, usize)> {
let count = match radix {
8 => 3,
16 => 2,
_ => return None,
};
let chars = self.chars().enumerate();
let mut res = 0;
let mut offset = 0;
for (i, c) in chars {
if i >= count {
break;
}
match c.to_digit(radix) {
Some(digit) => {
let tmp = res * radix + digit;
if tmp < 256 {
res = tmp;
} else {
break;
}
}
None => break,
}
offset = i + 1;
}
if offset > 0 {
Some((res as u8 as char, offset))
} else {
None
}
}
}
fn group_num(s: &str) -> Cow<str> {
let is_negative = s.starts_with('-');
assert!(is_negative || s.chars().take(1).all(|c| c.is_ascii_digit()));
assert!(s.chars().skip(1).all(|c| c.is_ascii_digit()));
if s.len() < 4 {
return s.into();
}
let mut res = String::with_capacity((s.len() - 1) / 3);
let s = if is_negative {
res.push('-');
&s[1..]
} else {
s
};
let mut alone = (s.len() - 1) % 3 + 1;
res.push_str(&s[..alone]);
while alone != s.len() {
res.push(',');
res.push_str(&s[alone..alone + 3]);
alone += 3;
}
res.into()
}
struct Stater {
follow: bool,
show_fs: bool,
from_user: bool,
files: Vec<OsString>,
mount_list: Option<Vec<String>>,
default_tokens: Vec<Token>,
default_dev_tokens: Vec<Token>,
}
#[allow(clippy::cognitive_complexity)]
fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option<usize>) {
let padding_char = if flags.zero && !flags.left && precision.is_none() {
Padding::Zero
} else {
Padding::Space
};
match output {
OutputType::Str(s) => {
let s = match precision {
Some(p) if p < s.len() => &s[..p],
_ => s,
};
pad_and_print(s, flags.left, width, Padding::Space);
}
OutputType::Integer(num) => {
let num = num.to_string();
let arg = if flags.group {
group_num(&num)
} else {
Cow::Borrowed(num.as_str())
};
let prefix = if flags.sign {
"+"
} else if flags.space {
" "
} else {
""
};
let extended = format!(
"{prefix}{arg:0>precision$}",
precision = precision.unwrap_or(0)
);
pad_and_print(&extended, flags.left, width, padding_char);
}
OutputType::Unsigned(num) => {
let num = num.to_string();
let s = if flags.group {
group_num(&num)
} else {
Cow::Borrowed(num.as_str())
};
let s = format!("{s:0>precision$}", precision = precision.unwrap_or(0));
pad_and_print(&s, flags.left, width, padding_char);
}
OutputType::UnsignedOct(num) => {
let prefix = if flags.alter { "0" } else { "" };
let s = format!(
"{prefix}{num:0>precision$o}",
precision = precision.unwrap_or(0)
);
pad_and_print(&s, flags.left, width, padding_char);
}
OutputType::UnsignedHex(num) => {
let prefix = if flags.alter { "0x" } else { "" };
let s = format!(
"{prefix}{num:0>precision$x}",
precision = precision.unwrap_or(0)
);
pad_and_print(&s, flags.left, width, padding_char);
}
OutputType::Unknown => print!("?"),
}
}
impl Stater {
fn generate_tokens(format_str: &str, use_printf: bool) -> UResult<Vec<Token>> {
let mut tokens = Vec::new();
let bound = format_str.len();
let chars = format_str.chars().collect::<Vec<char>>();
let mut i = 0;
while i < bound {
match chars[i] {
'%' => {
let old = i;
i += 1;
if i >= bound {
tokens.push(Token::Char('%'));
continue;
}
if chars[i] == '%' {
tokens.push(Token::Char('%'));
i += 1;
continue;
}
let mut flag = Flags::default();
while i < bound {
match chars[i] {
'#' => flag.alter = true,
'0' => flag.zero = true,
'-' => flag.left = true,
' ' => flag.space = true,
'+' => flag.sign = true,
'\'' => flag.group = true,
'I' => unimplemented!(),
_ => break,
}
i += 1;
}
check_bound(format_str, bound, old, i)?;
let mut width = 0;
let mut precision = None;
let mut j = i;
if let Some((field_width, offset)) = format_str[j..].scan_num::<usize>() {
width = field_width;
j += offset;
}
check_bound(format_str, bound, old, j)?;
if chars[j] == '.' {
j += 1;
check_bound(format_str, bound, old, j)?;
match format_str[j..].scan_num::<i32>() {
Some((value, offset)) => {
if value >= 0 {
precision = Some(value as usize);
}
j += offset;
}
None => precision = Some(0),
}
check_bound(format_str, bound, old, j)?;
}
i = j;
tokens.push(Token::Directive {
width,
flag,
precision,
format: chars[i],
});
}
'\\' => {
if !use_printf {
tokens.push(Token::Char('\\'));
} else {
i += 1;
if i >= bound {
show_warning!("backslash at end of format");
tokens.push(Token::Char('\\'));
continue;
}
match chars[i] {
'x' if i + 1 < bound => {
if let Some((c, offset)) = format_str[i + 1..].scan_char(16) {
tokens.push(Token::Char(c));
i += offset;
} else {
show_warning!("unrecognized escape '\\x'");
tokens.push(Token::Char('x'));
}
}
'0'..='7' => {
let (c, offset) = format_str[i..].scan_char(8).unwrap();
tokens.push(Token::Char(c));
i += offset - 1;
}
'"' => tokens.push(Token::Char('"')),
'\\' => tokens.push(Token::Char('\\')),
'a' => tokens.push(Token::Char('\x07')),
'b' => tokens.push(Token::Char('\x08')),
'e' => tokens.push(Token::Char('\x1B')),
'f' => tokens.push(Token::Char('\x0C')),
'n' => tokens.push(Token::Char('\n')),
'r' => tokens.push(Token::Char('\r')),
't' => tokens.push(Token::Char('\t')),
'v' => tokens.push(Token::Char('\x0B')),
c => {
show_warning!("unrecognized escape '\\{}'", c);
tokens.push(Token::Char(c));
}
}
}
}
c => tokens.push(Token::Char(c)),
}
i += 1;
}
if !use_printf && !format_str.ends_with('\n') {
tokens.push(Token::Char('\n'));
}
Ok(tokens)
}
fn new(matches: &ArgMatches) -> UResult<Self> {
let files = matches
.get_many::<OsString>(options::FILES)
.map(|v| v.map(OsString::from).collect())
.unwrap_or_default();
let format_str = if matches.contains_id(options::PRINTF) {
matches
.get_one::<String>(options::PRINTF)
.expect("Invalid format string")
} else {
matches
.get_one::<String>(options::FORMAT)
.map(|s| s.as_str())
.unwrap_or("")
};
let use_printf = matches.contains_id(options::PRINTF);
let terse = matches.get_flag(options::TERSE);
let show_fs = matches.get_flag(options::FILE_SYSTEM);
let default_tokens = if format_str.is_empty() {
Self::generate_tokens(&Self::default_format(show_fs, terse, false), use_printf)?
} else {
Self::generate_tokens(format_str, use_printf)?
};
let default_dev_tokens =
Self::generate_tokens(&Self::default_format(show_fs, terse, true), use_printf)?;
let mount_list = if show_fs {
None
} else {
let mut mount_list = read_fs_list()
.map_err_context(|| "cannot read table of mounted file systems".into())?
.iter()
.map(|mi| mi.mount_dir.clone())
.collect::<Vec<String>>();
mount_list.sort();
mount_list.reverse();
Some(mount_list)
};
Ok(Self {
follow: matches.get_flag(options::DEREFERENCE),
show_fs,
from_user: !format_str.is_empty(),
files,
default_tokens,
default_dev_tokens,
mount_list,
})
}
fn find_mount_point<P: AsRef<Path>>(&self, p: P) -> Option<String> {
let path = p.as_ref().canonicalize().ok()?;
for root in self.mount_list.as_ref()? {
if path.starts_with(root) {
return Some(root.clone());
}
}
None
}
fn exec(&self) -> i32 {
let mut stdin_is_fifo = false;
if cfg!(unix) {
if let Ok(md) = fs::metadata("/dev/stdin") {
stdin_is_fifo = md.file_type().is_fifo();
}
}
let mut ret = 0;
for f in &self.files {
ret |= self.do_stat(f, stdin_is_fifo);
}
ret
}
fn do_stat(&self, file: &OsStr, stdin_is_fifo: bool) -> i32 {
let display_name = file.to_string_lossy();
let file = if cfg!(unix) && display_name == "-" {
if let Ok(p) = Path::new("/dev/stdin").canonicalize() {
p.into_os_string()
} else {
OsString::from("/dev/stdin")
}
} else {
OsString::from(file)
};
if !self.show_fs {
let result = if self.follow || stdin_is_fifo && display_name == "-" {
fs::metadata(&file)
} else {
fs::symlink_metadata(&file)
};
match result {
Ok(meta) => {
let file_type = meta.file_type();
let tokens = if self.from_user
|| !(file_type.is_char_device() || file_type.is_block_device())
{
&self.default_tokens
} else {
&self.default_dev_tokens
};
for t in tokens.iter() {
match *t {
Token::Char(c) => print!("{}", c),
Token::Directive {
flag,
width,
precision,
format,
} => {
let output = match format {
'a' => OutputType::UnsignedOct(0o7777 & meta.mode()),
'A' => OutputType::Str(display_permissions(&meta, true)),
'b' => OutputType::Unsigned(meta.blocks()),
'B' => OutputType::Unsigned(512),
'd' => OutputType::Unsigned(meta.dev()),
'D' => OutputType::UnsignedHex(meta.dev()),
'f' => OutputType::UnsignedHex(meta.mode() as u64),
'F' => OutputType::Str(
pretty_filetype(meta.mode() as mode_t, meta.len())
.to_owned(),
),
'g' => OutputType::Unsigned(meta.gid() as u64),
'G' => {
let group_name = entries::gid2grp(meta.gid())
.unwrap_or_else(|_| "UNKNOWN".to_owned());
OutputType::Str(group_name)
}
'h' => OutputType::Unsigned(meta.nlink()),
'i' => OutputType::Unsigned(meta.ino()),
'm' => OutputType::Str(self.find_mount_point(&file).unwrap()),
'n' => OutputType::Str(display_name.to_string()),
'N' => {
let file_name = if file_type.is_symlink() {
let dst = match fs::read_link(&file) {
Ok(path) => path,
Err(e) => {
println!("{}", e);
return 1;
}
};
format!("{} -> {}", display_name.quote(), dst.quote())
} else {
display_name.to_string()
};
OutputType::Str(file_name)
}
'o' => OutputType::Unsigned(meta.blksize()),
's' => OutputType::Integer(meta.len() as i64),
't' => OutputType::UnsignedHex(meta.rdev() >> 8),
'T' => OutputType::UnsignedHex(meta.rdev() & 0xff),
'u' => OutputType::Unsigned(meta.uid() as u64),
'U' => {
let user_name = entries::uid2usr(meta.uid())
.unwrap_or_else(|_| "UNKNOWN".to_owned());
OutputType::Str(user_name)
}
'w' => OutputType::Str(meta.pretty_birth()),
'W' => OutputType::Unsigned(meta.birth()),
'x' => OutputType::Str(pretty_time(
meta.atime(),
meta.atime_nsec(),
)),
'X' => OutputType::Integer(meta.atime()),
'y' => OutputType::Str(pretty_time(
meta.mtime(),
meta.mtime_nsec(),
)),
'Y' => OutputType::Integer(meta.mtime()),
'z' => OutputType::Str(pretty_time(
meta.ctime(),
meta.ctime_nsec(),
)),
'Z' => OutputType::Integer(meta.ctime()),
_ => OutputType::Unknown,
};
print_it(&output, flag, width, precision);
}
}
}
}
Err(e) => {
show_error!("cannot stat {}: {}", display_name.quote(), e);
return 1;
}
}
} else {
#[cfg(unix)]
let p = file.as_bytes();
#[cfg(not(unix))]
let p = file.into_string().unwrap();
match statfs(p) {
Ok(meta) => {
let tokens = &self.default_tokens;
for t in tokens.iter() {
match *t {
Token::Char(c) => print!("{}", c),
Token::Directive {
flag,
width,
precision,
format,
} => {
let output = match format {
'a' => OutputType::Unsigned(meta.avail_blocks()),
'b' => OutputType::Unsigned(meta.total_blocks()),
'c' => OutputType::Unsigned(meta.total_file_nodes()),
'd' => OutputType::Unsigned(meta.free_file_nodes()),
'f' => OutputType::Unsigned(meta.free_blocks()),
'i' => OutputType::UnsignedHex(meta.fsid()),
'l' => OutputType::Unsigned(meta.namelen()),
'n' => OutputType::Str(display_name.to_string()),
's' => OutputType::Unsigned(meta.io_size()),
'S' => OutputType::Integer(meta.block_size()),
't' => OutputType::UnsignedHex(meta.fs_type() as u64),
'T' => OutputType::Str(pretty_fstype(meta.fs_type()).into()),
_ => OutputType::Unknown,
};
print_it(&output, flag, width, precision);
}
}
}
}
Err(e) => {
show_error!(
"cannot read file system information for {}: {}",
display_name.quote(),
e
);
return 1;
}
}
}
0
}
fn default_format(show_fs: bool, terse: bool, show_dev_type: bool) -> String {
if show_fs {
if terse {
"%n %i %l %t %s %S %b %f %a %c %d\n".into()
} else {
" File: \"%n\"\n ID: %-8i Namelen: %-7l Type: %T\nBlock \
size: %-10s Fundamental block size: %S\nBlocks: Total: %-10b \
Free: %-10f Available: %a\nInodes: Total: %-10c Free: %d\n"
.into()
}
} else if terse {
"%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %W %o\n".into()
} else {
[
" File: %N\n Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\n",
if show_dev_type {
"Device: %Dh/%dd\tInode: %-10i Links: %-5h Device type: %t,%T\n"
} else {
"Device: %Dh/%dd\tInode: %-10i Links: %h\n"
},
"Access: (%04a/%10.10A) Uid: (%5u/%8U) Gid: (%5g/%8G)\n",
"Access: %x\nModify: %y\nChange: %z\n Birth: %w\n",
]
.join("")
}
}
}
fn get_long_usage() -> &'static str {
"
The valid format sequences for files (without --file-system):
%a access rights in octal (note '#' and '0' printf flags)
%A access rights in human readable form
%b number of blocks allocated (see %B)
%B the size in bytes of each block reported by %b
%C SELinux security context string
%d device number in decimal
%D device number in hex
%f raw mode in hex
%F file type
%g group ID of owner
%G group name of owner
%h number of hard links
%i inode number
%m mount point
%n file name
%N quoted file name with dereference if symbolic link
%o optimal I/O transfer size hint
%s total size, in bytes
%t major device type in hex, for character/block device special files
%T minor device type in hex, for character/block device special files
%u user ID of owner
%U user name of owner
%w time of file birth, human-readable; - if unknown
%W time of file birth, seconds since Epoch; 0 if unknown
%x time of last access, human-readable
%X time of last access, seconds since Epoch
%y time of last data modification, human-readable
%Y time of last data modification, seconds since Epoch
%z time of last status change, human-readable
%Z time of last status change, seconds since Epoch
Valid format sequences for file systems:
%a free blocks available to non-superuser
%b total data blocks in file system
%c total file nodes in file system
%d free file nodes in file system
%f free blocks in file system
%i file system ID in hex
%l maximum length of filenames
%n file name
%s block size (for faster transfers)
%S fundamental block size (for block counts)
%t file system type in hex
%T file system type in human readable form
NOTE: your shell may have its own version of stat, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.
"
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let stater = Stater::new(&matches)?;
let exit_status = stater.exec();
if exit_status == 0 {
Ok(())
} else {
Err(exit_status.into())
}
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.arg(
Arg::new(options::DEREFERENCE)
.short('L')
.long(options::DEREFERENCE)
.help("follow links")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::FILE_SYSTEM)
.short('f')
.long(options::FILE_SYSTEM)
.help("display file system status instead of file status")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::TERSE)
.short('t')
.long(options::TERSE)
.help("print the information in terse form")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::FORMAT)
.short('c')
.long(options::FORMAT)
.help(
"use the specified FORMAT instead of the default;
output a newline after each use of FORMAT",
)
.value_name("FORMAT"),
)
.arg(
Arg::new(options::PRINTF)
.long(options::PRINTF)
.value_name("FORMAT")
.help(
"like --format, but interpret backslash escapes,
and do not output a mandatory trailing newline;
if you want a newline, include \n in FORMAT",
),
)
.arg(
Arg::new(options::FILES)
.action(ArgAction::Append)
.value_parser(ValueParser::os_string())
.value_hint(clap::ValueHint::FilePath),
)
}
#[cfg(test)]
mod tests {
use super::{group_num, Flags, ScanUtil, Stater, Token};
#[test]
fn test_scanners() {
assert_eq!(Some((-5, 2)), "-5zxc".scan_num::<i32>());
assert_eq!(Some((51, 2)), "51zxc".scan_num::<u32>());
assert_eq!(Some((192, 4)), "+192zxc".scan_num::<i32>());
assert_eq!(None, "z192zxc".scan_num::<i32>());
assert_eq!(Some(('a', 3)), "141zxc".scan_char(8));
assert_eq!(Some(('\n', 2)), "12qzxc".scan_char(8)); assert_eq!(Some(('\r', 1)), "dqzxc".scan_char(16)); assert_eq!(None, "z2qzxc".scan_char(8)); }
#[test]
fn test_group_num() {
assert_eq!("12,379,821,234", group_num("12379821234"));
assert_eq!("21,234", group_num("21234"));
assert_eq!("821,234", group_num("821234"));
assert_eq!("1,821,234", group_num("1821234"));
assert_eq!("1,234", group_num("1234"));
assert_eq!("234", group_num("234"));
assert_eq!("24", group_num("24"));
assert_eq!("4", group_num("4"));
assert_eq!("", group_num(""));
assert_eq!("-5", group_num("-5"));
assert_eq!("-1,234", group_num("-1234"));
}
#[test]
#[should_panic]
fn test_group_num_panic_if_invalid_numeric_characters() {
group_num("³³³³³");
}
#[test]
fn normal_format() {
let s = "%'010.2ac%-#5.w\n";
let expected = vec![
Token::Directive {
flag: Flags {
group: true,
zero: true,
..Default::default()
},
width: 10,
precision: Some(2),
format: 'a',
},
Token::Char('c'),
Token::Directive {
flag: Flags {
left: true,
alter: true,
..Default::default()
},
width: 5,
precision: Some(0),
format: 'w',
},
Token::Char('\n'),
];
assert_eq!(&expected, &Stater::generate_tokens(s, false).unwrap());
}
#[test]
fn printf_format() {
let s = r#"%-# 15a\t\r\"\\\a\b\e\f\v%+020.-23w\x12\167\132\112\n"#;
let expected = vec![
Token::Directive {
flag: Flags {
left: true,
alter: true,
space: true,
..Default::default()
},
width: 15,
precision: None,
format: 'a',
},
Token::Char('\t'),
Token::Char('\r'),
Token::Char('"'),
Token::Char('\\'),
Token::Char('\x07'),
Token::Char('\x08'),
Token::Char('\x1B'),
Token::Char('\x0C'),
Token::Char('\x0B'),
Token::Directive {
flag: Flags {
sign: true,
zero: true,
..Default::default()
},
width: 20,
precision: None,
format: 'w',
},
Token::Char('\x12'),
Token::Char('w'),
Token::Char('Z'),
Token::Char('J'),
Token::Char('\n'),
];
assert_eq!(&expected, &Stater::generate_tokens(s, true).unwrap());
}
}