Skip to content

Commit dde45bf

Browse files
tavianatorsharkdp
authored andcommitted
Add support for more indicators
Partially addresses #6.
1 parent e441c13 commit dde45bf

File tree

2 files changed

+184
-62
lines changed

2 files changed

+184
-62
lines changed

src/fs.rs

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
use std::fs;
22

33
#[cfg(any(unix, target_os = "redox"))]
4-
pub fn is_executable(md: &fs::Metadata) -> bool {
5-
use std::os::unix::fs::PermissionsExt;
6-
md.permissions().mode() & 0o111 != 0
4+
use std::os::unix::fs::MetadataExt;
5+
6+
/// Get the UNIX-style mode bits from some metadata if available, otherwise 0.
7+
#[allow(unused_variables)]
8+
pub fn mode(md: &fs::Metadata) -> u32 {
9+
#[cfg(any(unix, target_os = "redox"))]
10+
return md.mode();
11+
12+
#[cfg(not(any(unix, target_os = "redox")))]
13+
return 0;
714
}
815

9-
#[cfg(any(windows, target_os = "wasi"))]
10-
pub fn is_executable(_: &fs::Metadata) -> bool {
11-
false
16+
/// Get the number of hard links to a file, or 1 if unknown.
17+
#[allow(unused_variables)]
18+
pub fn nlink(md: &fs::Metadata) -> u64 {
19+
#[cfg(any(unix, target_os = "redox"))]
20+
return md.nlink();
21+
22+
#[cfg(not(any(unix, target_os = "redox")))]
23+
return 1;
1224
}

src/lib.rs

+166-56
Original file line numberDiff line numberDiff line change
@@ -256,68 +256,107 @@ impl LsColors {
256256
self.style_for_path_with_metadata(path, metadata.as_ref())
257257
}
258258

259-
/// Get the ANSI style for a path, given the corresponding `Metadata` struct.
260-
///
261-
/// *Note:* The `Metadata` struct must have been acquired via `Path::symlink_metadata` in
262-
/// order to colorize symbolic links correctly.
263-
pub fn style_for_path_with_metadata<P: AsRef<Path>>(
264-
&self,
265-
path: P,
266-
metadata: Option<&std::fs::Metadata>,
267-
) -> Option<&Style> {
268-
if let Some(metadata) = metadata {
269-
if metadata.is_dir() {
270-
return self.style_for_indicator(Indicator::Directory);
271-
}
259+
/// Check if an indicator has an associated color.
260+
fn has_color_for(&self, indicator: Indicator) -> bool {
261+
self.indicator_mapping.contains_key(&indicator)
262+
}
272263

273-
if metadata.file_type().is_symlink() {
274-
// This works because `Path::exists` traverses symlinks.
275-
if path.as_ref().exists() {
276-
return self.style_for_indicator(Indicator::SymbolicLink);
264+
/// Get the indicator type for a path with corresponding metadata.
265+
fn indicator_for(&self, path: &Path, metadata: Option<&std::fs::Metadata>) -> Indicator {
266+
if let Some(metadata) = metadata {
267+
let file_type = metadata.file_type();
268+
269+
if file_type.is_file() {
270+
let mode = crate::fs::mode(metadata);
271+
let nlink = crate::fs::nlink(metadata);
272+
273+
if self.has_color_for(Indicator::Setuid) && mode & 0o4000 != 0 {
274+
Indicator::Setuid
275+
} else if self.has_color_for(Indicator::Setgid) && mode & 0o2000 != 0 {
276+
Indicator::Setgid
277+
} else if self.has_color_for(Indicator::ExecutableFile) && mode & 0o0111 != 0 {
278+
Indicator::ExecutableFile
279+
} else if self.has_color_for(Indicator::MultipleHardLinks) && nlink > 1 {
280+
Indicator::MultipleHardLinks
277281
} else {
278-
return self.style_for_indicator(Indicator::OrphanedSymbolicLink);
282+
Indicator::RegularFile
279283
}
280-
}
281-
282-
#[cfg(unix)]
283-
{
284-
use std::os::unix::fs::FileTypeExt;
285-
286-
let filetype = metadata.file_type();
287-
if filetype.is_fifo() {
288-
return self.style_for_indicator(Indicator::FIFO);
289-
}
290-
if filetype.is_socket() {
291-
return self.style_for_indicator(Indicator::Socket);
284+
} else if file_type.is_dir() {
285+
let mode = crate::fs::mode(metadata);
286+
287+
if self.has_color_for(Indicator::StickyAndOtherWritable) && mode & 0o1002 == 0o1002
288+
{
289+
Indicator::StickyAndOtherWritable
290+
} else if self.has_color_for(Indicator::OtherWritable) && mode & 0o0002 != 0 {
291+
Indicator::OtherWritable
292+
} else if self.has_color_for(Indicator::Sticky) && mode & 0o1000 != 0 {
293+
Indicator::Sticky
294+
} else {
295+
Indicator::Directory
292296
}
293-
if filetype.is_block_device() {
294-
return self.style_for_indicator(Indicator::BlockDevice);
297+
} else if file_type.is_symlink() {
298+
// This works because `Path::exists` traverses symlinks.
299+
if self.has_color_for(Indicator::OrphanedSymbolicLink) && !path.exists() {
300+
return Indicator::OrphanedSymbolicLink;
295301
}
296-
if filetype.is_char_device() {
297-
return self.style_for_indicator(Indicator::CharacterDevice);
302+
303+
Indicator::SymbolicLink
304+
} else {
305+
#[cfg(unix)]
306+
{
307+
use std::os::unix::fs::FileTypeExt;
308+
309+
if file_type.is_fifo() {
310+
return Indicator::FIFO;
311+
}
312+
if file_type.is_socket() {
313+
return Indicator::Socket;
314+
}
315+
if file_type.is_block_device() {
316+
return Indicator::BlockDevice;
317+
}
318+
if file_type.is_char_device() {
319+
return Indicator::CharacterDevice;
320+
}
298321
}
299-
}
300322

301-
if crate::fs::is_executable(&metadata) {
302-
return self.style_for_indicator(Indicator::ExecutableFile);
323+
// Treat files of unknown type as errors
324+
Indicator::MissingFile
303325
}
326+
} else {
327+
// Default to a regular file, so we still try the suffix map when no metadata is available
328+
Indicator::RegularFile
304329
}
330+
}
305331

306-
// Note: using '.to_str()' here means that filename
307-
// matching will not work with invalid-UTF-8 paths.
308-
let filename = path.as_ref().file_name()?.to_str()?.to_ascii_lowercase();
309-
310-
// We need to traverse LS_COLORS from back to front
311-
// to be consistent with `ls`:
312-
for (suffix, style) in self.suffix_mapping.iter().rev() {
313-
// Note: For some reason, 'ends_with' is much
314-
// slower if we omit `.as_str()` here:
315-
if filename.ends_with(suffix.as_str()) {
316-
return Some(style);
332+
/// Get the ANSI style for a path, given the corresponding `Metadata` struct.
333+
///
334+
/// *Note:* The `Metadata` struct must have been acquired via `Path::symlink_metadata` in
335+
/// order to colorize symbolic links correctly.
336+
pub fn style_for_path_with_metadata<P: AsRef<Path>>(
337+
&self,
338+
path: P,
339+
metadata: Option<&std::fs::Metadata>,
340+
) -> Option<&Style> {
341+
let indicator = self.indicator_for(path.as_ref(), metadata);
342+
343+
if indicator == Indicator::RegularFile {
344+
// Note: using '.to_str()' here means that filename
345+
// matching will not work with invalid-UTF-8 paths.
346+
let filename = path.as_ref().file_name()?.to_str()?.to_ascii_lowercase();
347+
348+
// We need to traverse LS_COLORS from back to front
349+
// to be consistent with `ls`:
350+
for (suffix, style) in self.suffix_mapping.iter().rev() {
351+
// Note: For some reason, 'ends_with' is much
352+
// slower if we omit `.as_str()` here:
353+
if filename.ends_with(suffix.as_str()) {
354+
return Some(style);
355+
}
317356
}
318357
}
319358

320-
None
359+
self.style_for_indicator(indicator)
321360
}
322361

323362
/// Get ANSI styles for each component of a given path. Components already include the path
@@ -337,13 +376,27 @@ impl LsColors {
337376
/// For example, the style for `mi` (missing file) falls back to `or` (orphaned symbolic link)
338377
/// if it has not been specified explicitly.
339378
pub fn style_for_indicator(&self, indicator: Indicator) -> Option<&Style> {
340-
match indicator {
341-
Indicator::MissingFile => self
342-
.indicator_mapping
343-
.get(&Indicator::MissingFile)
344-
.or_else(|| self.indicator_mapping.get(&Indicator::OrphanedSymbolicLink)),
345-
_ => self.indicator_mapping.get(&indicator),
346-
}
379+
self.indicator_mapping
380+
.get(&indicator)
381+
.or_else(|| {
382+
self.indicator_mapping.get(&match indicator {
383+
Indicator::Setuid
384+
| Indicator::Setgid
385+
| Indicator::ExecutableFile
386+
| Indicator::MultipleHardLinks => Indicator::RegularFile,
387+
388+
Indicator::StickyAndOtherWritable
389+
| Indicator::OtherWritable
390+
| Indicator::Sticky => Indicator::Directory,
391+
392+
Indicator::OrphanedSymbolicLink => Indicator::SymbolicLink,
393+
394+
Indicator::MissingFile => Indicator::OrphanedSymbolicLink,
395+
396+
_ => indicator,
397+
})
398+
})
399+
.or_else(|| self.indicator_mapping.get(&Indicator::Normal))
347400
}
348401
}
349402

@@ -512,6 +565,63 @@ mod tests {
512565
assert_eq!(Some(Color::Yellow), style_missing.foreground);
513566
}
514567

568+
#[cfg(unix)]
569+
#[test]
570+
fn style_for_setid() {
571+
use std::fs::{set_permissions, Permissions};
572+
use std::os::unix::fs::PermissionsExt;
573+
574+
let tmp_dir = temp_dir();
575+
let tmp_file = create_file(tmp_dir.path().join("setid"));
576+
let perms = Permissions::from_mode(0o6750);
577+
set_permissions(&tmp_file, perms).unwrap();
578+
579+
let suid_style = get_default_style(&tmp_file).unwrap();
580+
assert_eq!(Some(Color::Red), suid_style.background);
581+
582+
let lscolors = LsColors::from_string("su=0");
583+
let sgid_style = lscolors.style_for_path(&tmp_file).unwrap();
584+
assert_eq!(Some(Color::Yellow), sgid_style.background);
585+
}
586+
587+
#[cfg(unix)]
588+
#[test]
589+
fn style_for_multi_hard_links() {
590+
let tmp_dir = temp_dir();
591+
let tmp_file = create_file(tmp_dir.path().join("file1"));
592+
std::fs::hard_link(&tmp_file, tmp_dir.path().join("file2")).unwrap();
593+
594+
let lscolors = LsColors::from_string("mh=35");
595+
let style = lscolors.style_for_path(&tmp_file).unwrap();
596+
assert_eq!(Some(Color::Magenta), style.foreground);
597+
}
598+
599+
#[cfg(unix)]
600+
#[test]
601+
fn style_for_sticky_other_writable() {
602+
use std::fs::{set_permissions, Permissions};
603+
use std::os::unix::fs::PermissionsExt;
604+
605+
let tmp_root = temp_dir();
606+
let tmp_dir = create_dir(tmp_root.path().join("test-dir"));
607+
let perms = Permissions::from_mode(0o1777);
608+
set_permissions(&tmp_dir, perms).unwrap();
609+
610+
let so_style = get_default_style(&tmp_dir).unwrap();
611+
assert_eq!(Some(Color::Black), so_style.foreground);
612+
assert_eq!(Some(Color::Green), so_style.background);
613+
614+
let lscolors1 = LsColors::from_string("tw=0");
615+
let ow_style = lscolors1.style_for_path(&tmp_dir).unwrap();
616+
assert_eq!(Some(Color::Blue), ow_style.foreground);
617+
assert_eq!(Some(Color::Green), ow_style.background);
618+
619+
let lscolors2 = LsColors::from_string("tw=0:ow=0");
620+
let st_style = lscolors2.style_for_path(&tmp_dir).unwrap();
621+
assert_eq!(Some(Color::White), st_style.foreground);
622+
assert_eq!(Some(Color::Blue), st_style.background);
623+
}
624+
515625
#[test]
516626
fn style_for_path_components() {
517627
use std::ffi::OsString;

0 commit comments

Comments
 (0)