#![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, } 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; 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 fmt::Display for DisplayWith<'_, '_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.item.display_bit(self.opts, f) } } impl 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, ty: Option, attrs: Option, } impl DisplayBit for Option { 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 { 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 { 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 { let timestamp = time.duration_since(SystemTime::UNIX_EPOCH).unwrap(); chrono::DateTime::from_timestamp(timestamp.as_secs() as _, 0).unwrap() } fn time_now() -> chrono::DateTime { let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); chrono::DateTime::from_timestamp(now.as_secs() as _, 0).unwrap() } impl DisplayBit for Option { 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(::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(::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> { 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> { 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> { 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 }