437 lines
11 KiB
Rust
437 lines
11 KiB
Rust
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
|
|
#![feature(let_chains, decl_macro)]
|
|
|
|
use std::{
|
|
cmp::Ordering, ffi::OsString, fmt, fs::{read_dir, FileType, Metadata}, io, path::{Path, PathBuf}, process::ExitCode, time::SystemTime
|
|
};
|
|
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::MetadataExt;
|
|
#[cfg(target_os = "yggdrasil")]
|
|
use std::os::yggdrasil::fs::MetadataExt;
|
|
|
|
use chrono::{Datelike, Timelike};
|
|
use clap::Parser;
|
|
use humansize::{FormatSize, BINARY};
|
|
|
|
#[derive(Parser)]
|
|
#[clap(disable_help_flag = true)]
|
|
pub struct Args {
|
|
#[arg(short)]
|
|
long: bool,
|
|
#[arg(short)]
|
|
inodes: bool,
|
|
#[arg(short, long)]
|
|
human_readable: bool,
|
|
#[arg(short)]
|
|
all: bool,
|
|
|
|
paths: Vec<PathBuf>,
|
|
}
|
|
|
|
trait DisplayBit {
|
|
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
|
|
|
|
fn display_with<'a, 'b>(&'a self, opts: &'b Args) -> DisplayWith<'a, 'b, Self>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
DisplayWith { item: self, opts }
|
|
}
|
|
}
|
|
|
|
trait DisplaySizeBit: Sized {
|
|
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
|
|
|
|
fn display_size_with(self, opts: &Args) -> DisplaySizeWith<'_, Self> {
|
|
DisplaySizeWith { size: self, opts }
|
|
}
|
|
}
|
|
|
|
trait MetadataImpl {
|
|
type Mode: DisplayBit;
|
|
|
|
fn size(&self) -> u64;
|
|
fn inode(&self) -> Option<u32>;
|
|
fn mode(&self) -> Self::Mode;
|
|
fn mtime(&self) -> SystemTime;
|
|
}
|
|
|
|
impl DisplaySizeBit for u64 {
|
|
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
if opts.human_readable {
|
|
fmt::Display::fmt(&self.format_size(BINARY), f)
|
|
} else {
|
|
fmt::Display::fmt(&self, f)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DisplaySizeWith<'a, T> {
|
|
size: T,
|
|
opts: &'a Args,
|
|
}
|
|
|
|
struct DisplayWith<'a, 'b, T: DisplayBit> {
|
|
item: &'a T,
|
|
opts: &'b Args,
|
|
}
|
|
|
|
impl<T: DisplayBit> fmt::Display for DisplayWith<'_, '_, T> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
self.item.display_bit(self.opts, f)
|
|
}
|
|
}
|
|
|
|
impl<T: DisplaySizeBit + Copy> fmt::Display for DisplaySizeWith<'_, T> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
self.size.display_size_bit(self.opts, f)
|
|
}
|
|
}
|
|
|
|
struct Entry {
|
|
name: String,
|
|
target: Option<String>,
|
|
ty: Option<FileType>,
|
|
attrs: Option<Metadata>,
|
|
}
|
|
|
|
impl DisplayBit for Option<FileType> {
|
|
fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
if let Some(ty) = self {
|
|
if ty.is_dir() {
|
|
f.write_str("d")
|
|
} else if ty.is_symlink() {
|
|
f.write_str("l")
|
|
} else {
|
|
f.write_str("-")
|
|
}
|
|
} else {
|
|
f.write_str("?")
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(any(target_os = "yggdrasil", rust_analyzer))]
|
|
impl DisplayBit for std::os::yggdrasil::io::RawFileMode {
|
|
fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{self}")
|
|
}
|
|
}
|
|
|
|
#[cfg(any(target_os = "yggdrasil", rust_analyzer))]
|
|
impl MetadataImpl for Metadata {
|
|
type Mode = std::os::yggdrasil::io::RawFileMode;
|
|
|
|
fn size(&self) -> u64 {
|
|
self.len()
|
|
}
|
|
|
|
fn mode(&self) -> Self::Mode {
|
|
self.mode_ext()
|
|
}
|
|
|
|
fn inode(&self) -> Option<u32> {
|
|
MetadataExt::inode(self)
|
|
}
|
|
|
|
fn mtime(&self) -> SystemTime {
|
|
self.modified().unwrap()
|
|
}
|
|
}
|
|
|
|
#[cfg(any(unix, rust_analyzer))]
|
|
struct UnixFileMode(u32);
|
|
|
|
#[cfg(any(unix, rust_analyzer))]
|
|
impl DisplayBit for UnixFileMode {
|
|
fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
use std::fmt::Write;
|
|
|
|
macro display_bit($self:expr, $letter:expr, $bit:expr) {
|
|
if $self.0 & (1 << $bit) != 0 {
|
|
$letter
|
|
} else {
|
|
'-'
|
|
}
|
|
}
|
|
|
|
for i in (0..3).rev() {
|
|
let r = i * 3 + 2;
|
|
let w = i * 3 + 1;
|
|
let x = i * 3;
|
|
f.write_char(display_bit!(self, 'r', r))?;
|
|
f.write_char(display_bit!(self, 'w', w))?;
|
|
f.write_char(display_bit!(self, 'x', x))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(any(unix, rust_analyzer))]
|
|
impl MetadataImpl for Metadata {
|
|
type Mode = UnixFileMode;
|
|
|
|
fn size(&self) -> u64 {
|
|
self.len()
|
|
}
|
|
|
|
fn inode(&self) -> Option<u32> {
|
|
self.ino().try_into().ok()
|
|
}
|
|
|
|
fn mode(&self) -> Self::Mode {
|
|
UnixFileMode(MetadataExt::mode(self))
|
|
}
|
|
|
|
fn mtime(&self) -> SystemTime {
|
|
self.modified().unwrap()
|
|
}
|
|
}
|
|
|
|
fn convert_file_time(time: SystemTime) -> chrono::DateTime<chrono::Utc> {
|
|
let timestamp = time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
|
|
chrono::DateTime::from_timestamp(timestamp.as_secs() as _, 0).unwrap()
|
|
}
|
|
|
|
fn time_now() -> chrono::DateTime<chrono::Utc> {
|
|
let now = SystemTime::now()
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.unwrap();
|
|
chrono::DateTime::from_timestamp(now.as_secs() as _, 0).unwrap()
|
|
}
|
|
|
|
impl DisplayBit for Option<Metadata> {
|
|
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
const MONTHS: &[&str] = &[
|
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
];
|
|
|
|
let now = time_now();
|
|
let this = self.as_ref();
|
|
let ino = this.and_then(MetadataImpl::inode);
|
|
let mode = this.map(MetadataImpl::mode);
|
|
let size = this.map(MetadataImpl::size);
|
|
let mtime = this
|
|
.map(MetadataImpl::mtime)
|
|
.map(convert_file_time)
|
|
.map(|time| {
|
|
if time.year() == now.year() {
|
|
format!(
|
|
"{} {:02} {:02}:{:02}",
|
|
MONTHS[time.month0() as usize],
|
|
time.day(),
|
|
time.hour(),
|
|
time.minute()
|
|
)
|
|
} else {
|
|
format!(
|
|
"{} {:02}, {:04}",
|
|
MONTHS[time.month0() as usize],
|
|
time.day(),
|
|
time.year()
|
|
)
|
|
}
|
|
});
|
|
|
|
if let Some(mode) = mode {
|
|
mode.display_bit(opts, f)?;
|
|
} else {
|
|
write!(f, "---------")?;
|
|
}
|
|
|
|
if let Some(size) = size {
|
|
write!(f, " {:>12}", size.display_size_with(opts))?;
|
|
} else {
|
|
write!(f, " {:>12}", "???")?;
|
|
}
|
|
|
|
if let Some(mtime) = mtime {
|
|
write!(f, " {:>14}", mtime)?;
|
|
} else {
|
|
write!(f, " {:<14}", "???")?;
|
|
}
|
|
|
|
if opts.inodes {
|
|
if let Some(ino) = ino {
|
|
write!(f, " {ino:>8}")?;
|
|
} else {
|
|
write!(f, " {:<8}", "---")?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl DisplayBit for Entry {
|
|
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// let ino = self.attrs.as_ref().and_then(<Metadata as MetadataExt>::inode);
|
|
|
|
if opts.long {
|
|
write!(
|
|
f,
|
|
"{}{} {}",
|
|
self.ty.display_with(opts),
|
|
self.attrs.display_with(opts),
|
|
self.name
|
|
)?;
|
|
|
|
if let Some(target) = self.target.as_ref() {
|
|
write!(f, " -> {}", target)?;
|
|
}
|
|
|
|
Ok(())
|
|
} else {
|
|
let ino = self
|
|
.attrs
|
|
.as_ref()
|
|
.and_then(<Metadata as MetadataImpl>::inode);
|
|
if opts.inodes {
|
|
if let Some(ino) = ino {
|
|
write!(f, "{ino:<8} ")?;
|
|
} else {
|
|
write!(f, "{:<8} ", "---")?;
|
|
}
|
|
}
|
|
f.write_str(&self.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Entry {
|
|
fn invalid() -> Self {
|
|
Self {
|
|
name: "???".to_owned(),
|
|
target: None,
|
|
ty: None,
|
|
attrs: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn sort_dirs_first(a: &Entry, b: &Entry) -> Ordering {
|
|
let a_is_dir = a.ty.map(|t| t.is_dir()).unwrap_or(false);
|
|
let b_is_dir = b.ty.map(|t| t.is_dir()).unwrap_or(false);
|
|
|
|
let by_type = Ord::cmp(&(b_is_dir as usize), &(a_is_dir as usize));
|
|
let by_name = Ord::cmp(&a.name, &b.name);
|
|
|
|
by_type.then(by_name)
|
|
}
|
|
|
|
fn include(filename: &OsString, all: bool) -> bool {
|
|
if all {
|
|
return true;
|
|
}
|
|
let Some(filename) = filename.to_str() else {
|
|
return true;
|
|
};
|
|
!filename.starts_with(".")
|
|
}
|
|
|
|
fn list_directory(path: &Path, all: bool) -> io::Result<Vec<Entry>> {
|
|
let mut entries = vec![];
|
|
for entry in read_dir(path)? {
|
|
let Ok(entry) = entry else {
|
|
entries.push(Entry::invalid());
|
|
continue;
|
|
};
|
|
|
|
let os_filename = entry.file_name();
|
|
|
|
if !include(&os_filename, all) {
|
|
continue;
|
|
}
|
|
|
|
let ty = entry.file_type().ok();
|
|
let attrs = entry.path().symlink_metadata().ok();
|
|
|
|
let target = if let Some(attrs) = attrs.as_ref()
|
|
&& attrs.is_symlink()
|
|
{
|
|
Some(match entry.path().read_link() {
|
|
Ok(res) => res.to_string_lossy().to_string(),
|
|
Err(_) => "???".into(),
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
entries.push(Entry {
|
|
name: os_filename.to_string_lossy().to_string(),
|
|
target,
|
|
ty,
|
|
attrs,
|
|
});
|
|
}
|
|
|
|
entries.sort_by(sort_dirs_first);
|
|
|
|
Ok(entries)
|
|
}
|
|
|
|
fn list(opts: &Args, path: &Path) -> io::Result<()> {
|
|
if path.is_dir() {
|
|
let entries = list_directory(path, opts.all)?;
|
|
|
|
for entry in entries {
|
|
println!("{}", entry.display_with(opts));
|
|
}
|
|
|
|
Ok(())
|
|
} else {
|
|
let attrs = path.symlink_metadata()?;
|
|
|
|
let target = attrs.is_symlink().then(|| match path.read_link() {
|
|
Ok(res) => res.display().to_string(),
|
|
Err(_) => "???".into(),
|
|
});
|
|
|
|
let entry = Entry {
|
|
name: path.display().to_string(),
|
|
ty: Some(attrs.file_type()),
|
|
attrs: Some(attrs),
|
|
target,
|
|
};
|
|
|
|
println!("{}", entry.display_with(opts));
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn run_inner(opts: &Args, paths: &[PathBuf], print_names: bool) -> Vec<Result<(), io::Error>> {
|
|
let mut results = vec![];
|
|
for path in paths {
|
|
if print_names {
|
|
println!("{}: ", path.display());
|
|
}
|
|
results.push(list(opts, path).inspect_err(|error| {
|
|
eprintln!("{}: {error}", path.display());
|
|
}));
|
|
}
|
|
results
|
|
}
|
|
|
|
fn run(opts: &Args) -> Vec<Result<(), io::Error>> {
|
|
if opts.paths.is_empty() {
|
|
run_inner(opts, &[".".into()], false)
|
|
} else {
|
|
run_inner(opts, &opts.paths, opts.paths.len() > 1)
|
|
}
|
|
}
|
|
|
|
pub fn main() -> ExitCode {
|
|
let args = Args::parse();
|
|
|
|
let results = run(&args);
|
|
let code = match results.iter().any(|e| e.is_err()) {
|
|
false => ExitCode::SUCCESS,
|
|
true => ExitCode::FAILURE,
|
|
};
|
|
|
|
code
|
|
}
|