diff --git a/Cargo.toml b/Cargo.toml index 2a128e9..8523280 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,5 +25,6 @@ rustix = { version = "1.0.7", features = [ "termios", "fs", "stdio", + "time", ] } sap = "0.0.5" diff --git a/src/bin/ls/main.rs b/src/bin/ls/main.rs index 3759390..2efb63f 100644 --- a/src/bin/ls/main.rs +++ b/src/bin/ls/main.rs @@ -1,40 +1,42 @@ -mod options; -mod settings; +pub(crate) mod options; +pub(crate) mod settings; +pub(crate) mod sorting; +pub(crate) mod traverse; use puppyutils::Result; -use rustix::{ - fs::{Dir, Mode, OFlags, open}, - termios::tcgetwinsize, -}; -use std::io::{self, BufWriter, stdout}; +use rustix::termios::tcgetwinsize; +use traverse::Printer; -const CURRENT_DIR_PATH: &str = "."; +use std::io::stdout; pub fn main() -> Result { let mut stdout = stdout(); let winsize = get_win_size(); - let _cfg = settings::parse_arguments(winsize.ws_col, &mut stdout)?; + let cfg = settings::parse_arguments(winsize.ws_col, &mut stdout)?; - let fd = open( - CURRENT_DIR_PATH, - OFlags::DIRECTORY | OFlags::RDONLY, - Mode::RUSR, - )?; + // let fd = open( + // cfg.directory(), + // OFlags::DIRECTORY | OFlags::RDONLY, + // Mode::RUSR, + // )?; - let dir = Dir::new(fd)?; + // let dir = Dir::new(fd)?; // bad bad bad // FIXME: do not allocate - let names = dir - .filter_map(Result::ok) - .map(|entry| entry.file_name().to_string_lossy().into_owned()) - .filter(|entry| !entry.starts_with('.')) - .collect::>(); + // let names = dir + // .filter_map(Result::ok) + // .map(|entry| entry.file_name().to_string_lossy().into_owned()) + // .filter(|entry| !entry.starts_with('.')) + // .collect::>(); - let mut stdout = BufWriter::new(stdout); + // let mut stdout = BufWriter::new(stdout); - print_all(names, &mut stdout)?; + // print_all(names, &mut stdout)?; + let printer = Printer::new(cfg, &mut stdout); + + printer?.traverse()?; Ok(()) } @@ -42,47 +44,3 @@ fn get_win_size() -> rustix::termios::Winsize { let stderr_fd = rustix::stdio::stderr(); tcgetwinsize(stderr_fd).expect("couldn't get terminal size") } - -// FIXME: This algorithm to print out lines is incredibly simplistic -// and slightly worse than the one used in GNU's ls. -fn print_all(cols: Vec, stdout: &mut O) -> Result { - const MIN_COLUMN_WIDTH: u16 = 3; - - let len = cols.len(); - let stderr_fd = rustix::stdio::stderr(); - let winsize = tcgetwinsize(stderr_fd).expect("couldn't get terminal size"); - - let max_idx = ((winsize.ws_col / 3) / MIN_COLUMN_WIDTH - 1) as usize; - - let max_cols = if max_idx < len { max_idx } else { len }; - - print_into_columns(cols.iter().map(String::as_str), max_cols, stdout) -} - -fn print_into_columns(iter: I, columns: usize, stdout: &mut O) -> Result -where - I: IntoIterator + core::fmt::Display>, -{ - let mut counter = 0; - for line in iter { - if counter == columns { - stdout.write_all(b"\n")?; - counter = 0; - } - - if counter == columns - 1 { - stdout.write_all(line.as_ref().as_bytes())?; - } else { - stdout.write_all(line.as_ref().as_bytes())?; - stdout.write_all(b" ")?; - } - - counter += 1; - } - - // fixes the shell returning a "return symbol" at the end. - stdout.write_all(b"\n")?; - stdout.flush()?; - - Ok(()) -} diff --git a/src/bin/ls/options.rs b/src/bin/ls/options.rs index fc1c15f..94bc54d 100644 --- a/src/bin/ls/options.rs +++ b/src/bin/ls/options.rs @@ -1,5 +1,8 @@ #![allow(dead_code, unused_variables)] + +use std::{fmt::Display, num::NonZero}; #[repr(u8)] +#[derive(Copy, Clone)] pub(crate) enum SortOrder { None, Name, @@ -123,3 +126,100 @@ from_bytes! { {Extension} => {b"extension"} {Width} => {b"width"} } + +#[derive(Debug)] +pub(crate) enum SizeParseError<'a> { + TooLarge(&'a [u8]), + InvalidSuffix(&'a [u8]), + InvalidArgument(&'a [u8]), +} + +impl Display for SizeParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + +impl std::error::Error for SizeParseError<'_> {} + +pub(crate) fn size_arg_to_multiplier(arg: &[u8]) -> Result, SizeParseError<'_>> { + use core::str::from_utf8_unchecked; + let mut num_len = 0; + + while arg[num_len].is_ascii_digit() { + num_len += 1; + } + + if num_len == 0 { + let err = SizeParseError::InvalidArgument(arg); + return Err(err); + } + + let digits = &arg[..num_len + 1]; + + // SAFETY: + // + // The above loop guarantees that + // the bytes in this subslice + // represent only ASCII digits. + let multiplier = unsafe { + from_utf8_unchecked(digits) + .parse::() + .expect("infallible") + }; + + if multiplier == 0 { + let err = SizeParseError::InvalidArgument(arg); + return Err(err); + } + + let (base, shift) = match &arg[num_len..] { + b"K" | b"KiB" => (1024_u64, 1_u32), + b"M" | b"MiB" => (1024, 10), + b"G" | b"GiB" => (1024, 20), + b"T" | b"TiB" => (1024, 30), + b"P" | b"PiB" => (1024, 40), + b"E" | b"EiB" => (1024, 50), + + b"KB" => (1024, 0), + b"MB" => (1000, 10), + b"GB" => (1000, 20), + b"TB" => (1000, 30), + b"PB" => (1000, 40), + b"EB" => (1000, 50), + + b"" => return unsafe { Ok(NonZero::new_unchecked(multiplier)) }, + + b"Z" | b"ZiB" | b"Y" | b"YiB" | b"R" | b"RiB" | b"ZB" | b"RB" | b"YB" => { + let err = SizeParseError::TooLarge(arg); + return Err(err); + } + + _ => return Err(SizeParseError::InvalidSuffix(arg)), + }; + + let unit = match base.checked_shl(shift) { + Some(val) => val, + None => { + let err = SizeParseError::TooLarge(arg); + return Err(err); + } + }; + + debug_assert!( + multiplier != 0 && unit != 0, + "this equation would end up as zero" + ); + + match unit.checked_mul(multiplier) { + None => Err(SizeParseError::TooLarge(arg)), + + // SAFETY: + // + // The check above that `multiplier` is non-zero + // guarantees this value is non-zero + // the other part of the multiplication (`unit`) is + // always guaranteed to be above 0 + Some(val) => unsafe { Ok(NonZero::new_unchecked(val)) }, + } +} diff --git a/src/bin/ls/settings.rs b/src/bin/ls/settings.rs index c48f5a6..c1a26bd 100644 --- a/src/bin/ls/settings.rs +++ b/src/bin/ls/settings.rs @@ -3,7 +3,9 @@ use super::options::*; use puppyutils::{Result, cli_with_args}; use sap::Parser; use std::io; +use std::num::NonZero; +const CURRENT_DIR_PATH: &str = "."; const DEFAULT_BLOCK_SIZE: usize = 512; fn needs_an_argument() -> ! { @@ -72,6 +74,7 @@ pub(crate) fn parse_arguments(width: u16, out: &mut O) -> Result(width: u16, out: &mut O) -> Result { - if let Some(_arg) = args.value() { - // do the block-size - } else { - needs_an_argument(); + // use crate::options::SizeParseError; + + if let Some(arg) = args.value() { + match size_arg_to_multiplier(arg.as_bytes()) { + Err(err) => match err { + SizeParseError::TooLarge(_arg) => { + todo!() + } + + SizeParseError::InvalidSuffix(_arg) => { + todo!() + } + + SizeParseError::InvalidArgument(_arg) => { + todo!() + } + + } + + Ok(val) => settings.size_unit = Some(val) + } } } @@ -401,42 +421,51 @@ pub(crate) fn parse_arguments(width: u16, out: &mut O) -> Result, + pub dir: Option, // block size - blk_size: usize, + pub blk_size: usize, // formatting used - format: Formatting, + pub format: Formatting, // line width. - width: u16, + pub width: u16, + + // size from `--block-size` + pub size_unit: Option>, +} + +impl LsConfig { + pub(crate) fn directory(&self) -> &str { + self.dir.as_deref().unwrap_or(CURRENT_DIR_PATH) + } } diff --git a/src/bin/ls/sorting.rs b/src/bin/ls/sorting.rs new file mode 100644 index 0000000..7b1d0e3 --- /dev/null +++ b/src/bin/ls/sorting.rs @@ -0,0 +1,144 @@ +use super::options::SortOrder; +use super::traverse::LsDisplay; +use core::cmp::Ordering; + +pub(crate) fn sorting_fn(order: SortOrder) -> fn(&LsDisplay, &LsDisplay) -> Ordering { + match order { + SortOrder::Name => sort_by_name, + SortOrder::Size => sort_by_size, + SortOrder::Extension => sort_by_extension, + SortOrder::AccessTime => sort_by_access_time, + SortOrder::Time => sort_by_time, + SortOrder::Width => sort_by_width, + SortOrder::Version => sort_by_version, + SortOrder::None => sort_by_none, + + SortOrder::Directory => unimplemented!(), + } +} + +fn sort_by_none(_lhs: &LsDisplay, _rhs: &LsDisplay) -> Ordering { + Ordering::Equal +} + +fn sort_by_name(lhs: &LsDisplay, rhs: &LsDisplay) -> Ordering { + lhs.file_name().cmp(rhs.file_name()) +} + +fn sort_by_size(lhs: &LsDisplay, rhs: &LsDisplay) -> Ordering { + let lhs_stat = lhs + .stat() + .unwrap_or_else(|| unreachable!("`sort_by_size` requires a present `Stat`")); + + let rhs_stat = rhs + .stat() + .unwrap_or_else(|| unreachable!("`sort_by_size` requires a present `Stat`")); + + lhs_stat.stx_size.cmp(&rhs_stat.stx_size) +} + +fn sort_by_extension(lhs: &LsDisplay, rhs: &LsDisplay) -> Ordering { + let mut lhs_extension_start_ix = 0; + + // short circuit + // if the names are the same + if lhs.file_name() == rhs.file_name() { + return Ordering::Equal; + } + + for (ix, byte) in lhs.file_name().to_bytes().iter().enumerate() { + if byte == &b'.' { + lhs_extension_start_ix = ix; + } + } + + let mut rhs_extension_start_ix = 0; + + for (ix, byte) in rhs.file_name().to_bytes().iter().enumerate() { + if byte == &b'.' { + rhs_extension_start_ix = ix; + } + } + + lhs.file_name()[lhs_extension_start_ix..].cmp(&rhs.file_name()[rhs_extension_start_ix..]) +} + +fn sort_by_access_time(lhs: &LsDisplay, rhs: &LsDisplay) -> Ordering { + let lhs_stat = lhs + .stat() + .unwrap_or_else(|| unreachable!("`sort_by_access_time` requires a present `Stat`")); + + let rhs_stat = rhs + .stat() + .unwrap_or_else(|| unreachable!("`sort_by_access_time` requires a present `Stat`")); + + lhs_stat.stx_atime.tv_sec.cmp(&rhs_stat.stx_atime.tv_sec) +} + +fn sort_by_time(lhs: &LsDisplay, rhs: &LsDisplay) -> Ordering { + let lhs_stat = lhs + .stat() + .unwrap_or_else(|| unreachable!("`sort_by_time` requires a present `Stat`")); + + let rhs_stat = rhs + .stat() + .unwrap_or_else(|| unreachable!("`sort_by_time` requires a present `Stat`")); + + lhs_stat.stx_mtime.tv_sec.cmp(&rhs_stat.stx_mtime.tv_sec) +} + +fn sort_by_width(lhs: &LsDisplay, rhs: &LsDisplay) -> Ordering { + // might be naive + // but that is what impls of ls + // seem to do + lhs.file_name() + .to_bytes() + .len() + .cmp(&rhs.file_name().to_bytes().len()) +} + +fn sort_by_version(lhs: &LsDisplay, rhs: &LsDisplay) -> Ordering { + if lhs.file_name() == rhs.file_name() { + return Ordering::Equal; + } + + let mut lhs_iter = lhs.file_name().to_bytes().iter(); + let mut rhs_iter = rhs.file_name().to_bytes().iter(); + + while let Some(left) = lhs_iter.next() + && let Some(right) = rhs_iter.next() + { + // If not a digit, just compare by byte value + if !left.is_ascii_digit() || !right.is_ascii_digit() { + if left != right { + return left.cmp(right); + } + + continue; + } + + // Trailing zeroes + if *left == b'0' && *right == b'0' { + let mut zero_count_lhs = 0; + + while let Some(0) = lhs_iter.next() { + zero_count_lhs += 1; + } + + let mut zero_count_rhs = 0; + + while let Some(0) = rhs_iter.next() { + zero_count_rhs += 1; + } + + if zero_count_lhs != zero_count_rhs { + return zero_count_lhs.cmp(&zero_count_lhs); + } + + // special case here + } else { + // handling of actual digits + } + } + todo!() +} diff --git a/src/bin/ls/traverse.rs b/src/bin/ls/traverse.rs new file mode 100644 index 0000000..bb92cae --- /dev/null +++ b/src/bin/ls/traverse.rs @@ -0,0 +1,756 @@ +// temporary +#![allow(dead_code)] + +use super::options::Formatting; +use super::settings::{LsConfig, LsFlags}; +use super::sorting; +use acumen::{Passwd, getpwuid}; +use core::cmp; +use puppyutils::Result; + +use rustix::ffi; +use rustix::fs::{AtFlags, Dir, Mode, OFlags, Statx, StatxFlags, Uid, open, statx}; + +use std::ffi::CStr; +use std::ffi::CString; +use std::io::Write; +use std::os::fd::OwnedFd; + +/* File type masks */ +const SOCKET: u16 = 0o0140000; +const SYMBOLIC_LINK: u16 = 0o0120000; +const REGULAR_FILE: u16 = 0o0100000; +const BLOCK_DEVICE: u16 = 0o0060000; +const DIRECTORY: u16 = 0o0040000; +const CHAR_DEVICE: u16 = 0o0020000; +const FIFO_NAMED_PIPE: u16 = 0o0010000; + +/// The size required by a string of text +/// representing a human readable file size +/// +/// like `5.1K` or `51M` +const HUMAN_READABLE_SIZE_LENGTH: usize = 4; + +/// Stores state that is related +/// to printing out the `ls` output +/// +/// also stores a mutable reference to +/// a `Write` object. +pub(crate) struct Printer<'a, O> { + stdout: &'a mut O, + cfg: LsConfig, + fd: OwnedFd, + base_dir: String, + + // longest file size + longest_size: u64, + + // longest owner name (+ author name) + longest_owner: usize, + + // longest symlink amount + longest_symlink: usize, + + // longest group name, + longest_group_name: usize, +} + +impl<'a, O> Printer<'a, O> { + /// Creates a new `Printer` + pub(crate) fn new(cfg: LsConfig, stdout: &'a mut O) -> Result + where + O: Write, + { + let base_dir = cfg.directory(); + + let fd = open(base_dir, OFlags::DIRECTORY | OFlags::RDONLY, Mode::RUSR)?; + + let res = Self { + base_dir: base_dir.to_owned(), + stdout, + cfg, + fd, + longest_size: 0, + longest_owner: 0, + longest_symlink: 0, + longest_group_name: 0, + }; + + Ok(res) + } + + /// Checks if we shouldn't print the group column + /// in long format + fn no_groups(&self) -> bool { + self.cfg.flags.contains(LsFlags::NO_GROUPS_LISTED) + } + + /// Checks if we should print the author + /// + /// On Unix systems, this is going to just be the + /// owner of the file + fn show_file_author(&self) -> bool { + self.cfg.flags.contains(LsFlags::PRINT_AUTHOR) + } + + /// Checks if we shouldn't print the user column + /// + /// This is used by the `-g` parameter + fn no_users(&self) -> bool { + debug_assert!( + matches!(self.cfg.format, Formatting::Long), + "the `-g` option was used, however the formatting was not set to `Long`" + ); + self.cfg.flags.contains(LsFlags::NO_OWNER_LISTED) + } + + /// Checks if sizes should be printed in a human-readable form. + /// + /// like `3.4M` + fn human_readable(&self) -> bool { + self.cfg.flags.contains(LsFlags::HUMAN_READABLE_SIZES) + } + + /// Checks if sizes should be calculated using SI units + /// + /// (powers of 10 instead of powers of 2) + fn si_units(&self) -> bool { + self.cfg.flags.contains(LsFlags::SI_SIZES) + } + + // Do we show all files with `.` at the start + fn show_dot_files(&self) -> bool { + self.cfg.flags.contains(LsFlags::NOT_IGNORE_DOTS) + } + + /// Do we show the current and parent directory entries + /// + /// (`.` and `..`) + fn show_parent_and_current_directory(&self) -> bool { + self.cfg.flags.contains(LsFlags::IGNORE_DOTS_EXCEPT_DIRS) + } + + /// Is the format of formatting set to `Formatting::Long` + /// + /// for example by the `-l` option. + fn is_long_format(&self) -> bool { + matches!(self.cfg.format, Formatting::Long) + } +} + +impl Printer<'_, O> { + /// Traverses the directory + /// to form `LsDisplay` entries + /// which will be passed forward + /// for sorting and printing. + pub(crate) fn traverse(&mut self) -> Result { + let dir = Dir::read_from(&self.fd)?; + let mut displays = Vec::with_capacity(16); + self.construct(&mut displays, dir, self.needs_stat())?; + + // Somehow sort the vec of displays yeah + displays.sort_by(sorting::sorting_fn(self.cfg.order)); + + // find the longest values + for display in &mut displays { + // we need the longest: + // size + // symlink amount + // user name + // group name + + if let Some(ref stat) = display.stat { + if self.human_readable() { + self.longest_size = HUMAN_READABLE_SIZE_LENGTH as u64 + } else { + self.longest_size = + cmp::max(self.longest_size, number_length_u64(stat.stx_size) as u64) + } + + // maybe check if you actually need the user/group name + if let Some(passwd) = getpwuid(Uid::from_raw(stat.stx_uid)) { + self.longest_owner = cmp::max(self.longest_owner, passwd.name.len()); + + display.passwd = Some(passwd) // unwrapping here is weird but we need the name length + } else { + unreachable!("unable to obtain passwd for this uid") // redundant however nice for debugging + } + + // group name + self.longest_group_name = 7; + + // symlink amount + self.longest_symlink = cmp::max( + self.longest_symlink, + number_length_u64(stat.stx_nlink as u64) as usize, + ); + } + } + + // find the longest values + for display in &mut displays { + // we need the longest: + // size + // symlink amount + // user name + // group name + + if let Some(ref stat) = display.stat { + if self.human_readable() { + self.longest_size = HUMAN_READABLE_SIZE_LENGTH as u64 + } else { + self.longest_size = + cmp::max(self.longest_size, number_length_u64(stat.stx_size) as u64) + } + + // maybe check if you actually need the user/group name + if let Some(passwd) = getpwuid(Uid::from_raw(stat.stx_uid)) { + self.longest_owner = cmp::max(self.longest_owner, passwd.name.len()); + + display.passwd = Some(passwd) // unwrapping here is weird but we need the name length + } else { + unreachable!("unable to obtain passwd for this uid") // redundant however nice for debugging + } + + // group name + self.longest_group_name = 7; + + // symlink amount + self.longest_symlink = cmp::max( + self.longest_symlink, + number_length_u64(stat.stx_nlink.into()) as usize, + ); + } + } + + for display in displays { + self.print_entry(display)? + } + + Ok(()) + } + + fn needs_stat(&self) -> bool { + true + } + + /// Prints a singular entry + /// + /// Format specified by commandline arguments + /// or the default + fn print_entry(&mut self, display: LsDisplay) -> Result { + if self.cfg.flags.contains(LsFlags::RECURSIVE) { + self.stdout.write_all(display.file_name.to_bytes())?; + self.stdout.write_all(b":\n")?; + } + + match self.cfg.format { + Formatting::Long => self.print_entry_long(display), + _ => todo!(), + } + } + + /// Printing for `Formatting::Long` + fn print_entry_long(&mut self, display: LsDisplay) -> Result { + let stat = match display.stat { + None => unreachable!("long format should have stat"), + Some(ref stat) => stat, + }; + + // print file type + print_filetype(self.stdout, stat)?; + + // print permissions + let perms = Permissions::new(stat); + perms.print_permissions(self.stdout)?; + + // print link amount + let longest = self.longest_symlink; + write!(self.stdout, "{:>longest$} ", stat.stx_nlink)?; + + if !self.no_users() { + print_owner(self.stdout, stat.stx_uid, self.longest_owner)?; + } + + if !self.no_groups() { + print_group(self.stdout, stat.stx_gid, self.longest_group_name)?; + } + + if self.show_file_author() { + // on Unix, the file author and owner + // are the same + print_owner(self.stdout, stat.stx_uid, self.longest_owner)?; + } + + print_size( + self.stdout, + self.human_readable(), + self.si_units(), + stat.stx_size, + self.cfg.size_unit.map(Into::into).unwrap_or(1), + self.longest_size as usize, // might be bad, + )?; + + // change later to detect time + let date = Date::new(stat.stx_ctime.tv_sec); + print_date(self.stdout, date)?; + + self.stdout.write_all(display.file_name.to_bytes())?; + self.stdout.write_all(b"\n")?; + + Ok(()) + } + + /// Constructs `LsDisplay` structs for printing + fn construct(&mut self, displays: &mut Vec, dir: Dir, needs_stat: bool) -> Result { + for entry in dir { + let entry = entry?; + let file_name = entry.file_name(); + + if self.skip(file_name) { + continue; + } + + let mut display = LsDisplay { + file_name: file_name.to_owned(), + stat: None, + group_name: None, + passwd: None, + }; + + if needs_stat { + let stat = statx( + &self.fd, + entry.file_name(), + AtFlags::empty(), + StatxFlags::MODE + | StatxFlags::GID + | StatxFlags::SIZE + | StatxFlags::TYPE + | StatxFlags::UID + | StatxFlags::BTIME + | StatxFlags::ATIME + | StatxFlags::MTIME + | StatxFlags::CTIME + | StatxFlags::BASIC_STATS + | StatxFlags::NLINK, + )?; + + if self.is_long_format() { + let new_size = if self.human_readable() { + HUMAN_READABLE_SIZE_LENGTH + } else { + number_length_u64(stat.stx_size) as usize + }; + + if new_size as u64 > self.longest_size { + self.longest_size = new_size as u64 + } + } + + // if let Some(group) = acumen::group::GroupEntries::new()? {}; + + display.stat = Some(stat) + } + + displays.push(display) + } + + Ok(()) + } + + /// Determines whether an entry should be skipped + fn skip(&self, file_name: &CStr) -> bool { + let bytes = file_name.to_bytes(); + + (!self.show_dot_files() && bytes.first().copied().is_some_and(|byte| byte == b'.')) + || (!self.show_parent_and_current_directory() && (bytes == b".." || bytes == b".")) + } +} + +/// An "entry" of a singular file (or directory) +/// that will be printed out. +#[non_exhaustive] +pub(crate) struct LsDisplay { + stat: Option, + file_name: CString, + group_name: Option, + passwd: Option, +} + +impl LsDisplay { + pub(crate) fn stat(&self) -> Option<&Statx> { + self.stat.as_ref() + } + + pub(crate) fn file_name(&self) -> &CStr { + &self.file_name + } +} + +/// Opaque struct to handle Unix permissions +struct Permissions(u16); + +impl Permissions { + const OWNER_ALL: u16 = 0o0700; + const OWNER_READ: u16 = 0o0400; + const OWNER_WRITE: u16 = 0o0200; + const OWNER_EXEC: u16 = 0o0100; + + const GROUP_ALL: u16 = 0o0070; + const GROUP_READ: u16 = 0o0040; + const GROUP_WRITE: u16 = 0o0020; + const GROUP_EXEC: u16 = 0o0010; + + const OTHERS_ALL: u16 = 0o0007; + const OTHERS_READ: u16 = 0o0004; + const OTHERS_WRITE: u16 = 0o0002; + const OTHERS_EXEC: u16 = 0o0001; + + fn new(stat: &Statx) -> Self { + Self(stat.stx_mode & 0o7777) + } + + fn owner(&self) -> [u8; 3] { + let mut perms = [b'-'; 3]; + + if self.0 == Self::OWNER_ALL { + perms = [b'r', b'w', b'x']; + return perms; + } + + if self.0 >= Self::OWNER_READ { + perms[0] = b'r'; + } + + if self.0 >= Self::OWNER_WRITE { + perms[1] = b'w'; + } + + if self.0 >= Self::OWNER_EXEC { + perms[2] = b'x'; + } + + perms + } + + fn group(&self) -> [u8; 3] { + let mut perms = [b'-'; 3]; + + if self.0 == Self::GROUP_ALL { + perms = [b'r', b'w', b'x']; + return perms; + } + + if self.0 >= Self::GROUP_READ { + perms[0] = b'r'; + } + + if self.0 >= Self::GROUP_WRITE { + perms[1] = b'w'; + } + + if self.0 >= Self::GROUP_EXEC { + perms[2] = b'x'; + } + + perms + } + + fn others(&self) -> [u8; 3] { + let mut perms = [b'-'; 3]; + + if self.0 == Self::OTHERS_ALL { + perms = [b'r', b'w', b'x']; + return perms; + } + + if self.0 >= Self::OTHERS_READ { + perms[0] = b'r'; + } + + if self.0 >= Self::OTHERS_WRITE { + perms[1] = b'w'; + } + + if self.0 >= Self::OTHERS_EXEC { + perms[2] = b'x'; + } + + perms + } + + fn print_permissions(&self, out: &mut O) -> Result + where + O: Write, + { + out.write_all(&self.owner())?; + out.write_all(&self.group())?; + out.write_all(&self.others())?; + + out.write_all(b" ").map_err(Into::into) + } +} + +/// Prints the character representing a file type +/// into `out` +fn print_filetype(out: &mut O, stat: &Statx) -> Result +where + O: Write, +{ + let file_char = match stat.stx_mode & 0o170000 { + SOCKET => b's', + SYMBOLIC_LINK => b'l', + REGULAR_FILE => b'-', + BLOCK_DEVICE => b'b', + DIRECTORY => b'd', + CHAR_DEVICE => b'c', + FIFO_NAMED_PIPE => b'p', + + _ => unreachable!("there are no more file types present"), + }; + + out.write_all(&[file_char]).map_err(Into::into) +} + +/// Prints the user from the `uid` +fn print_owner(out: &mut O, uid: ffi::c_uint, _width: usize) -> Result +where + O: Write, +{ + let Some(passwd) = getpwuid(Uid::from_raw(uid)) else { + // somehow error out? + todo!() + }; + + write!(out, "{} ", passwd.name)?; + Ok(()) +} + +/// Prints the group from the `gid` +fn print_group(_out: &mut O, _gid: u32, _width: usize) -> Result +where + O: Write, +{ + // let Some(group) = getpwuid(Uid::from_raw(gid)) else { + // // error out + // todo!() + // }; + // write!(out, "{} ", group.name)?; + + Ok(()) +} + +/// Prints the size of the entry +/// +/// if `si_units` is `true` it uses `1000` instead of `1024` +/// for division +/// +/// if `human_readable` is `true` it writes sizes in a +/// human readable format (like `4.2M`) +fn print_size( + out: &mut O, + human_readable: bool, + si_units: bool, + mut size: u64, + scale: u64, + width: usize, +) -> Result +where + O: Write + ?Sized, +{ + if human_readable { + let mut buf: [u8; HUMAN_READABLE_SIZE_LENGTH] = [0_u8; HUMAN_READABLE_SIZE_LENGTH]; + humanize_number(&mut (&mut buf as &mut [u8]), size, si_units); + } else if scale == 1 { + write!(out, "{size:>width$} ")?; + } else { + while size >= scale { + size /= scale; + } + + write!(out, "{size:>width$} ")?; + } + Ok(()) +} + +fn print_date(out: &mut O, date: Date) -> Result +where + O: Write + ?Sized, +{ + let month = date.month_as_str(); + let day = date.days; + write!(out, "{month} {day:>2} ")?; + + if date.hours < 10 { + write!(out, "0")?; + } + write!(out, "{}:", date.hours)?; + + if date.minutes < 10 { + write!(out, "0")?; + } + write!(out, "{} ", date.minutes).map_err(Into::into) +} + +fn humanize_number(buf: &mut O, num: u64, si: bool) +where + O: Write + ?Sized, +{ + const UNITS: [&str; 8] = ["", "K", "M", "G", "T", "P", "E", "Z"]; + + let divisor: f64 = [1024.0, 1000.0][si as usize]; + let maximum_scale = 7; + + let mut scale = 0; + let mut floating_num = num as f64; + + while (floating_num >= divisor) && scale < maximum_scale { + floating_num /= divisor; + scale += 1; + } + + write!(&mut *buf, "{:.1}{}", floating_num, UNITS[scale]).expect("infallible"); +} + +pub(crate) struct Date { + year: i64, + month: i64, + days: i64, + hours: i64, + minutes: i64, + seconds: i64, + weekday: i64, +} + +impl Date { + pub(crate) fn new(seconds: i64) -> Self { + const MONTH_DAYS: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut minutes = seconds / 60; + let seconds = seconds - minutes * 60; + + let mut hours = minutes / 60; + minutes -= hours * 60; + + let mut days = hours / 24; + hours -= days * 24; + + let mut year = 1970; // unix starts from 1970 + let mut week_day: i64 = 4; // on a thursday + + let mut month = 0; // this will be overwritten anyway + + loop { + let leap_year = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + let days_in_a_year = 365 + i64::from(leap_year); + + if days >= days_in_a_year { + week_day += 1 + i64::from(leap_year); + days -= days_in_a_year; + + if week_day >= 7 { + week_day -= 7; + year += 1; + } + } else { + week_day += days; + week_day %= days; + + for (month_index, month_day) in MONTH_DAYS.iter().enumerate() { + month = month_index as i64; + let cmp = *month_day + i64::from(month_index == 1 && leap_year); + + if days >= cmp { + days -= cmp; + } else { + break; + } + } + + break; + } + } + + month += 1; + + Self { + year, + month, + days, + hours, + minutes, + seconds, + weekday: week_day, + } + } + pub(crate) fn month_as_str(&self) -> &'static str { + const MONTH_NAMES: [&str; 13] = [ + "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + + debug_assert!(self.month != 0, "month was set to 0, which is invalid"); + + MONTH_NAMES[self.month as usize] + } + + pub(crate) fn hours_minutes_as_str<'a, 'b>(&'a self, buf: &'b mut [u8]) -> Option<&'b str> + where + 'b: 'a, + { + const END_OF_BUFFER: usize = 5; + + if buf.len() < 5 { + return None; + } + + write!(&mut *buf, "{}:{}", self.hours, self.minutes) + .expect("infallible, this is a memory buffer"); + + unsafe { Some(core::str::from_utf8_unchecked(&buf[..END_OF_BUFFER])) } + } + + /// Turns the `days` in the `Date` struct + /// into a padded value that is written + /// into `buf` + /// + /// Example: + /// + /// `9` -> `" 9"` + /// `20` -> `"10"` + /// + /// if `buf` doesn't have a length of atleast 2 + /// the day written may be malformed. + pub(crate) fn day_with_padding(&self, mut buf: &mut [u8]) -> Result { + if self.days < 10 { + write!(buf, " ")?; + }; + + write!(buf, "{}", self.days).map_err(Into::into) + } +} + +#[inline] +fn number_length_u64(n: u64) -> u32 { + match n { + 0..=9 => 1, + 10..=99 => 2, + 100..=999 => 3, + 1000..=9999 => 4, + 10000..=99999 => 5, + 100000..=999999 => 6, + 1000000..=9999999 => 7, + 100000000..=999999999 => 8, + 1000000000..=9999999999 => 9, + 10000000000..=99999999999 => 10, + 100000000000..=999999999999 => 11, + 1000000000000..=9999999999999 => 12, + 10000000000000..=99999999999999 => 13, + 100000000000000..=999999999999999 => 14, + 1000000000000000..=9999999999999999 => 15, + 10000000000000000..=99999999999999999 => 16, + 100000000000000000..=999999999999999999 => 17, + 1000000000000000000..=9999999999999999999 => 18, + 10000000000000000000..=18446744073709551615 => 19, + _ => unsafe { std::hint::unreachable_unchecked() }, + } +}