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
}