diff --git a/src/fs.rs b/src/fs.rs index a495085..0b33896 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,12 +1,24 @@ use std::fs; #[cfg(any(unix, target_os = "redox"))] -pub fn is_executable(md: &fs::Metadata) -> bool { - use std::os::unix::fs::PermissionsExt; - md.permissions().mode() & 0o111 != 0 +use std::os::unix::fs::MetadataExt; + +/// Get the UNIX-style mode bits from some metadata if available, otherwise 0. +#[allow(unused_variables)] +pub fn mode(md: &fs::Metadata) -> u32 { + #[cfg(any(unix, target_os = "redox"))] + return md.mode(); + + #[cfg(not(any(unix, target_os = "redox")))] + return 0; } -#[cfg(any(windows, target_os = "wasi"))] -pub fn is_executable(_: &fs::Metadata) -> bool { - false +/// Get the number of hard links to a file, or 1 if unknown. +#[allow(unused_variables)] +pub fn nlink(md: &fs::Metadata) -> u64 { + #[cfg(any(unix, target_os = "redox"))] + return md.nlink(); + + #[cfg(not(any(unix, target_os = "redox")))] + return 1; } diff --git a/src/lib.rs b/src/lib.rs index 06d0641..2ff199e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,12 +230,17 @@ impl LsColors { let parts: Vec<_> = entry.split('=').collect(); if let Some([entry, ansi_style]) = parts.get(0..2) { - if let Some(style) = Style::from_ansi_sequence(ansi_style) { - if entry.starts_with('*') { + let style = Style::from_ansi_sequence(ansi_style); + if let Some(suffix) = entry.strip_prefix('*') { + if let Some(style) = style { self.suffix_mapping - .push((entry[1..].to_string().to_ascii_lowercase(), style)); - } else if let Some(indicator) = Indicator::from(entry) { + .push((suffix.to_string().to_ascii_lowercase(), style)); + } + } else if let Some(indicator) = Indicator::from(entry) { + if let Some(style) = style { self.indicator_mapping.insert(indicator, style); + } else { + self.indicator_mapping.remove(&indicator); } } } @@ -251,68 +256,107 @@ impl LsColors { self.style_for_path_with_metadata(path, metadata.as_ref()) } - /// Get the ANSI style for a path, given the corresponding `Metadata` struct. - /// - /// *Note:* The `Metadata` struct must have been acquired via `Path::symlink_metadata` in - /// order to colorize symbolic links correctly. - pub fn style_for_path_with_metadata>( - &self, - path: P, - metadata: Option<&std::fs::Metadata>, - ) -> Option<&Style> { - if let Some(metadata) = metadata { - if metadata.is_dir() { - return self.style_for_indicator(Indicator::Directory); - } + /// Check if an indicator has an associated color. + fn has_color_for(&self, indicator: Indicator) -> bool { + self.indicator_mapping.contains_key(&indicator) + } - if metadata.file_type().is_symlink() { - // This works because `Path::exists` traverses symlinks. - if path.as_ref().exists() { - return self.style_for_indicator(Indicator::SymbolicLink); + /// Get the indicator type for a path with corresponding metadata. + fn indicator_for(&self, path: &Path, metadata: Option<&std::fs::Metadata>) -> Indicator { + if let Some(metadata) = metadata { + let file_type = metadata.file_type(); + + if file_type.is_file() { + let mode = crate::fs::mode(metadata); + let nlink = crate::fs::nlink(metadata); + + if self.has_color_for(Indicator::Setuid) && mode & 0o4000 != 0 { + Indicator::Setuid + } else if self.has_color_for(Indicator::Setgid) && mode & 0o2000 != 0 { + Indicator::Setgid + } else if self.has_color_for(Indicator::ExecutableFile) && mode & 0o0111 != 0 { + Indicator::ExecutableFile + } else if self.has_color_for(Indicator::MultipleHardLinks) && nlink > 1 { + Indicator::MultipleHardLinks } else { - return self.style_for_indicator(Indicator::OrphanedSymbolicLink); - } - } - - #[cfg(unix)] - { - use std::os::unix::fs::FileTypeExt; - - let filetype = metadata.file_type(); - if filetype.is_fifo() { - return self.style_for_indicator(Indicator::FIFO); + Indicator::RegularFile } - if filetype.is_socket() { - return self.style_for_indicator(Indicator::Socket); + } else if file_type.is_dir() { + let mode = crate::fs::mode(metadata); + + if self.has_color_for(Indicator::StickyAndOtherWritable) && mode & 0o1002 == 0o1002 + { + Indicator::StickyAndOtherWritable + } else if self.has_color_for(Indicator::OtherWritable) && mode & 0o0002 != 0 { + Indicator::OtherWritable + } else if self.has_color_for(Indicator::Sticky) && mode & 0o1000 != 0 { + Indicator::Sticky + } else { + Indicator::Directory } - if filetype.is_block_device() { - return self.style_for_indicator(Indicator::BlockDevice); + } else if file_type.is_symlink() { + // This works because `Path::exists` traverses symlinks. + if self.has_color_for(Indicator::OrphanedSymbolicLink) && !path.exists() { + return Indicator::OrphanedSymbolicLink; } - if filetype.is_char_device() { - return self.style_for_indicator(Indicator::CharacterDevice); + + Indicator::SymbolicLink + } else { + #[cfg(unix)] + { + use std::os::unix::fs::FileTypeExt; + + if file_type.is_fifo() { + return Indicator::FIFO; + } + if file_type.is_socket() { + return Indicator::Socket; + } + if file_type.is_block_device() { + return Indicator::BlockDevice; + } + if file_type.is_char_device() { + return Indicator::CharacterDevice; + } } - } - if crate::fs::is_executable(&metadata) { - return self.style_for_indicator(Indicator::ExecutableFile); + // Treat files of unknown type as errors + Indicator::MissingFile } + } else { + // Default to a regular file, so we still try the suffix map when no metadata is available + Indicator::RegularFile } + } - // Note: using '.to_str()' here means that filename - // matching will not work with invalid-UTF-8 paths. - let filename = path.as_ref().file_name()?.to_str()?.to_ascii_lowercase(); - - // We need to traverse LS_COLORS from back to front - // to be consistent with `ls`: - for (suffix, style) in self.suffix_mapping.iter().rev() { - // Note: For some reason, 'ends_with' is much - // slower if we omit `.as_str()` here: - if filename.ends_with(suffix.as_str()) { - return Some(style); + /// Get the ANSI style for a path, given the corresponding `Metadata` struct. + /// + /// *Note:* The `Metadata` struct must have been acquired via `Path::symlink_metadata` in + /// order to colorize symbolic links correctly. + pub fn style_for_path_with_metadata>( + &self, + path: P, + metadata: Option<&std::fs::Metadata>, + ) -> Option<&Style> { + let indicator = self.indicator_for(path.as_ref(), metadata); + + if indicator == Indicator::RegularFile { + // Note: using '.to_str()' here means that filename + // matching will not work with invalid-UTF-8 paths. + let filename = path.as_ref().file_name()?.to_str()?.to_ascii_lowercase(); + + // We need to traverse LS_COLORS from back to front + // to be consistent with `ls`: + for (suffix, style) in self.suffix_mapping.iter().rev() { + // Note: For some reason, 'ends_with' is much + // slower if we omit `.as_str()` here: + if filename.ends_with(suffix.as_str()) { + return Some(style); + } } } - None + self.style_for_indicator(indicator) } /// Get ANSI styles for each component of a given path. Components already include the path @@ -332,13 +376,27 @@ impl LsColors { /// For example, the style for `mi` (missing file) falls back to `or` (orphaned symbolic link) /// if it has not been specified explicitly. pub fn style_for_indicator(&self, indicator: Indicator) -> Option<&Style> { - match indicator { - Indicator::MissingFile => self - .indicator_mapping - .get(&Indicator::MissingFile) - .or_else(|| self.indicator_mapping.get(&Indicator::OrphanedSymbolicLink)), - _ => self.indicator_mapping.get(&indicator), - } + self.indicator_mapping + .get(&indicator) + .or_else(|| { + self.indicator_mapping.get(&match indicator { + Indicator::Setuid + | Indicator::Setgid + | Indicator::ExecutableFile + | Indicator::MultipleHardLinks => Indicator::RegularFile, + + Indicator::StickyAndOtherWritable + | Indicator::OtherWritable + | Indicator::Sticky => Indicator::Directory, + + Indicator::OrphanedSymbolicLink => Indicator::SymbolicLink, + + Indicator::MissingFile => Indicator::OrphanedSymbolicLink, + + _ => indicator, + }) + }) + .or_else(|| self.indicator_mapping.get(&Indicator::Normal)) } } @@ -507,6 +565,63 @@ mod tests { assert_eq!(Some(Color::Yellow), style_missing.foreground); } + #[cfg(unix)] + #[test] + fn style_for_setid() { + use std::fs::{set_permissions, Permissions}; + use std::os::unix::fs::PermissionsExt; + + let tmp_dir = temp_dir(); + let tmp_file = create_file(tmp_dir.path().join("setid")); + let perms = Permissions::from_mode(0o6750); + set_permissions(&tmp_file, perms).unwrap(); + + let suid_style = get_default_style(&tmp_file).unwrap(); + assert_eq!(Some(Color::Red), suid_style.background); + + let lscolors = LsColors::from_string("su=0"); + let sgid_style = lscolors.style_for_path(&tmp_file).unwrap(); + assert_eq!(Some(Color::Yellow), sgid_style.background); + } + + #[cfg(unix)] + #[test] + fn style_for_multi_hard_links() { + let tmp_dir = temp_dir(); + let tmp_file = create_file(tmp_dir.path().join("file1")); + std::fs::hard_link(&tmp_file, tmp_dir.path().join("file2")).unwrap(); + + let lscolors = LsColors::from_string("mh=35"); + let style = lscolors.style_for_path(&tmp_file).unwrap(); + assert_eq!(Some(Color::Magenta), style.foreground); + } + + #[cfg(unix)] + #[test] + fn style_for_sticky_other_writable() { + use std::fs::{set_permissions, Permissions}; + use std::os::unix::fs::PermissionsExt; + + let tmp_root = temp_dir(); + let tmp_dir = create_dir(tmp_root.path().join("test-dir")); + let perms = Permissions::from_mode(0o1777); + set_permissions(&tmp_dir, perms).unwrap(); + + let so_style = get_default_style(&tmp_dir).unwrap(); + assert_eq!(Some(Color::Black), so_style.foreground); + assert_eq!(Some(Color::Green), so_style.background); + + let lscolors1 = LsColors::from_string("tw=0"); + let ow_style = lscolors1.style_for_path(&tmp_dir).unwrap(); + assert_eq!(Some(Color::Blue), ow_style.foreground); + assert_eq!(Some(Color::Green), ow_style.background); + + let lscolors2 = LsColors::from_string("tw=0:ow=0"); + let st_style = lscolors2.style_for_path(&tmp_dir).unwrap(); + assert_eq!(Some(Color::White), st_style.foreground); + assert_eq!(Some(Color::Blue), st_style.background); + } + #[test] fn style_for_path_components() { use std::ffi::OsString;