diff --git a/src/find/matchers/ls.rs b/src/find/matchers/ls.rs new file mode 100644 index 0000000..0b89b25 --- /dev/null +++ b/src/find/matchers/ls.rs @@ -0,0 +1,294 @@ +// This file is part of the uutils findutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use chrono::DateTime; +use std::{ + fs::File, + io::{stderr, Write}, +}; + +use super::{Matcher, MatcherIO, WalkEntry}; + +#[cfg(unix)] +fn format_permissions(mode: uucore::libc::mode_t) -> String { + let file_type = match mode & (uucore::libc::S_IFMT as uucore::libc::mode_t) { + uucore::libc::S_IFDIR => "d", + uucore::libc::S_IFREG => "-", + _ => "?", + }; + + // S_$$USR means "user permissions" + let user_perms = format!( + "{}{}{}", + if mode & uucore::libc::S_IRUSR != 0 { + "r" + } else { + "-" + }, + if mode & uucore::libc::S_IWUSR != 0 { + "w" + } else { + "-" + }, + if mode & uucore::libc::S_IXUSR != 0 { + "x" + } else { + "-" + } + ); + + // S_$$GRP means "group permissions" + let group_perms = format!( + "{}{}{}", + if mode & uucore::libc::S_IRGRP != 0 { + "r" + } else { + "-" + }, + if mode & uucore::libc::S_IWGRP != 0 { + "w" + } else { + "-" + }, + if mode & uucore::libc::S_IXGRP != 0 { + "x" + } else { + "-" + } + ); + + // S_$$OTH means "other permissions" + let other_perms = format!( + "{}{}{}", + if mode & uucore::libc::S_IROTH != 0 { + "r" + } else { + "-" + }, + if mode & uucore::libc::S_IWOTH != 0 { + "w" + } else { + "-" + }, + if mode & uucore::libc::S_IXOTH != 0 { + "x" + } else { + "-" + } + ); + + format!("{}{}{}{}", file_type, user_perms, group_perms, other_perms) +} + +#[cfg(windows)] +fn format_permissions(file_attributes: u32) -> String { + let mut attributes = Vec::new(); + + // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + if file_attributes & 0x0001 != 0 { + attributes.push("read-only"); + } + if file_attributes & 0x0002 != 0 { + attributes.push("hidden"); + } + if file_attributes & 0x0004 != 0 { + attributes.push("system"); + } + if file_attributes & 0x0020 != 0 { + attributes.push("archive"); + } + if file_attributes & 0x0040 != 0 { + attributes.push("compressed"); + } + if file_attributes & 0x0080 != 0 { + attributes.push("offline"); + } + + attributes.join(", ") +} + +pub struct Ls { + output_file: Option, +} + +impl Ls { + pub fn new(output_file: Option) -> Self { + Self { output_file } + } + + #[cfg(unix)] + fn print(&self, file_info: &WalkEntry, mut out: impl Write, print_error_message: bool) { + use nix::unistd::{Gid, Group, Uid, User}; + use std::os::unix::fs::{MetadataExt, PermissionsExt}; + + let metadata = file_info.metadata().unwrap(); + + let inode_number = metadata.ino(); + let number_of_blocks = { + let size = metadata.size(); + let number_of_blocks = size / 1024; + let remainder = number_of_blocks % 4; + + if remainder == 0 { + if number_of_blocks == 0 { + 4 + } else { + number_of_blocks + } + } else { + number_of_blocks + (4 - (remainder)) + } + }; + let permission = + { format_permissions(metadata.permissions().mode() as uucore::libc::mode_t) }; + let hard_links = metadata.nlink(); + let user = { + let uid = metadata.uid(); + User::from_uid(Uid::from_raw(uid)).unwrap().unwrap().name + }; + let group = { + let gid = metadata.gid(); + Group::from_gid(Gid::from_raw(gid)).unwrap().unwrap().name + }; + let size = metadata.size(); + let last_modified = { + let system_time = metadata.modified().unwrap(); + let now_utc: DateTime = system_time.into(); + now_utc.format("%b %e %H:%M") + }; + let path = file_info.path().to_string_lossy(); + + match writeln!( + out, + " {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}", + inode_number, + number_of_blocks, + permission, + hard_links, + user, + group, + size, + last_modified, + path, + ) { + Ok(_) => {} + Err(e) => { + if print_error_message { + writeln!( + &mut stderr(), + "Error writing {:?} for {}", + file_info.path().to_string_lossy(), + e + ) + .unwrap(); + uucore::error::set_exit_code(1); + } + } + } + } + + #[cfg(windows)] + fn print(&self, file_info: &WalkEntry, mut out: impl Write, print_error_message: bool) { + use std::os::windows::fs::MetadataExt; + + let metadata = file_info.metadata().unwrap(); + + let inode_number = 0; + let number_of_blocks = { + let size = metadata.file_size(); + let number_of_blocks = size / 1024; + let remainder = number_of_blocks % 4; + + if remainder == 0 { + if number_of_blocks == 0 { + 4 + } else { + number_of_blocks + } + } else { + number_of_blocks + (4 - (remainder)) + } + }; + let permission = { format_permissions(metadata.file_attributes()) }; + let hard_links = 0; + let user = 0; + let group = 0; + let size = metadata.file_size(); + let last_modified = { + let system_time = metadata.modified().unwrap(); + let now_utc: DateTime = system_time.into(); + now_utc.format("%b %e %H:%M") + }; + let path = file_info.path().to_string_lossy(); + + match write!( + out, + " {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}\n", + inode_number, + number_of_blocks, + permission, + hard_links, + user, + group, + size, + last_modified, + path, + ) { + Ok(_) => {} + Err(e) => { + if print_error_message { + writeln!( + &mut stderr(), + "Error writing {:?} for {}", + file_info.path().to_string_lossy(), + e + ) + .unwrap(); + uucore::error::set_exit_code(1); + } + } + } + } +} + +impl Matcher for Ls { + fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool { + if let Some(file) = &self.output_file { + self.print(file_info, file, true); + } else { + self.print( + file_info, + &mut *matcher_io.deps.get_output().borrow_mut(), + false, + ); + } + true + } + + fn has_side_effects(&self) -> bool { + true + } +} + +#[cfg(test)] +mod tests { + #[test] + #[cfg(unix)] + fn test_format_permissions() { + use super::format_permissions; + + let mode: uucore::libc::mode_t = 0o100644; + let expected = "-rw-r--r--"; + assert_eq!(format_permissions(mode), expected); + + let mode: uucore::libc::mode_t = 0o040755; + let expected = "drwxr-xr-x"; + assert_eq!(format_permissions(mode), expected); + + let mode: uucore::libc::mode_t = 0o100777; + let expected = "-rwxrwxrwx"; + assert_eq!(format_permissions(mode), expected); + } +} diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index c7a5a6a..dfe21fb 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -14,6 +14,7 @@ mod glob; mod group; mod lname; mod logical_matchers; +mod ls; mod name; mod path; mod perm; @@ -33,6 +34,7 @@ mod user; use ::regex::Regex; use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; use fs::FileSystemMatcher; +use ls::Ls; use std::fs::{File, Metadata}; use std::path::Path; use std::time::SystemTime; @@ -462,6 +464,16 @@ fn build_matcher_tree( let file = get_or_create_file(args[i])?; Some(Printer::new(PrintDelimiter::Newline, Some(file)).into_box()) } + "-ls" => Some(Ls::new(None).into_box()), + "-fls" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + + let file = get_or_create_file(args[i])?; + Some(Ls::new(Some(file)).into_box()) + } "-true" => Some(TrueMatcher.into_box()), "-false" => Some(FalseMatcher.into_box()), "-lname" | "-ilname" => { diff --git a/src/find/mod.rs b/src/find/mod.rs index 649550c..0101241 100644 --- a/src/find/mod.rs +++ b/src/find/mod.rs @@ -1429,4 +1429,13 @@ mod tests { fix_up_slashes("./test_data/links/link-d\n") ); } + + #[test] + #[cfg(unix)] + fn test_ls() { + let deps = FakeDependencies::new(); + let rc = find_main(&["find", "./test_data/simple/subdir", "-ls"], &deps); + + assert_eq!(rc, 0); + } } diff --git a/tests/find_cmd_tests.rs b/tests/find_cmd_tests.rs index bc47c7d..68a3377 100644 --- a/tests/find_cmd_tests.rs +++ b/tests/find_cmd_tests.rs @@ -983,3 +983,14 @@ fn find_follow() { .stdout(predicate::str::contains("test_data/links/link-f")) .stderr(predicate::str::is_empty()); } + +#[test] +#[serial(working_dir)] +fn find_ls() { + Command::cargo_bin("find") + .expect("found binary") + .args(["./test_data/simple/subdir", "-ls"]) + .assert() + .success() + .stderr(predicate::str::is_empty()); +}