diff --git a/Cargo.toml b/Cargo.toml index 23e63faa8..e17c40bd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ unix = [ "nice", "nohup", "pathchk", + "stat", "stdbuf", "timeout", "touch", @@ -153,6 +154,7 @@ shuf = { optional=true, path="src/shuf" } sleep = { optional=true, path="src/sleep" } sort = { optional=true, path="src/sort" } split = { optional=true, path="src/split" } +stat = { optional=true, path="src/stat" } stdbuf = { optional=true, path="src/stdbuf" } sum = { optional=true, path="src/sum" } sync = { optional=true, path="src/sync" } diff --git a/Makefile b/Makefile index 20dd47815..e7abc7007 100644 --- a/Makefile +++ b/Makefile @@ -113,6 +113,7 @@ UNIX_PROGS := \ nice \ nohup \ pathchk \ + stat \ stdbuf \ timeout \ touch \ @@ -168,6 +169,7 @@ TEST_PROGS := \ seq \ sort \ split \ + stat \ stdbuf \ sum \ tac \ diff --git a/src/stat/Cargo.toml b/src/stat/Cargo.toml new file mode 100644 index 000000000..da3788528 --- /dev/null +++ b/src/stat/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "stat" +version = "0.0.1" +authors = [] + +[lib] +name = "uu_stat" +path = "stat.rs" + +[dependencies] +getopts = "*" +libc = "^0.2" +time = "*" +uucore = { path="../uucore" } + +[[bin]] +name = "stat" +path = "main.rs" diff --git a/src/stat/fsext.rs b/src/stat/fsext.rs new file mode 100644 index 000000000..a169ed084 --- /dev/null +++ b/src/stat/fsext.rs @@ -0,0 +1,430 @@ +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. +// + +extern crate libc; +extern crate time; + +use self::time::Timespec; +pub use self::libc::{S_IFMT, S_IFDIR, S_IFCHR, S_IFBLK, S_IFREG, S_IFIFO, S_IFLNK, S_IFSOCK, + S_ISUID, S_ISGID, S_ISVTX, S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, + S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH, mode_t, c_int, strerror}; + +#[macro_export] +macro_rules! has { + ($mode:expr, $perm:expr) => ( + $mode & $perm != 0 + ) +} + +pub fn pretty_time(sec: i64, nsec: i64) -> String { + let tm = time::at(Timespec::new(sec, nsec as i32)); + let res = time::strftime("%Y-%m-%d %H:%M:%S.%f %z", &tm).unwrap(); + if res.ends_with(" -0000") { + res.replace(" -0000", " +0000") + } else { + res + } +} + +pub fn pretty_filetype<'a>(mode: mode_t, size: u64) -> &'a str { + match mode & S_IFMT { + S_IFREG => { + if size != 0 { + "regular file" + } else { + "regular empty file" + } + } + S_IFDIR => "directory", + S_IFLNK => "symbolic link", + S_IFCHR => "character special file", + S_IFBLK => "block special file", + S_IFIFO => "fifo", + S_IFSOCK => "socket", + // TODO: Other file types + // See coreutils/gnulib/lib/file-type.c + _ => "weird file", + } +} + +pub fn pretty_access(mode: mode_t) -> String { + let mut result = String::with_capacity(10); + result.push(match mode & S_IFMT { + S_IFDIR => 'd', + S_IFCHR => 'c', + S_IFBLK => 'b', + S_IFREG => '-', + S_IFIFO => 'p', + S_IFLNK => 'l', + S_IFSOCK => 's', + // TODO: Other file types + // See coreutils/gnulib/lib/filemode.c + _ => '?', + }); + + result.push(if has!(mode, S_IRUSR) { + 'r' + } else { + '-' + }); + result.push(if has!(mode, S_IWUSR) { + 'w' + } else { + '-' + }); + result.push(if has!(mode, S_ISUID as mode_t) { + if has!(mode, S_IXUSR) { + 's' + } else { + 'S' + } + } else if has!(mode, S_IXUSR) { + 'x' + } else { + '-' + }); + + result.push(if has!(mode, S_IRGRP) { + 'r' + } else { + '-' + }); + result.push(if has!(mode, S_IWGRP) { + 'w' + } else { + '-' + }); + result.push(if has!(mode, S_ISGID as mode_t) { + if has!(mode, S_IXGRP) { + 's' + } else { + 'S' + } + } else if has!(mode, S_IXGRP) { + 'x' + } else { + '-' + }); + + result.push(if has!(mode, S_IROTH) { + 'r' + } else { + '-' + }); + result.push(if has!(mode, S_IWOTH) { + 'w' + } else { + '-' + }); + result.push(if has!(mode, S_ISVTX as mode_t) { + if has!(mode, S_IXOTH) { + 't' + } else { + 'T' + } + } else if has!(mode, S_IXOTH) { + 'x' + } else { + '-' + }); + + result +} + +use std::mem::{self, transmute}; +use std::path::Path; +use std::borrow::Cow; +use std::ffi::CString; +use std::convert::{AsRef, From}; +use std::error::Error; +use std::io::Error as IOError; + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] +use self::libc::statfs as Sstatfs; +// #[cfg(any(target_os = "openbsd", target_os = "netbsd", target_os = "openbsd", target_os = "bitrig", target_os = "dragonfly"))] +// use self::libc::statvfs as Sstatfs; + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] +use self::libc::statfs as statfs_fn; +// #[cfg(any(target_os = "openbsd", target_os = "netbsd", target_os = "openbsd", target_os = "bitrig", target_os = "dragonfly"))] +// use self::libc::statvfs as statfs_fn; + +pub trait FsMeta { + fn fs_type(&self) -> i64; + fn iosize(&self) -> i64; + fn blksize(&self) -> i64; + fn total_blocks(&self) -> u64; + fn free_blocks(&self) -> u64; + fn avail_blocks(&self) -> u64; + fn total_fnodes(&self) -> u64; + fn free_fnodes(&self) -> u64; + fn fsid(&self) -> u64; + fn namelen(&self) -> i64; +} + +impl FsMeta for Sstatfs { + fn blksize(&self) -> i64 { + self.f_bsize as i64 + } + fn total_blocks(&self) -> u64 { + self.f_blocks as u64 + } + fn free_blocks(&self) -> u64 { + self.f_bfree as u64 + } + fn avail_blocks(&self) -> u64 { + self.f_bavail as u64 + } + fn total_fnodes(&self) -> u64 { + self.f_files as u64 + } + fn free_fnodes(&self) -> u64 { + self.f_ffree as u64 + } + fn fs_type(&self) -> i64 { + self.f_type as i64 + } + + #[cfg(target_os = "linux")] + fn iosize(&self) -> i64 { + self.f_frsize as i64 + } + #[cfg(target_os = "macos")] + fn iosize(&self) -> i64 { + self.f_iosize as i64 + } + // FIXME: + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + fn iosize(&self) -> i64 { + 0 + } + + // Linux, SunOS, HP-UX, 4.4BSD, FreeBSD have a system call statfs() that returns + // a struct statfs, containing a fsid_t f_fsid, where fsid_t is defined + // as struct { int val[2]; } + // + // Solaris, Irix and POSIX have a system call statvfs(2) that returns a + // struct statvfs, containing an unsigned long f_fsid + #[cfg(any(target_os = "macos", target_os = "linux"))] + fn fsid(&self) -> u64 { + let f_fsid: &[u32; 2] = unsafe { transmute(&self.f_fsid) }; + (f_fsid[0] as u64) << 32 | f_fsid[1] as u64 + } + // FIXME: + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + fn fsid(&self) -> u64 { + 0 + } + + #[cfg(target_os = "linux")] + fn namelen(&self) -> i64 { + self.f_namelen as i64 + } + #[cfg(target_os = "macos")] + fn namelen(&self) -> i64 { + 1024 + } + // FIXME: + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + fn namelen(&self) -> u64 { + 0 + } +} + +pub fn statfs>(path: P) -> Result + where Vec: From

+{ + match CString::new(path) { + Ok(p) => { + let mut buffer: Sstatfs = unsafe { mem::zeroed() }; + unsafe { + match statfs_fn(p.as_ptr(), &mut buffer) { + 0 => Ok(buffer), + _ => { + let errno = IOError::last_os_error().raw_os_error().unwrap_or(0); + Err(CString::from_raw(strerror(errno)) + .into_string() + .unwrap_or("Unknown Error".to_owned())) + } + } + } + } + Err(e) => Err(e.description().to_owned()), + } +} + +pub fn pretty_fstype<'a>(fstype: i64) -> Cow<'a, str> { + match fstype { + 0x61636673 => "acfs".into(), + 0xADF5 => "adfs".into(), + 0xADFF => "affs".into(), + 0x5346414F => "afs".into(), + 0x09041934 => "anon-inode FS".into(), + 0x61756673 => "aufs".into(), + 0x0187 => "autofs".into(), + 0x42465331 => "befs".into(), + 0x62646576 => "bdevfs".into(), + 0x1BADFACE => "bfs".into(), + 0xCAFE4A11 => "bpf_fs".into(), + 0x42494E4D => "binfmt_misc".into(), + 0x9123683E => "btrfs".into(), + 0x73727279 => "btrfs_test".into(), + 0x00C36400 => "ceph".into(), + 0x0027E0EB => "cgroupfs".into(), + 0xFF534D42 => "cifs".into(), + 0x73757245 => "coda".into(), + 0x012FF7B7 => "coh".into(), + 0x62656570 => "configfs".into(), + 0x28CD3D45 => "cramfs".into(), + 0x453DCD28 => "cramfs-wend".into(), + 0x64626720 => "debugfs".into(), + 0x1373 => "devfs".into(), + 0x1CD1 => "devpts".into(), + 0xF15F => "ecryptfs".into(), + 0xDE5E81E4 => "efivarfs".into(), + 0x00414A53 => "efs".into(), + 0x5DF5 => "exofs".into(), + 0x137D => "ext".into(), + 0xEF53 => "ext2/ext3".into(), + 0xEF51 => "ext2".into(), + 0xF2F52010 => "f2fs".into(), + 0x4006 => "fat".into(), + 0x19830326 => "fhgfs".into(), + 0x65735546 => "fuseblk".into(), + 0x65735543 => "fusectl".into(), + 0x0BAD1DEA => "futexfs".into(), + 0x01161970 => "gfs/gfs2".into(), + 0x47504653 => "gpfs".into(), + 0x4244 => "hfs".into(), + 0x482B => "hfs+".into(), + 0x4858 => "hfsx".into(), + 0x00C0FFEE => "hostfs".into(), + 0xF995E849 => "hpfs".into(), + 0x958458F6 => "hugetlbfs".into(), + 0x11307854 => "inodefs".into(), + 0x013111A8 => "ibrix".into(), + 0x2BAD1DEA => "inotifyfs".into(), + 0x9660 => "isofs".into(), + 0x4004 => "isofs".into(), + 0x4000 => "isofs".into(), + 0x07C0 => "jffs".into(), + 0x72B6 => "jffs2".into(), + 0x3153464A => "jfs".into(), + 0x6B414653 => "k-afs".into(), + 0xC97E8168 => "logfs".into(), + 0x0BD00BD0 => "lustre".into(), + 0x5346314D => "m1fs".into(), + 0x137F => "minix".into(), + 0x138F => "minix (30 char.)".into(), + 0x2468 => "minix v2".into(), + 0x2478 => "minix v2 (30 char.)".into(), + 0x4D5A => "minix3".into(), + 0x19800202 => "mqueue".into(), + 0x4D44 => "msdos".into(), + 0x564C => "novell".into(), + 0x6969 => "nfs".into(), + 0x6E667364 => "nfsd".into(), + 0x3434 => "nilfs".into(), + 0x6E736673 => "nsfs".into(), + 0x5346544E => "ntfs".into(), + 0x9FA1 => "openprom".into(), + 0x7461636F => "ocfs2".into(), + 0x794C7630 => "overlayfs".into(), + 0xAAD7AAEA => "panfs".into(), + 0x50495045 => "pipefs".into(), + 0x7C7C6673 => "prl_fs".into(), + 0x9FA0 => "proc".into(), + 0x6165676C => "pstorefs".into(), + 0x002F => "qnx4".into(), + 0x68191122 => "qnx6".into(), + 0x858458F6 => "ramfs".into(), + 0x52654973 => "reiserfs".into(), + 0x7275 => "romfs".into(), + 0x67596969 => "rpc_pipefs".into(), + 0x73636673 => "securityfs".into(), + 0xF97CFF8C => "selinux".into(), + 0x43415D53 => "smackfs".into(), + 0x517B => "smb".into(), + 0xFE534D42 => "smb2".into(), + 0xBEEFDEAD => "snfs".into(), + 0x534F434B => "sockfs".into(), + 0x73717368 => "squashfs".into(), + 0x62656572 => "sysfs".into(), + 0x012FF7B6 => "sysv2".into(), + 0x012FF7B5 => "sysv4".into(), + 0x01021994 => "tmpfs".into(), + 0x74726163 => "tracefs".into(), + 0x24051905 => "ubifs".into(), + 0x15013346 => "udf".into(), + 0x00011954 => "ufs".into(), + 0x54190100 => "ufs".into(), + 0x9FA2 => "usbdevfs".into(), + 0x01021997 => "v9fs".into(), + 0xBACBACBC => "vmhgfs".into(), + 0xA501FCF5 => "vxfs".into(), + 0x565A4653 => "vzfs".into(), + 0x53464846 => "wslfs".into(), + 0xABBA1974 => "xenfs".into(), + 0x012FF7B4 => "xenix".into(), + 0x58465342 => "xfs".into(), + 0x012FD16D => "xia".into(), + 0x2FC12FC1 => "zfs".into(), + other => format!("UNKNOWN ({:#x})", other).into(), + } +} + +#[cfg(test)] +mod test_fsext { + use super::*; + + #[test] + fn test_access() { + assert_eq!("drwxr-xr-x", pretty_access(S_IFDIR | 0o755)); + assert_eq!("-rw-r--r--", pretty_access(S_IFREG | 0o644)); + assert_eq!("srw-r-----", pretty_access(S_IFSOCK | 0o640)); + assert_eq!("lrw-r-xr-x", pretty_access(S_IFLNK | 0o655)); + assert_eq!("?rw-r-xr-x", pretty_access(0o655)); + + assert_eq!("brwSr-xr-x", + pretty_access(S_IFBLK | S_ISUID as mode_t | 0o655)); + assert_eq!("brwsr-xr-x", + pretty_access(S_IFBLK | S_ISUID as mode_t | 0o755)); + + assert_eq!("prw---sr--", + pretty_access(S_IFIFO | S_ISGID as mode_t | 0o614)); + assert_eq!("prw---Sr--", + pretty_access(S_IFIFO | S_ISGID as mode_t | 0o604)); + + assert_eq!("c---r-xr-t", + pretty_access(S_IFCHR | S_ISVTX as mode_t | 0o055)); + assert_eq!("c---r-xr-T", + pretty_access(S_IFCHR | S_ISVTX as mode_t | 0o054)); + } + + #[test] + fn test_file_type() { + assert_eq!("block special file", pretty_filetype(S_IFBLK, 0)); + assert_eq!("character special file", pretty_filetype(S_IFCHR, 0)); + assert_eq!("regular file", pretty_filetype(S_IFREG, 1)); + assert_eq!("regular empty file", pretty_filetype(S_IFREG, 0)); + assert_eq!("weird file", pretty_filetype(0, 0)); + } + + #[test] + fn test_fs_type() { + assert_eq!("ext2/ext3", pretty_fstype(0xEF53)); + assert_eq!("tmpfs", pretty_fstype(0x01021994)); + assert_eq!("nfs", pretty_fstype(0x6969)); + assert_eq!("btrfs", pretty_fstype(0x9123683e)); + assert_eq!("xfs", pretty_fstype(0x58465342)); + assert_eq!("zfs", pretty_fstype(0x2FC12FC1)); + assert_eq!("ntfs", pretty_fstype(0x5346544e)); + assert_eq!("fat", pretty_fstype(0x4006)); + assert_eq!("UNKNOWN (0x1234)", pretty_fstype(0x1234)); + } +} diff --git a/src/stat/main.rs b/src/stat/main.rs new file mode 100644 index 000000000..7a290254b --- /dev/null +++ b/src/stat/main.rs @@ -0,0 +1,5 @@ +extern crate uu_stat; + +fn main() { + std::process::exit(uu_stat::uumain(std::env::args().collect())); +} diff --git a/src/stat/stat.rs b/src/stat/stat.rs new file mode 100644 index 000000000..58152a080 --- /dev/null +++ b/src/stat/stat.rs @@ -0,0 +1,988 @@ +#![crate_name = "uu_stat"] + +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. +// + +extern crate getopts; +use getopts::Options; + +#[macro_use] +mod fsext; +use fsext::*; + +#[macro_use] +extern crate uucore; + +use std::{fs, iter, cmp}; +use std::fs::File; +use std::io::{Write, BufReader, BufRead}; +use std::borrow::Cow; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; +use std::path::Path; +use std::convert::AsRef; + +#[cfg(test)] +mod test_stat; + +macro_rules! check_bound { + ($str: ident, $bound:expr, $beg: expr, $end: expr) => ( + if $end >= $bound { + return Err(format!("‘{}’: invalid directive", &$str[$beg..$end])); + } + + ) +} +macro_rules! fill_string { + ($str: ident, $c: expr, $cnt: expr) => ( + iter::repeat($c).take($cnt).map(|c| $str.push(c)).all(|_| true) + ) +} +macro_rules! extend_digits { + ($str: ident, $min: expr) => ( + if $min > $str.len() { + let mut pad = String::with_capacity($min); + fill_string!(pad, '0', $min - $str.len()); + pad.push_str($str); + pad.into() + } else { + $str.into() + } + ) +} +macro_rules! pad_and_print { + ($result: ident, $str: ident, $left: expr, $width: expr, $padding: expr) => ( + if $str.len() < $width { + if $left { + $result.push_str($str.as_ref()); + fill_string!($result, $padding, $width - $str.len()); + } else { + fill_string!($result, $padding, $width - $str.len()); + $result.push_str($str.as_ref()); + } + } else { + $result.push_str($str.as_ref()); + } + print!("{}", $result); + ) +} +macro_rules! print_adjusted { + ($str: ident, $left: expr, $width: expr, $padding: expr) => ({ + let field_width = cmp::max($width, $str.len()); + let mut result = String::with_capacity(field_width); + pad_and_print!(result, $str, $left, field_width, $padding); + }); + ($str: ident, $left: expr, $need_prefix: expr, $prefix: expr, $width: expr, $padding: expr) => ({ + let mut field_width = cmp::max($width, $str.len()); + let mut result = String::with_capacity(field_width + $prefix.len()); + if $need_prefix { + result.push_str($prefix); + field_width -= $prefix.len(); + } + pad_and_print!(result, $str, $left, field_width, $padding); + }) +} + +static NAME: &'static str = "stat"; +static VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +const MOUNT_INFO: &'static str = "/etc/mtab"; +pub const F_ALTER: u8 = 1; +pub const F_ZERO: u8 = 1 << 1; +pub const F_LEFT: u8 = 1 << 2; +pub const F_SPACE: u8 = 1 << 3; +pub const F_SIGN: u8 = 1 << 4; +// unused at present +pub const F_GROUP: u8 = 1 << 5; + +#[derive(Debug, PartialEq)] +pub enum OutputType { + Str, + Integer, + Unsigned, + UnsignedHex, + UnsignedOct, + Unknown, +} + +#[derive(Debug, PartialEq)] +pub enum Token { + Char(char), + Directive { + flag: u8, + width: usize, + precision: i32, + format: char, + }, +} + +pub trait ScanUtil { + fn scan_num(&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(&self) -> Option<(F, usize)> + where F: std::str::FromStr + { + let mut chars = self.chars(); + let mut i = 0; + match chars.next() { + Some('-') | Some('+') | Some('0'...'9') => i += 1, + _ => return None, + } + while let Some(c) = chars.next() { + 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_usize, + 16 => 2, + _ => return None, + }; + let mut chars = self.chars().enumerate(); + let mut res = 0_u32; + let mut offset = 0_usize; + while let Some((i, c)) = chars.next() { + 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 + } + } +} + +pub struct Stater { + follow: bool, + showfs: bool, + from_user: bool, + files: Vec, + mount_list: Vec, + default_tokens: Vec, + default_dev_tokens: Vec, +} + +fn print_it(arg: &str, otype: OutputType, flag: u8, width: usize, precision: i32) { + + // If the precision is given as just '.', the precision is taken to be zero. + // A negative precision is taken as if the precision were omitted. + // This gives the minimum number of digits to appear for d, i, o, u, x, and X conversions, + // the maximum number of characters to be printed from a string for s and S conversions. + + // # + // The value should be converted to an "alternate form". + // For o conversions, the first character of the output string is made zero (by prefixing a 0 if it was not zero already). + // For x and X conversions, a nonzero result has the string "0x" (or "0X" for X conversions) prepended to it. + + // 0 + // The value should be zero padded. + // For d, i, o, u, x, X, a, A, e, E, f, F, g, and G conversions, the converted value is padded on the left with zeros rather than blanks. + // If the 0 and - flags both appear, the 0 flag is ignored. + // If a precision is given with a numeric conversion (d, i, o, u, x, and X), the 0 flag is ignored. + // For other conversions, the behavior is undefined. + + // - + // The converted value is to be left adjusted on the field boundary. (The default is right justification.) + // The converted value is padded on the right with blanks, rather than on the left with blanks or zeros. + // A - overrides a 0 if both are given. + + // ' ' (a space) + // A blank should be left before a positive number (or empty string) produced by a signed conversion. + + // + + // A sign (+ or -) should always be placed before a number produced by a signed conversion. + // By default, a sign is used only for negative numbers. + // A + overrides a space if both are used. + + if otype == OutputType::Unknown { + return print!("?"); + } + + let left_align = has!(flag, F_LEFT); + let padding_char = if has!(flag, F_ZERO) && !left_align && precision == -1 { + '0' + } else { + ' ' + }; + + let has_sign = has!(flag, F_SIGN) || has!(flag, F_SPACE); + + let should_alter = has!(flag, F_ALTER); + let prefix = match otype { + OutputType::UnsignedOct => "0", + OutputType::UnsignedHex => "0x", + OutputType::Integer => { + if has!(flag, F_SIGN) { + "+" + } else { + " " + } + } + _ => "", + }; + + match otype { + OutputType::Str => { + let limit = cmp::min(precision, arg.len() as i32); + let s: &str = if limit >= 0 { + &arg[..limit as usize] + } else { + arg + }; + print_adjusted!(s, left_align, width, ' '); + } + OutputType::Integer => { + let min_digits = cmp::max(precision, arg.len() as i32) as usize; + let extended: Cow = extend_digits!(arg, min_digits); + print_adjusted!(extended, left_align, has_sign, prefix, width, padding_char); + } + OutputType::Unsigned => { + let min_digits = cmp::max(precision, arg.len() as i32) as usize; + let extended: Cow = extend_digits!(arg, min_digits); + print_adjusted!(extended, left_align, width, padding_char); + } + OutputType::UnsignedOct => { + let min_digits = cmp::max(precision, arg.len() as i32) as usize; + let extended: Cow = extend_digits!(arg, min_digits); + print_adjusted!(extended, + left_align, + should_alter, + prefix, + width, + padding_char); + } + OutputType::UnsignedHex => { + let min_digits = cmp::max(precision, arg.len() as i32) as usize; + let extended: Cow = extend_digits!(arg, min_digits); + print_adjusted!(extended, + left_align, + should_alter, + prefix, + width, + padding_char); + } + _ => unreachable!(), + } +} + + +use std::ptr; +use std::ffi::CStr; +use uucore::c_types::{getpwuid, getgrgid}; +fn get_grp_name(gid: u32) -> String { + let p = unsafe { getgrgid(gid) }; + if !p.is_null() { + unsafe { CStr::from_ptr(ptr::read(p).gr_name).to_string_lossy().into_owned() } + } else { + "UNKNOWN".to_owned() + } +} +fn get_usr_name(uid: u32) -> String { + let p = unsafe { getpwuid(uid) }; + if !p.is_null() { + unsafe { + CStr::from_ptr(ptr::read(p).pw_name) + .to_string_lossy() + .into_owned() + } + } else { + "UNKNOWN".to_owned() + } +} + +impl Stater { + pub fn generate_tokens(fmtstr: &str, use_printf: bool) -> Result, String> { + + let mut tokens = Vec::new(); + let bound = fmtstr.len(); + let chars = fmtstr.chars().collect::>(); + + let mut i = 0_usize; + 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: u8 = 0; + + while i < bound { + match chars[i] { + '#' => flag |= F_ALTER, + '0' => flag |= F_ZERO, + '-' => flag |= F_LEFT, + ' ' => flag |= F_SPACE, + '+' => flag |= F_SIGN, + // '\'' => flag |= F_GROUP, + '\'' => unimplemented!(), + 'I' => unimplemented!(), + _ => break, + } + i += 1; + } + check_bound!(fmtstr, bound, old, i); + + let mut width = 0_usize; + let mut precision = -1_i32; + let mut j = i; + + match fmtstr[j..].scan_num::() { + Some((field_width, offset)) => { + width = field_width; + j += offset; + } + None => (), + } + check_bound!(fmtstr, bound, old, j); + + if chars[j] == '.' { + j += 1; + check_bound!(fmtstr, bound, old, j); + + match fmtstr[j..].scan_num::() { + Some((prec, offset)) => { + if prec >= 0 { + precision = prec; + } + j += offset; + } + None => precision = 0, + } + check_bound!(fmtstr, bound, old, j); + } + + i = j; + tokens.push(Token::Directive { + width: width, + flag: flag, + precision: 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)) = fmtstr[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) = fmtstr[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')), + '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 && !fmtstr.ends_with('\n') { + tokens.push(Token::Char('\n')); + } + Ok(tokens) + } + + fn new(matches: getopts::Matches) -> Result { + let fmtstr = if matches.opt_present("printf") { + matches.opt_str("printf").expect("Invalid format string") + } else { + matches.opt_str("format").unwrap_or("".to_owned()) + }; + + let use_printf = matches.opt_present("printf"); + let terse = matches.opt_present("terse"); + let showfs = matches.opt_present("file-system"); + + let default_tokens = if fmtstr.is_empty() { + Stater::generate_tokens(&Stater::default_fmt(showfs, terse, false), use_printf).unwrap() + } else { + try!(Stater::generate_tokens(&fmtstr, use_printf)) + }; + let default_dev_tokens = Stater::generate_tokens(&Stater::default_fmt(showfs, terse, true), + use_printf) + .unwrap(); + + let reader = BufReader::new(File::open(MOUNT_INFO).expect("Failed to read /etc/mtab")); + let mut mount_list = reader.lines() + .filter_map(|s| s.ok()) + .filter_map(|line| { + line.split_whitespace().nth(1).map(|s| s.to_owned()) + }) + .collect::>(); + // Reverse sort. The longer comes first. + mount_list.sort_by(|a, b| b.cmp(a)); + + Ok(Stater { + follow: matches.opt_present("dereference"), + showfs: showfs, + from_user: !fmtstr.is_empty(), + files: matches.free, + default_tokens: default_tokens, + default_dev_tokens: default_dev_tokens, + mount_list: mount_list, + }) + } + + fn find_mount_point>(&self, p: P) -> Option { + let path = match p.as_ref().canonicalize() { + Ok(s) => s, + Err(_) => return None, + }; + for root in (&self.mount_list).into_iter() { + if path.starts_with(root) { + return Some(root.clone()); + } + } + None + } + + fn exec(&self) -> i32 { + let mut ret = 0; + for f in &self.files { + ret |= self.do_stat(f.as_str()); + } + ret + } + + fn do_stat(&self, file: &str) -> i32 { + + if !self.showfs { + let result = if self.follow { + fs::metadata(file) + } else { + fs::symlink_metadata(file) + }; + match result { + Ok(meta) => { + let ftype = meta.file_type(); + let tokens = if self.from_user || + !(ftype.is_char_device() || ftype.is_block_device()) { + &self.default_tokens + } else { + &self.default_dev_tokens + }; + + for t in tokens.into_iter() { + match t { + &Token::Char(c) => print!("{}", c), + &Token::Directive { flag, width, precision, format } => { + + let arg: String; + let otype: OutputType; + + match format { + // access rights in octal + 'a' => { + arg = format!("{:o}", 0o7777 & meta.mode()); + otype = OutputType::UnsignedOct; + } + // access rights in human readable form + 'A' => { + arg = pretty_access(meta.mode() as mode_t); + otype = OutputType::Str; + } + // number of blocks allocated (see %B) + 'b' => { + arg = format!("{}", meta.blocks()); + otype = OutputType::Unsigned; + } + + // the size in bytes of each block reported by %b + // FIXME: blocksize differs on various platform + // See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE + 'B' => { + // the size in bytes of each block reported by %b + arg = format!("{}", 512); + otype = OutputType::Unsigned; + } + + // device number in decimal + 'd' => { + arg = format!("{}", meta.dev()); + otype = OutputType::Unsigned; + } + // device number in hex + 'D' => { + arg = format!("{:x}", meta.dev()); + otype = OutputType::UnsignedHex; + } + // raw mode in hex + 'f' => { + arg = format!("{:x}", meta.mode()); + otype = OutputType::UnsignedHex; + } + // file type + 'F' => { + arg = pretty_filetype(meta.mode() as mode_t, meta.len()).to_owned(); + otype = OutputType::Str; + } + // group ID of owner + 'g' => { + arg = format!("{}", meta.gid()); + otype = OutputType::Unsigned; + } + // group name of owner + 'G' => { + arg = get_grp_name(meta.gid()); + otype = OutputType::Str; + } + // number of hard links + 'h' => { + arg = format!("{}", meta.nlink()); + otype = OutputType::Unsigned; + } + // inode number + 'i' => { + arg = format!("{}", meta.ino()); + otype = OutputType::Unsigned; + } + + // mount point + 'm' => { + arg = self.find_mount_point(file).unwrap(); + otype = OutputType::Str; + } + + // file name + 'n' => { + arg = file.to_owned(); + otype = OutputType::Str; + } + // quoted file name with dereference if symbolic link + 'N' => { + if ftype.is_symlink() { + arg = format!("`{}' -> `{}'", + file, + fs::read_link(file) + .expect("Invalid symlink") + .to_string_lossy()); + } else { + arg = format!("`{}'", file); + } + otype = OutputType::Str; + } + // optimal I/O transfer size hint + 'o' => { + arg = format!("{}", meta.blksize()); + otype = OutputType::Unsigned; + } + // total size, in bytes + 's' => { + arg = format!("{}", meta.len()); + otype = OutputType::Integer; + } + // major device type in hex, for character/block device special + // files + 't' => { + arg = format!("{:x}", meta.rdev() >> 8); + otype = OutputType::UnsignedHex; + } + // minor device type in hex, for character/block device special + // files + 'T' => { + arg = format!("{:x}", meta.rdev() & 0xff); + otype = OutputType::UnsignedHex; + } + // user ID of owner + 'u' => { + arg = format!("{}", meta.uid()); + otype = OutputType::Unsigned; + } + // user name of owner + 'U' => { + arg = get_usr_name(meta.uid()); + otype = OutputType::Str; + } + + // time of file birth, human-readable; - if unknown + 'w' => { + // Unstable. Commented + //arg = if let Ok(elapsed) = meta.created() + //.map(|t| { + //t.elapsed().unwrap() + //}) { + //pretty_time(elapsed.as_secs() as i64, + //elapsed.subsec_nanos() as i64) + //} else { + //"-".to_owned() + //}; + arg = "-".to_owned(); + otype = OutputType::Str; + } + + // time of file birth, seconds since Epoch; 0 if unknown + 'W' => { + // Unstable. Commented + //arg = if let Ok(elapsed) = meta.created() + //.map(|t| { + //t.elapsed().unwrap() + //}) { + //format!("{}", elapsed.as_secs()) + //} else { + //"0".to_owned() + //}; + arg = "0".to_owned(); + otype = OutputType::Integer; + } + + // time of last access, human-readable + 'x' => { + arg = pretty_time(meta.atime(), meta.atime_nsec()); + otype = OutputType::Str; + } + // time of last access, seconds since Epoch + 'X' => { + arg = format!("{}", meta.atime()); + otype = OutputType::Integer; + } + // time of last data modification, human-readable + 'y' => { + arg = pretty_time(meta.mtime(), meta.mtime_nsec()); + otype = OutputType::Str; + } + // time of last data modification, seconds since Epoch + 'Y' => { + arg = format!("{}", meta.mtime()); + otype = OutputType::Str; + } + // time of last status change, human-readable + 'z' => { + arg = pretty_time(meta.ctime(), meta.ctime_nsec()); + otype = OutputType::Str; + } + // time of last status change, seconds since Epoch + 'Z' => { + arg = format!("{}", meta.ctime()); + otype = OutputType::Integer; + } + + _ => { + arg = "?".to_owned(); + otype = OutputType::Unknown; + } + } + print_it(&arg, otype, flag, width, precision); + } + } + } + } + Err(e) => { + show_info!("cannot stat '{}': {}", file, e); + return 1; + } + } + } else { + match statfs(file) { + Ok(meta) => { + let tokens = &self.default_tokens; + + for t in tokens.into_iter() { + match t { + &Token::Char(c) => print!("{}", c), + &Token::Directive { flag, width, precision, format } => { + + let arg: String; + let otype: OutputType; + match format { + // free blocks available to non-superuser + 'a' => { + arg = format!("{}", meta.avail_blocks()); + otype = OutputType::Integer; + } + // total data blocks in file system + 'b' => { + arg = format!("{}", meta.total_blocks()); + otype = OutputType::Integer; + } + // total file nodes in file system + 'c' => { + arg = format!("{}", meta.total_fnodes()); + otype = OutputType::Unsigned; + } + // free file nodes in file system + 'd' => { + arg = format!("{}", meta.free_fnodes()); + otype = OutputType::Integer; + } + // free blocks in file system + 'f' => { + arg = format!("{}", meta.free_blocks()); + otype = OutputType::Integer; + } + // file system ID in hex + 'i' => { + arg = format!("{:x}", meta.fsid()); + otype = OutputType::UnsignedHex; + } + // maximum length of filenames + 'l' => { + arg = format!("{}", meta.namelen()); + otype = OutputType::Unsigned; + } + // file name + 'n' => { + arg = file.to_owned(); + otype = OutputType::Str; + } + // block size (for faster transfers) + 's' => { + arg = format!("{}", meta.iosize()); + otype = OutputType::Unsigned; + } + // fundamental block size (for block counts) + 'S' => { + arg = format!("{}", meta.blksize()); + otype = OutputType::Unsigned; + } + // file system type in hex + 't' => { + arg = format!("{:x}", meta.fs_type()); + otype = OutputType::UnsignedHex; + } + // file system type in human readable form + 'T' => { + arg = pretty_fstype(meta.fs_type()).into_owned(); + otype = OutputType::Str; + } + _ => { + arg = "?".to_owned(); + otype = OutputType::Unknown; + } + } + + print_it(&arg, otype, flag, width, precision); + } + } + } + } + Err(e) => { + show_info!("cannot read file system information for '{}': {}", file, e); + return 1; + } + } + } + 0 + } + + // taken from coreutils/src/stat.c + fn default_fmt(showfs: bool, terse: bool, dev: bool) -> String { + + // SELinux related format is *ignored* + + let mut fmtstr = String::with_capacity(36); + if showfs { + if terse { + fmtstr.push_str("%n %i %l %t %s %S %b %f %a %c %d\n"); + } else { + fmtstr.push_str(" 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"); + } + } else if terse { + fmtstr.push_str("%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %W %o\n"); + } else { + fmtstr.push_str(" File: %N\n Size: %-10s\tBlocks: %-10b IO Block: %-6o %F\n"); + if dev { + fmtstr.push_str("Device: %Dh/%dd\tInode: %-10i Links: %-5h Device type: %t,%T\n"); + } else { + fmtstr.push_str("Device: %Dh/%dd\tInode: %-10i Links: %h\n"); + } + fmtstr.push_str("Access: (%04a/%10.10A) Uid: (%5u/%8U) Gid: (%5g/%8G)\n"); + fmtstr.push_str("Access: %x\nModify: %y\nChange: %z\n Birth: %w\n"); + } + fmtstr + } +} + +pub fn uumain(args: Vec) -> i32 { + let mut opts = Options::new(); + + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("", "version", "output version information and exit"); + + opts.optflag("L", "dereference", "follow links"); + opts.optflag("f", + "file-system", + "display file system status instead of file status"); + opts.optflag("t", "terse", "print the information in terse form"); + + // Omit the unused description as they are too long + opts.optopt("c", "format", "", "FORMAT"); + opts.optopt("", "printf", "", "FORMAT"); + + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + disp_err!("{}", f); + return 1; + } + }; + + if matches.opt_present("help") { + return help(); + } else if matches.opt_present("version") { + return version(); + } + + if matches.free.is_empty() { + disp_err!("missing operand"); + return 1; + } + + match Stater::new(matches) { + Ok(stater) => stater.exec(), + Err(e) => { + show_info!("{}", e); + 1 + } + } +} + +fn version() -> i32 { + println!("{} {}", NAME, VERSION); + 0 +} + +fn help() -> i32 { + let msg = format!(r#"Usage: {} [OPTION]... FILE... +Display file or file system status. + +Mandatory arguments to long options are mandatory for short options too. + -L, --dereference follow links + -f, --file-system display file system status instead of file status + -c --format=FORMAT use the specified FORMAT instead of the default; + output a newline after each use of FORMAT + --printf=FORMAT like --format, but interpret backslash escapes, + and do not output a mandatory trailing newline; + if you want a newline, include \n in FORMAT + -t, --terse print the information in terse form + --help display this help and exit + --version output version information and exit + +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."#, + NAME); + println!("{}", msg); + 0 +} diff --git a/src/stat/test_stat.rs b/src/stat/test_stat.rs new file mode 100644 index 000000000..816ad680a --- /dev/null +++ b/src/stat/test_stat.rs @@ -0,0 +1,70 @@ +pub use super::*; + +#[test] +fn test_scanutil() { + assert_eq!(Some((-5, 2)), "-5zxc".scan_num::()); + assert_eq!(Some((51, 2)), "51zxc".scan_num::()); + assert_eq!(Some((192, 4)), "+192zxc".scan_num::()); + assert_eq!(None, "z192zxc".scan_num::()); + + 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)); +} + +#[cfg(test)] +mod test_generate_tokens { + use super::*; + + #[test] + fn test_normal_format() { + let s = "%10.2ac%-5.w\n"; + let expected = vec![Token::Directive { + flag: 0, + width: 10, + precision: 2, + format: 'a', + }, + Token::Char('c'), + Token::Directive { + flag: F_LEFT, + width: 5, + precision: 0, + format: 'w', + }, + Token::Char('\n')]; + assert_eq!(&expected, &Stater::generate_tokens(s, false).unwrap()); + } + + #[test] + fn test_printf_format() { + let s = "%-# 15a\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.-23w\\x12\\167\\132\\112\\n"; + let expected = vec![Token::Directive { + flag: F_LEFT | F_ALTER | F_SPACE, + width: 15, + precision: -1, + format: 'a', + }, + 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: F_SIGN | F_ZERO, + width: 20, + precision: -1, + 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()); + } +} diff --git a/tests/test_stat.rs b/tests/test_stat.rs new file mode 100644 index 000000000..baf924528 --- /dev/null +++ b/tests/test_stat.rs @@ -0,0 +1,98 @@ +use common::util::*; + +static UTIL_NAME: &'static str = "stat"; + +use std::process::Command; + +#[test] +fn test_invalid_option() { + let (_, mut ucmd) = testing(UTIL_NAME); + ucmd.arg("-w").arg("-q").arg("/"); + ucmd.fails(); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_terse_fs_format() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-f", "-t", "/proc"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, expected_result(&args)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_fs_format() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-f", "--format=%n %i 0x%t %T", "/dev/shm"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, "/dev/shm 0 0x1021994 tmpfs\n"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_terse_normal_format() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-t", "/"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, expected_result(&args)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_normal_format() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["/boot"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, expected_result(&args)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_follow_symlink() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-L", "/dev/cdrom"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, expected_result(&args)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_symlink() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["/dev/cdrom"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, expected_result(&args)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_char() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["/dev/zero"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, expected_result(&args)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_multi_files() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["/dev", "/usr/lib", "/etc/fstab", "/var"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, expected_result(&args)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_printf() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["--printf=123%-# 15q\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.23m\\x12\\167\\132\\112\\n", "/"]; + ucmd.args(&args); + assert_eq!(ucmd.run().stdout, "123?\r\"\\\x07\x08\x1B\x0C\x0B /\x12wZJ\n"); +} + +fn expected_result(args: &[&str]) -> String { + let output = Command::new(UTIL_NAME).args(args).output().unwrap(); + String::from_utf8_lossy(&output.stdout).into_owned() +} diff --git a/tests/tests.rs b/tests/tests.rs index 044ba6ffa..1f07567f5 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -21,6 +21,7 @@ mod sieve; #[cfg(unix)] mod test_stdbuf; #[cfg(unix)] mod test_touch; #[cfg(unix)] mod test_unlink; +#[cfg(unix)] mod test_stat; mod test_base64; mod test_basename;