use crate::working_directory;
use chrono::DateTime;
use chrono::Local;
use chrono::Utc;
use std::fs;
use std::fs::DirEntry;
use std::fs::Permissions;
use std::io;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::PermissionsExt;
use std::time::SystemTime;
#[derive(Copy, Clone)]
enum SortBy {
None,
Size,
Modified,
LatsAccess,
Created,
}
struct LsParams<'a> {
handle: &'a mut Box<dyn Write>,
hidden: bool,
long_list: bool,
print_owner: bool,
print_group: bool,
recursive: bool,
reverse: bool,
sort_by: SortBy,
time_display: SortBy,
unit_suffixes: bool,
is_longtime_format: bool,
}
pub fn ls(args: &str, handle: &mut Box<dyn Write>) -> Result<(), Box<dyn std::error::Error>> {
let mut params = LsParams {
handle,
hidden: false,
long_list: false,
print_owner: true,
print_group: true,
recursive: false,
reverse: false,
sort_by: SortBy::None,
time_display: SortBy::Modified,
unit_suffixes: false,
is_longtime_format: false,
};
let mut directory = working_directory()?;
for arg in args.split_whitespace() {
let mut options = arg.chars();
if options.next() == Some('-') {
loop {
match options.next() {
None | Some(' ') => break,
Some('a') => params.hidden = true,
Some('l') => if params.long_list == false {
params.long_list = true;
}
else {
params.hidden = true;
},
Some('C') => params.long_list = false,
Some('g') => {
params.long_list = true;
params.print_owner = false
}
Some('o') => {
params.long_list = true;
params.print_group = false
}
Some('R') => params.recursive = true,
Some('h') => params.unit_suffixes = true,
Some('c') => params.time_display = SortBy::Modified,
Some('S') => params.sort_by = SortBy::Size,
Some('t') => params.sort_by = params.time_display,
Some('u') => params.time_display = SortBy::LatsAccess,
Some('f') => params.sort_by = SortBy::None,
Some('r') => params.reverse = true,
Some('T') => params.is_longtime_format = true,
Some('U') => params.time_display = SortBy::Created,
Some(invalid) => {
eprintln!("Invalid argument {}", invalid);
return Ok(());
}
}
}
} else {
directory = arg.to_string();
}
}
let ret = print_directory(directory, &mut params);
match ret {
Ok(()) => {}
Err(error) => {
eprintln!("Error: {}", error);
}
}
Ok(())
}
#[cfg(not(docsrs))]
fn print_directory(
directory: String,
ls_params: &mut LsParams,
) -> Result<(), Box<dyn std::error::Error>> {
if ls_params.recursive {
writeln!(ls_params.handle, "\n{}", directory)?;
}
let mut paths: Vec<_> = fs::read_dir(directory.clone())?
.map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error>))
.collect::<Result<Vec<_>, _>>()?;
let width = longest_filename_length(&paths)?;
if ls_params.long_list {
let size = sum_file_sizes(&paths, ls_params.hidden)?;
writeln!(ls_params.handle, "total {}", size)?;
}
sort(&mut paths, &ls_params);
if ls_params.long_list == false {
write!(ls_params.handle, "\n")?;
}
for path in paths {
let __filename = path.file_name();
let _filename = __filename.to_str().ok_or("Invalid UTF-8 in file name")?;
let file_type = path.file_type()?;
let mut symbolic_filename = String::new();
if file_type.is_symlink() {
match fs::read_link(path.path()) {
Ok(target) => {
symbolic_filename = target.to_str().ok_or("Invalid UTF-8 in file name")?.to_string();
symbolic_filename = format!(" -> {}", symbolic_filename);
},
Err(e) => eprintln!("Failed to read symlink target for {}: {}", _filename, e),
}
}
let filename = format!("{}{}", _filename, symbolic_filename);
if ls_params.hidden == false && filename.starts_with('.') {
continue;
}
if ls_params.long_list == false {
write!(ls_params.handle, "{:width$}", filename, width = width)?;
} else {
if let Ok(metadata) = path.metadata() {
let file_len = if ls_params.unit_suffixes {
unit_suffixes(metadata.len())
} else {
format!("{}", metadata.len())
};
let permissions = permission_text(metadata.permissions());
let owner: String = match get_user_by_uid(metadata.uid()) {
Some(user) => user.name().to_string_lossy().into_owned(),
None => String::new(),
};
let group: String = match get_group_by_gid(metadata.gid()) {
Some(a) => {
a.name().to_string_lossy().to_string()
},
None => String::new()
};
let file_date = match ls_params.time_display {
SortBy::LatsAccess => metadata.accessed(),
SortBy::Created => metadata.created(),
_ => metadata.modified(),
};
let utc_time: DateTime<Utc> = file_date?.into();
let local_time: DateTime<Local> = utc_time.with_timezone(&Local);
let file_date = if ls_params.is_longtime_format {
format!("{}", local_time.format("%Y-%m-%d %H:%M:%S"))
} else {
format!("{}", local_time.format("%d/%m/%Y %H:%M"))
};
let is_dir = path.path().is_dir();
let formatted_owner = if ls_params.print_owner {
format!("{:10}", owner)
} else {
String::new()
};
let formatted_group = if ls_params.print_group {
format!("{:10}", group)
} else {
String::new()
};
writeln!(
ls_params.handle,
"{} {} {} {:>7} {:3} {} {}",
permissions,
formatted_owner,
formatted_group,
file_len,
if is_dir { "dir" } else { " " }, file_date,
filename
)
.expect("Failed to write to handle");
}
}
}
if ls_params.long_list == false {
writeln!(ls_params.handle, "")?;
}
if ls_params.recursive {
let paths = fs::read_dir(directory.clone());
match paths {
Ok(paths) => {
for path in paths {
let path = path?;
let is_dir = path.path().is_dir();
if is_dir {
let filename = path.file_name();
let dir_name = filename.to_str().ok_or("Invalid UTF-8 in file name")?;
print_directory(
format!("{directory}/{}", &dir_name.to_string()),
ls_params,
)?;
}
}
}
Err(error) => {
if error.kind() == io::ErrorKind::PermissionDenied {
return Ok(());
}
eprintln!("{} {}", directory, error);
return Ok(());
}
}
}
Ok(())
}
#[cfg(docsrs)]
fn print_directory(
directory: String,
ls_params: &mut LsParams,
) -> Result<(), Box<dyn std::error::Error>> {
if ls_params.recursive {
writeln!(ls_params.handle, "\n{}", directory)?;
}
.map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error>))
.collect::<Result<Vec<_>, _>>()?;
let width = longest_filename_length(&paths)?;
if ls_params.long_list {
let size = sum_file_sizes(&paths, ls_params.hidden)?;
writeln!(ls_params.handle, "total {}", size)?;
}
sort(&mut paths, &ls_params);
if ls_params.long_list == false {
write!(ls_params.handle, "\n")?;
}
for path in paths {
let __filename = path.file_name();
let _filename = __filename.to_str().ok_or("Invalid UTF-8 in file name")?;
let file_type = path.file_type()?;
let mut symbolic_filename = String::new();
if file_type.is_symlink() {
match fs::read_link(path.path()) {
Ok(target) => {
symbolic_filename = target.to_str().ok_or("Invalid UTF-8 in file name")?.to_string();
symbolic_filename = format!(" -> {}", symbolic_filename);
},
Err(e) => eprintln!("Failed to read symlink target for {}: {}", _filename, e),
}
}
let filename = format!("{}{}", _filename, symbolic_filename);
if ls_params.hidden == false && filename.starts_with('.') {
continue;
}
if ls_params.long_list == false {
write!(ls_params.handle, "{:width$}", filename, width = width)?;
} else {
if let Ok(metadata) = path.metadata() {
let file_len = if ls_params.unit_suffixes {
unit_suffixes(metadata.len())
} else {
format!("{}", metadata.len())
};
let permissions = permission_text(metadata.permissions());
let owner: String = id_to_username(metadata.uid())?;
let group = id_to_username(metadata.gid())?;
let file_date = match ls_params.time_display {
SortBy::LatsAccess => metadata.accessed(),
SortBy::Created => metadata.created(),
_ => metadata.modified(),
};
let utc_time: DateTime<Utc> = file_date?.into();
let local_time: DateTime<Local> = utc_time.with_timezone(&Local);
let file_date = if ls_params.is_longtime_format {
format!("{}", local_time.format("%Y-%m-%d %H:%M:%S"))
} else {
format!("{}", local_time.format("%d/%m/%Y %H:%M"))
};
let is_dir = path.path().is_dir();
let formatted_owner = if ls_params.print_owner {
format!("{:10}", owner)
} else {
String::new()
};
let formatted_group = if ls_params.print_group {
format!("{:10}", group)
} else {
String::new()
};
writeln!(
ls_params.handle,
"{} {} {} {:>7} {:3} {} {}",
permissions,
formatted_owner,
formatted_group,
file_len,
if is_dir { "dir" } else { " " }, file_date,
filename
)
.expect("Failed to write to handle");
}
}
}
if ls_params.long_list == false {
writeln!(ls_params.handle, "")?;
}
if ls_params.recursive {
match paths {
Ok(paths) => {
for path in paths {
let path = path?;
let is_dir = path.path().is_dir();
if is_dir {
let filename = path.file_name();
let dir_name = filename.to_str().ok_or("Invalid UTF-8 in file name")?;
print_directory(
format!("{directory}/{}", &dir_name.to_string()),
ls_params,
)?;
}
}
}
Err(error) => {
if error.kind() == io::ErrorKind::PermissionDenied {
return Ok(());
}
eprintln!("{} {}", directory, error);
return Ok(());
}
}
}
Ok(())
}
fn sort(paths: &mut Vec<DirEntry>, ls_params: &LsParams) {
match ls_params.sort_by {
SortBy::None => {
paths.sort_by_key(|dir| {
dir.file_name()
.into_string()
.unwrap_or_else(|_| String::from(""))
});
if ls_params.reverse {
paths.reverse();
}
}
SortBy::Size => {
paths.sort_by_key(|dir| dir.metadata().map(|m| m.len()).unwrap_or(0));
if ls_params.reverse == false {
paths.reverse();
}
}
SortBy::Modified => {
paths.sort_by(|a, b| {
let a_time = a.metadata().and_then(|m| m.modified()).unwrap_or(SystemTime::UNIX_EPOCH);
let b_time = b.metadata().and_then(|m| m.modified()).unwrap_or(SystemTime::UNIX_EPOCH);
if ls_params.reverse {
a_time.cmp(&b_time)
} else {
a_time.cmp(&b_time)
}
});
}
SortBy::LatsAccess => {
paths.sort_by(|a, b| {
let time_a = a
.metadata()
.and_then(|m| m.accessed())
.unwrap_or(SystemTime::UNIX_EPOCH);
let time_b = b
.metadata()
.and_then(|m| m.accessed())
.unwrap_or(SystemTime::UNIX_EPOCH);
if ls_params.reverse {
time_a.cmp(&time_b)
} else {
time_b.cmp(&time_a)
}
});
}
SortBy::Created => {
paths.sort_by(|a, b| {
let time_a = a
.metadata()
.and_then(|m| m.created())
.unwrap_or(SystemTime::UNIX_EPOCH);
let time_b = b
.metadata()
.and_then(|m| m.created())
.unwrap_or(SystemTime::UNIX_EPOCH);
if ls_params.reverse {
time_a.cmp(&time_b)
} else {
time_b.cmp(&time_a)
}
});
}
}
}
fn permission_text(permissions: Permissions) -> String {
let mut perms = permissions.mode();
let mut retval = String::new();
for _i in 0..3 {
let p = perms & 0o7;
retval.push(if (p & 1) > 0 { 'x' } else { '-' });
retval.push(if (p & 2) > 0 { 'w' } else { '-' });
retval.push(if (p & 4) > 0 { 'r' } else { '-' });
perms >>= 3;
}
retval.push(
if perms == 32 { 'd' } else if perms == 80 { 'l' } else { '-' }
);
retval.chars().rev().collect::<String>()
}
fn unit_suffixes(size: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if size >= GB {
format!("{:.2}G", size as f64 / GB as f64)
} else if size >= MB {
format!("{:.1}M", size as f64 / MB as f64)
} else if size >= KB {
format!("{:.0}K", size as f64 / KB as f64)
} else {
format!("{}B", size)
}
}
fn longest_filename_length(entries: &[DirEntry]) -> io::Result<usize> {
entries
.iter()
.map(|entry| {
entry
.file_name()
.to_str()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 in file name")
})
.map(|name| name.len())
})
.collect::<io::Result<Vec<usize>>>()?
.into_iter()
.max()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No valid file names found"))
}
use std::path::PathBuf;
use users::{get_group_by_gid, get_user_by_uid};
use nix::sys::stat::lstat;
#[cfg(not(docsrs))]
fn get_block_size(path: &PathBuf) -> io::Result<u64> {
let stat = lstat(path)?;
let blocks_allocated = stat.st_blocks as u64;
Ok(blocks_allocated)
}
#[cfg(docsrs)]
fn get_block_size(path: &PathBuf) -> io::Result<u64> {
Ok(0)
}
fn sum_file_sizes(entries: &[fs::DirEntry], hidden: bool) -> io::Result<u64> {
let mut total_size: u64 = 0;
for entry in entries {
let path_os:PathBuf = entry.path();
if let Some(path_str) = path_os.to_str() {
if !hidden && path_str.starts_with('.') {
continue;
}
} else {
continue; }
let block_size = get_block_size(&path_os)?;
total_size += block_size;
}
Ok(total_size)
}