109 lines
3.4 KiB
Rust
109 lines
3.4 KiB
Rust
//! Print portable filesystem metadata and the world's fixed root identity.
|
|
use chrono::{DateTime, Utc};
|
|
use clap::Parser;
|
|
use std::{fs, io, path::PathBuf, time::SystemTime};
|
|
#[derive(Parser)]
|
|
struct Args {
|
|
#[arg(short = 'c', long = "format")]
|
|
format: Option<String>,
|
|
#[arg(short = 'L', long)]
|
|
dereference: bool,
|
|
#[arg(required = true)]
|
|
paths: Vec<PathBuf>,
|
|
}
|
|
fn timestamp(value: io::Result<SystemTime>, seconds: bool) -> String {
|
|
match value {
|
|
Ok(time) => {
|
|
let date: DateTime<Utc> = time.into();
|
|
if seconds {
|
|
date.timestamp().to_string()
|
|
} else {
|
|
date.format("%Y-%m-%d %H:%M:%S%.9f +0000").to_string()
|
|
}
|
|
}
|
|
Err(_) => {
|
|
if seconds {
|
|
"0".into()
|
|
} else {
|
|
"-".into()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn render(path: &PathBuf, meta: &fs::Metadata, format: &str) -> io::Result<String> {
|
|
let mut out = String::new();
|
|
let mut chars = format.chars();
|
|
while let Some(ch) = chars.next() {
|
|
if ch != '%' {
|
|
out.push(ch);
|
|
continue;
|
|
}
|
|
let field = chars.next().unwrap_or('%');
|
|
let value = match field {
|
|
'%' => "%".into(),
|
|
'n' => path.display().to_string(),
|
|
'N' => {
|
|
if meta.is_symlink() {
|
|
format!(
|
|
"'{}' -> '{}'",
|
|
path.display(),
|
|
fs::read_link(path)?.display()
|
|
)
|
|
} else {
|
|
format!("'{}'", path.display())
|
|
}
|
|
}
|
|
's' => meta.len().to_string(),
|
|
'F' => if meta.is_dir() {
|
|
"directory"
|
|
} else if meta.is_symlink() {
|
|
"symbolic link"
|
|
} else if meta.len() == 0 {
|
|
"regular empty file"
|
|
} else {
|
|
"regular file"
|
|
}
|
|
.into(),
|
|
'Y' => timestamp(meta.modified(), true),
|
|
'y' => timestamp(meta.modified(), false),
|
|
'X' => timestamp(meta.accessed(), true),
|
|
'x' => timestamp(meta.accessed(), false),
|
|
'a' => if meta.is_dir() { "755" } else { "644" }.into(),
|
|
'A' => if meta.is_dir() {
|
|
"drwxr-xr-x"
|
|
} else if meta.is_symlink() {
|
|
"lrw-r--r--"
|
|
} else {
|
|
"-rw-r--r--"
|
|
}
|
|
.into(),
|
|
'u' | 'g' => "0".into(),
|
|
'U' | 'G' => "root".into(),
|
|
_ => return Err(io::Error::other(format!("unsupported format %{field}"))),
|
|
};
|
|
out.push_str(&value);
|
|
}
|
|
Ok(out)
|
|
}
|
|
fn main() {
|
|
let args = Args::parse();
|
|
let format = args.format.as_deref().unwrap_or(" File: %N\n Size: %s\tType: %F\nAccess: (%a/%A) Uid: (%u/%U) Gid: (%g/%G)\nAccess: %x\nModify: %y");
|
|
let mut failed = false;
|
|
for path in &args.paths {
|
|
let result = if args.dereference {
|
|
fs::metadata(path)
|
|
} else {
|
|
fs::symlink_metadata(path)
|
|
}
|
|
.and_then(|meta| render(path, &meta, format));
|
|
match result {
|
|
Ok(text) => println!("{text}"),
|
|
Err(e) => {
|
|
eprintln!("stat: {}: {e}", path.display());
|
|
failed = true;
|
|
}
|
|
}
|
|
}
|
|
std::process::exit(i32::from(failed));
|
|
}
|