Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for more indicators #35

Merged
merged 2 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions src/fs.rs
Original file line number Diff line number Diff line change
@@ -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;
}
235 changes: 175 additions & 60 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand All @@ -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<P: AsRef<Path>>(
&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<P: AsRef<Path>>(
&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
Expand All @@ -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))
}
}

Expand Down Expand Up @@ -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;
Expand Down