diff --git a/Cargo.lock b/Cargo.lock index 05047386..b22f88e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -26,6 +35,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" @@ -42,6 +57,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + [[package]] name = "cc" version = "1.0.79" @@ -54,6 +75,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + [[package]] name = "clap" version = "4.1.9" @@ -100,11 +136,72 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cxx" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.10", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.10", +] + [[package]] name = "erdtree" version = "1.8.1" dependencies = [ "ansi_term", + "chrono", "clap", "clap_complete", "filesize", @@ -112,6 +209,7 @@ dependencies = [ "indextree", "indoc", "is-terminal", + "libc", "lscolors", "once_cell", "regex", @@ -201,6 +299,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "ignore" version = "0.4.20" @@ -262,6 +384,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -270,9 +401,18 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] [[package]] name = "linux-raw-sys" @@ -321,6 +461,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -444,6 +603,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + [[package]] name = "serde" version = "1.0.156" @@ -539,12 +704,29 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "utf8parse" version = "0.2.1" @@ -588,6 +770,66 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + [[package]] name = "winapi" version = "0.3.9" @@ -619,19 +861,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -640,7 +891,7 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", ] [[package]] @@ -649,13 +900,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -664,38 +930,80 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index cb5621b6..fdab8a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ path = "src/main.rs" [dependencies] ansi_term = "0.12.1" +chrono = "0.4.24" clap = { version = "4.1.1", features = ["derive"] } clap_complete = "4.1.1" filesize = "0.2.0" @@ -36,6 +37,9 @@ once_cell = "1.17.0" regex = "1.7.3" thiserror = "1.0.40" +[target.'cfg(unix)'.dependencies] +libc = "0.2.141" + [dev-dependencies] indoc = "2.0.0" strip-ansi-escapes = "0.1.1" diff --git a/src/fs/inode.rs b/src/fs/inode.rs index 4183906c..8f9ff6b9 100644 --- a/src/fs/inode.rs +++ b/src/fs/inode.rs @@ -1,7 +1,7 @@ use std::{convert::TryFrom, fs::Metadata}; /// Represents a file's underlying inode. -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Inode { pub ino: u64, pub dev: u64, diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 73e5b5c1..a266260f 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -4,6 +4,14 @@ use std::{fs, path::PathBuf}; /// Operations pertaining to underlying inodes of files. pub mod inode; +/// Unix file permissions. +#[cfg(unix)] +pub mod permissions; + +#[cfg(unix)] +/// Determining whether or not a file has extended attributes. +pub mod xattr; + /// Returns the path to the target of the soft link. Returns `None` if provided `dir_entry` isn't a /// symlink. pub fn symlink_target(dir_entry: &DirEntry) -> Option { diff --git a/src/fs/permissions/class.rs b/src/fs/permissions/class.rs new file mode 100644 index 00000000..5b1d44d1 --- /dev/null +++ b/src/fs/permissions/class.rs @@ -0,0 +1,152 @@ +use std::fmt::{self, Display}; + +/// The set of permissions for a particular class i.e. user, group, or other. +#[derive(Debug)] +#[allow(clippy::module_name_repetitions)] +pub struct ClassPermissions { + class: Class, + attr: Option, + pub(super) triad: PermissionsTriad, +} + +/// The class type that is associated with a permissions triad. +#[derive(Debug)] +pub enum Class { + User, + Group, + Other, +} + +/// Represents the special attributes that exist on the overall file corresponding to the setuid, +/// setgid, and the sticky bit. +#[derive(Debug, PartialEq, Eq)] +#[allow(clippy::upper_case_acronyms)] +pub enum Attribute { + SUID, + SGID, + Sticky, +} + +/// Read, write, execute permissions. +#[derive(Debug, PartialEq, Eq)] +pub enum PermissionsTriad { + Read, + Write, + Execute, + ReadWrite, + ReadExecute, + WriteExecute, + ReadWriteExecute, + None, +} + +/// All `permissions_mask` arguments represents the bits of `st_mode` which excludes the file-type +/// and the setuid, setgid, and sticky bit. +impl ClassPermissions { + /// Computes user permissions. + pub fn user_permissions_from(st_mode: u32) -> Self { + let read = Self::enabled(st_mode, libc::S_IRUSR); + let write = Self::enabled(st_mode, libc::S_IWUSR); + let execute = Self::enabled(st_mode, libc::S_IXUSR); + let suid = Self::enabled(st_mode, libc::S_ISUID).then_some(Attribute::SUID); + + Self::permissions_from_rwx(Class::User, read, write, execute, suid) + } + + /// Computes group permissions. + pub fn group_permissions_from(st_mode: u32) -> Self { + let read = Self::enabled(st_mode, libc::S_IRGRP); + let write = Self::enabled(st_mode, libc::S_IWGRP); + let execute = Self::enabled(st_mode, libc::S_IXGRP); + let sgid = Self::enabled(st_mode, libc::S_ISGID).then_some(Attribute::SGID); + + Self::permissions_from_rwx(Class::Group, read, write, execute, sgid) + } + + /// Computes other permissions. + pub fn other_permissions_from(st_mode: u32) -> Self { + let read = Self::enabled(st_mode, libc::S_IROTH); + let write = Self::enabled(st_mode, libc::S_IWOTH); + let execute = Self::enabled(st_mode, libc::S_IXOTH); + let sticky = Self::enabled(st_mode, libc::S_ISVTX).then_some(Attribute::Sticky); + + Self::permissions_from_rwx(Class::Other, read, write, execute, sticky) + } + + /// Checks if a particular mode (read, write, or execute) is enabled. + fn enabled(st_mode: u32, mask: N) -> bool + where + N: Copy + Into, + { + st_mode & mask.into() == mask.into() + } + + /// Returns `true` if sticky bit is enabled. + pub fn attr_is_sticky(&self) -> bool { + self.attr + .as_ref() + .map_or(false, |attr| attr == &Attribute::Sticky) + } + + /// Helper function to compute permissions. + const fn permissions_from_rwx( + class: Class, + r: bool, + w: bool, + x: bool, + attr: Option, + ) -> Self { + let triad = match (r, w, x) { + (true, false, false) => PermissionsTriad::Read, + (false, true, false) => PermissionsTriad::Write, + (false, false, true) => PermissionsTriad::Execute, + (true, true, false) => PermissionsTriad::ReadWrite, + (true, false, true) => PermissionsTriad::ReadExecute, + (false, true, true) => PermissionsTriad::WriteExecute, + (true, true, true) => PermissionsTriad::ReadWriteExecute, + (false, false, false) => PermissionsTriad::None, + }; + + Self { class, attr, triad } + } +} + +/// The symbolic representation of a [PermissionsTriad]. +impl Display for ClassPermissions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.class { + Class::Other if self.attr_is_sticky() => match self.triad { + PermissionsTriad::Read => write!(f, "r-T"), + PermissionsTriad::Write => write!(f, "-wT"), + PermissionsTriad::Execute => write!(f, "--t"), + PermissionsTriad::ReadWrite => write!(f, "rwT"), + PermissionsTriad::ReadExecute => write!(f, "r-t"), + PermissionsTriad::WriteExecute => write!(f, "-wt"), + PermissionsTriad::ReadWriteExecute => write!(f, "rwt"), + PermissionsTriad::None => write!(f, "--T"), + }, + + _ if self.attr.is_some() => match self.triad { + PermissionsTriad::Read => write!(f, "r-S"), + PermissionsTriad::Write => write!(f, "-wS"), + PermissionsTriad::Execute => write!(f, "--s"), + PermissionsTriad::ReadWrite => write!(f, "rwS"), + PermissionsTriad::ReadExecute => write!(f, "r-s"), + PermissionsTriad::WriteExecute => write!(f, "-ws"), + PermissionsTriad::ReadWriteExecute => write!(f, "rws"), + PermissionsTriad::None => write!(f, "--S"), + }, + + _ => match self.triad { + PermissionsTriad::Read => write!(f, "r--"), + PermissionsTriad::Write => write!(f, "-w-"), + PermissionsTriad::Execute => write!(f, "--x"), + PermissionsTriad::ReadWrite => write!(f, "rw-"), + PermissionsTriad::ReadExecute => write!(f, "r-x"), + PermissionsTriad::WriteExecute => write!(f, "-wx"), + PermissionsTriad::ReadWriteExecute => write!(f, "rwx"), + PermissionsTriad::None => write!(f, "---"), + }, + } + } +} diff --git a/src/fs/permissions/error.rs b/src/fs/permissions/error.rs new file mode 100644 index 00000000..075f70f2 --- /dev/null +++ b/src/fs/permissions/error.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Encountered an unknown file type.")] + UnknownFileType, +} diff --git a/src/fs/permissions/file_type.rs b/src/fs/permissions/file_type.rs new file mode 100644 index 00000000..9c409973 --- /dev/null +++ b/src/fs/permissions/file_type.rs @@ -0,0 +1,55 @@ +use super::error::Error; + +/// Unix file types. +#[derive(Debug, PartialEq, Eq)] +pub enum FileType { + Directory, + File, + Symlink, + Fifo, + Socket, + CharDevice, + BlockDevice, +} + +impl FileType { + /// Unix file identifiers that you'd find in the `ls -l` command. + pub const fn identifier(&self) -> char { + match self { + Self::Directory => 'd', + Self::File => '.', + Self::Symlink => 'l', + Self::Fifo => 'p', + Self::Socket => 's', + Self::CharDevice => 'c', + Self::BlockDevice => 'b', + } + } +} + +/// The argument `mode` is meant to come from the `mode` method of [std::fs::Permissions]. +impl TryFrom for FileType { + type Error = Error; + + fn try_from(mode: u32) -> Result { + let file_mask = mode & u32::from(libc::S_IFMT); + + if file_mask == u32::from(libc::S_IFIFO) { + Ok(Self::Fifo) + } else if file_mask == u32::from(libc::S_IFCHR) { + Ok(Self::CharDevice) + } else if file_mask == u32::from(libc::S_IFDIR) { + Ok(Self::Directory) + } else if file_mask == u32::from(libc::S_IFBLK) { + Ok(Self::BlockDevice) + } else if file_mask == u32::from(libc::S_IFREG) { + Ok(Self::File) + } else if file_mask == u32::from(libc::S_IFLNK) { + Ok(Self::Symlink) + } else if file_mask == u32::from(libc::S_IFSOCK) { + Ok(Self::Socket) + } else { + Err(Error::UnknownFileType) + } + } +} diff --git a/src/fs/permissions/mod.rs b/src/fs/permissions/mod.rs new file mode 100644 index 00000000..82814607 --- /dev/null +++ b/src/fs/permissions/mod.rs @@ -0,0 +1,126 @@ +use class::ClassPermissions; +use error::Error; +use file_type::FileType; +use std::{ + convert::TryFrom, + fmt::{self, Display, Octal}, + os::unix::fs::PermissionsExt, +}; + +/// For working with permissions for a particular class i.e. user, group, or other. +pub mod class; + +/// File permission related errors. +pub mod error; + +/// For working with Unix file identifiers. +pub mod file_type; + +#[cfg(test)] +mod test; + +impl SymbolicNotation for std::fs::Permissions {} + +/// Trait that is used to extend [std::fs::Permissions] behavior such that it allows for `mode` to +/// be expressed in Unix's symbolic notation for file permissions. +pub trait SymbolicNotation: PermissionsExt { + /// Attempts to return a [FileMode] which implements [Display] allowing it to be presented in + /// symbolic notation for file permissions. + fn try_mode_symbolic_notation(&self) -> Result { + let mode = self.mode(); + FileMode::try_from(mode) + } +} + +/// A struct which holds information about the permissions of a particular file. [FileMode] +/// implements [Display] which allows it to be conveniently presented in symbolic notation when +/// expressing file permissions. +pub struct FileMode { + pub st_mode: u32, + file_type: FileType, + user_permissions: ClassPermissions, + group_permissions: ClassPermissions, + other_permissions: ClassPermissions, +} + +impl FileMode { + /// Constructor for [FileMode]. + pub const fn new( + st_mode: u32, + file_type: FileType, + user_permissions: ClassPermissions, + group_permissions: ClassPermissions, + other_permissions: ClassPermissions, + ) -> Self { + Self { + st_mode, + file_type, + user_permissions, + group_permissions, + other_permissions, + } + } + + /// Returns a reference to `file_type`. + pub const fn file_type(&self) -> &FileType { + &self.file_type + } + + /// Returns a reference to a [ClassPermissions] which represents the permissions of the user class. + pub const fn user_permissions(&self) -> &ClassPermissions { + &self.user_permissions + } + + /// Returns a reference to a [ClassPermissions] which represents the permissions of the group class. + pub const fn group_permissions(&self) -> &ClassPermissions { + &self.group_permissions + } + + /// Returns a reference to a [ClassPermissions] which represents the permissions of the other class. + pub const fn other_permissions(&self) -> &ClassPermissions { + &self.other_permissions + } +} + +/// For representing [FileMode] in symbolic notation. +impl Display for FileMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file_iden = self.file_type().identifier(); + let user_permissions = self.user_permissions(); + let group_permissions = self.group_permissions(); + let other_permissions = self.other_permissions(); + + write!( + f, + "{file_iden}{user_permissions}{group_permissions}{other_permissions}" + ) + } +} + +/// For the octal representation of permissions +impl Octal for FileMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let modes_mask = self.st_mode & !u32::from(libc::S_IFMT); + fmt::Octal::fmt(&modes_mask, f) + } +} + +/// The argument `st_mode` is meant to come from the `mode` method of [std::fs::Permissions]. +impl TryFrom for FileMode { + type Error = Error; + + fn try_from(st_mode: u32) -> Result { + let file_type = FileType::try_from(st_mode)?; + let user_permissions = ClassPermissions::user_permissions_from(st_mode); + let group_permissions = ClassPermissions::group_permissions_from(st_mode); + let other_permissions = ClassPermissions::other_permissions_from(st_mode); + + Ok(Self::new( + st_mode, + file_type, + user_permissions, + group_permissions, + other_permissions, + )) + } +} diff --git a/src/fs/permissions/test.rs b/src/fs/permissions/test.rs new file mode 100644 index 00000000..5e2b4b9a --- /dev/null +++ b/src/fs/permissions/test.rs @@ -0,0 +1,96 @@ +use super::{class::PermissionsTriad, file_type::FileType, SymbolicNotation}; +use std::{error::Error, fs::File, os::unix::fs::PermissionsExt}; + +#[test] +fn test_symbolic_notation() -> Result<(), Box> { + let temp = std::env::temp_dir().join("yogsothoth.hpl"); + + // File is created with read + write for user and read-only for all others. + let file = File::create(temp)?; + let metadata = file.metadata()?; + + let permissions = metadata.permissions(); + + let file_mode = permissions.try_mode_symbolic_notation()?; + + let file_type = file_mode.file_type(); + let user = file_mode.user_permissions(); + let group = file_mode.group_permissions(); + let other = file_mode.other_permissions(); + + assert_eq!(file_type, &FileType::File); + assert_eq!(&user.triad, &PermissionsTriad::ReadWrite); + assert_eq!(&group.triad, &PermissionsTriad::Read); + assert_eq!(&other.triad, &PermissionsTriad::Read); + + let rwx = format!("{file_mode}"); + assert_eq!(rwx, ".rw-r--r--"); + + let octal = format!("{:o}", file_mode); + assert_eq!(octal, "644"); + + Ok(()) +} + +#[test] +fn test_symbolic_notation_special_attr() -> Result<(), Box> { + let temp = std::env::temp_dir().join("sub-niggurath.hpl"); + + // File is created with read + write for user and read-only for all others. + let file = File::create(temp)?; + + let metadata = file.metadata()?; + let mut permissions = metadata.permissions(); + + // Set the sticky bit + permissions.set_mode(0o101644); + + let file_mode = permissions.try_mode_symbolic_notation()?; + let rwx = format!("{file_mode}"); + assert_eq!(rwx, ".rw-r--r-T"); + + let octal = format!("{:o}", file_mode); + assert_eq!(octal, "1644"); + + // Set the getuid bit + permissions.set_mode(0o102644); + + let file_mode = permissions.try_mode_symbolic_notation()?; + let rwx = format!("{file_mode}"); + assert_eq!(rwx, ".rw-r-Sr--"); + + let octal = format!("{:o}", file_mode); + assert_eq!(octal, "2644"); + + // Set the setuid bit + permissions.set_mode(0o104644); + + let file_mode = permissions.try_mode_symbolic_notation()?; + let rwx = format!("{file_mode}"); + assert_eq!(rwx, ".rwSr--r--"); + + let octal = format!("{:o}", file_mode); + assert_eq!(octal, "4644"); + + // Set the all the attr bits + permissions.set_mode(0o107644); + + let file_mode = permissions.try_mode_symbolic_notation()?; + let rwx = format!("{file_mode}"); + assert_eq!(rwx, ".rwSr-Sr-T"); + + let octal = format!("{:o}", file_mode); + assert_eq!(octal, "7644"); + + // Set all the attr bits and give all classes execute permissions + permissions.set_mode(0o107777); + + let file_mode = permissions.try_mode_symbolic_notation()?; + let rwx = format!("{file_mode}"); + assert_eq!(rwx, ".rwsrwsrwt"); + + let octal = format!("{:o}", file_mode); + assert_eq!(octal, "7777"); + + Ok(()) +} diff --git a/src/fs/xattr.rs b/src/fs/xattr.rs new file mode 100644 index 00000000..43ff2ee7 --- /dev/null +++ b/src/fs/xattr.rs @@ -0,0 +1,37 @@ +use ignore::DirEntry; +use std::{os::unix::ffi::OsStrExt, path::Path, ptr}; + +/// Allow extended attributes to be queried directly from the directory entry. +impl ExtendedAttr for DirEntry { + fn path(&self) -> &Path { + self.path() + } +} + +/// Simple trait that allows files to query extended attributes if it exists. +pub trait ExtendedAttr { + fn path(&self) -> &Path; + + /// Queries the filesystem to check if there exists extended attributes for the implementor's + /// path. + fn has_xattrs(&self) -> bool { + unsafe { has_xattrs(self.path()) } + } +} + +/// Checks to see if a directory entry referred to by `path` has extended attributes. +unsafe fn has_xattrs(path: &Path) -> bool { + use libc::{c_char, listxattr}; + + let path_ptr = { + let slice = path.as_os_str().as_bytes(); + let slice_ptr = slice.as_ptr(); + slice_ptr.cast::() + }; + + #[cfg(target_os = "linux")] + return 0 < listxattr(path_ptr, ptr::null_mut::(), 0); + + #[cfg(target_os = "macos")] + return 0 < listxattr(path_ptr, ptr::null_mut::(), 0, 0); +} diff --git a/src/icons.rs b/src/icons.rs index b5980f8e..f8648f3a 100644 --- a/src/icons.rs +++ b/src/icons.rs @@ -1,20 +1,114 @@ use crate::hash; -use ansi_term::Color; +use ansi_term::Style; +use ansi_term::{ANSIGenericString, Color}; +use ignore::DirEntry; use once_cell::sync::Lazy; use std::{ + borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, fs::FileType, + path::Path, }; +/// Computes a plain, colorless icon with given parameters. +/// +/// The precedent from highest to lowest in terms of which parameters determine the icon used +/// is as followed: file-type, file-extension, and then file-name. If an icon cannot be +/// computed the fall-back default icon is used. +/// +/// If a directory entry is a link and the link target is provided, the link target will be +/// used to determine the icon. +pub fn compute(entry: &DirEntry, link_target: Option<&Path>) -> Cow<'static, str> { + let icon = entry + .file_type() + .and_then(icon_from_file_type) + .map(Cow::from); + + if let Some(i) = icon { + return i; + } + + let ext = match link_target { + Some(target) if entry.path_is_symlink() => target.extension(), + _ => entry.path().extension(), + }; + + let icon = ext.and_then(icon_from_ext).map(|(_, i)| Cow::from(i)); + + if let Some(i) = icon { + return i; + } + + let icon = icon_from_file_name(entry.file_name()).map(Cow::from); + + if let Some(i) = icon { + return i; + } + + Cow::from(get_default_icon().1) +} + +/// Computes a plain, colored icon with given parameters. See [compute] for more details. +pub fn compute_with_color( + entry: &DirEntry, + link_target: Option<&Path>, + style: Option