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

gnu compatiblity fix for case sensitivity handling of suffixes #86

Closed
wants to merge 4 commits into from
Closed
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
199 changes: 177 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ impl Indicator {
}

type FileNameSuffix = String;
type Priority = usize;

/// Iterator over the path components with their respective style.
pub struct StyledComponents<'a> {
Expand Down Expand Up @@ -220,6 +221,29 @@ impl Colorable for DirEntry {

const LS_COLORS_DEFAULT: &str = "rs=0:lc=\x1b[:rc=m:cl=\x1b[K:ex=01;32:sg=30;43:su=37;41:di=01;34:st=37;44:ow=34;42:tw=30;42:ln=01;36:bd=01;33:cd=01;33:do=01;35:pi=33:so=01;35:";

#[derive(Default, Debug, Clone)]
struct SuffixMappingEntry {
/// Keys are non-normalized suffixes(variants of the suffix), and values are
/// tuples containing an style and their priority.
/// We need to store the priority (the index of the entry in the env) here
/// because ls has an overriding mechanism. For example,
/// *.gz=01;33:*.tar.gz=01;31: and *.tar.gz=01;31:*.gz=01;33: are not the
/// same.
variants: HashMap<FileNameSuffix, (Option<Style>, Priority)>,
// whether the variants are case sensitive or not
case_sensitive: bool,
}

impl SuffixMappingEntry {
fn init_case_sensitivity(&mut self) {
// we treat an entry as case-insensitive when all the styles are equal
let mut iter = self.variants.values();
// it's okay to unwrap here because there must be atleast element present
let (ref_style, _) = iter.next().unwrap();
self.case_sensitive = iter.any(|(style, _)| style != ref_style);
}
}

/// Holds information about how different file system entries should be colorized / styled.
#[derive(Debug, Clone)]
pub struct LsColors {
Expand All @@ -228,10 +252,7 @@ pub struct LsColors {
/// Whether Indicator::RegularFile falls back to Indicator::Normal
/// (see <https://github.com/sharkdp/lscolors/issues/48#issuecomment-1582830387>)
file_normal_fallback: bool,

// Note: you might expect to see a `HashMap` for `suffix_mapping` as well, but we need to
// preserve the exact order of the mapping in order to be consistent with `ls`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to this? The new implementation uses only a HashMap so I don't see how it can distinguish between *.gz=01;33:*.tar.gz=01;31: and *.tar.gz=01;31:*.gz=01;33:, which should behave differently. (Perhaps we are missing a test if this wasn't caught.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was looking for the longest match, that's my bad, I'll fix it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I fixed this, fixed the test also

suffix_mapping: Vec<(FileNameSuffix, Option<Style>)>,
suffix_mapping: HashMap<FileNameSuffix, SuffixMappingEntry>,
}

impl Default for LsColors {
Expand All @@ -250,7 +271,7 @@ impl LsColors {
LsColors {
indicator_mapping: HashMap::new(),
file_normal_fallback: true,
suffix_mapping: vec![],
suffix_mapping: HashMap::new(),
}
}

Expand All @@ -272,14 +293,12 @@ impl LsColors {
}

fn add_from_string(&mut self, input: &str) {
for entry in input.split(':') {
for (idx, entry) in input.split(':').enumerate() {
let parts: Vec<_> = entry.split('=').collect();

if let Some([entry, ansi_style]) = parts.get(0..2) {
let style = Style::from_ansi_sequence(ansi_style);
if let Some(suffix) = entry.strip_prefix('*') {
self.suffix_mapping
.push((suffix.to_string().to_ascii_lowercase(), style));
self.add_suffix_entry(suffix.to_string(), style, idx);
} else if let Some(indicator) = Indicator::from(entry) {
if let Some(style) = style {
self.indicator_mapping.insert(indicator, style);
Expand All @@ -292,6 +311,23 @@ impl LsColors {
}
}
}
self.suffix_mapping
.values_mut()
.for_each(|entry| entry.init_case_sensitivity());
}

fn add_suffix_entry(
&mut self,
suffix: FileNameSuffix,
style: Option<Style>,
priority: Priority,
) {
// We use `to_ascii_lowercase` here to normalize suffix to use as keys.
let suffix_normalized = suffix.to_ascii_lowercase();
let suffix_mapping_entry = self.suffix_mapping.entry(suffix_normalized).or_default();
suffix_mapping_entry
.variants
.insert(suffix, (style, priority));
}

/// Get the ANSI style for a given path.
Expand Down Expand Up @@ -421,21 +457,44 @@ impl LsColors {

/// Get the ANSI style for a string. This does not have to be a valid filepath.
pub fn style_for_str(&self, file_str: &str) -> Option<&Style> {
// make input lowercase for compatibility reasons
let input = file_str.to_ascii_lowercase();
let input_ref = input.as_str();

// 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 input_ref.ends_with(suffix.as_str()) {
return style.as_ref();
let file_str_len = file_str.len();
let file_str_normalized = file_str.to_ascii_lowercase();
let mut last_matched_style: Option<&Style> = None;
let mut last_matched_priority = 0;
for (suffix_normalized, suffix_mapping_entry) in self.suffix_mapping.iter() {
// check if there is an entry for the suffix
if file_str_normalized.ends_with(suffix_normalized) {
// check whether the entry is marked as case sensitive or not
if suffix_mapping_entry.case_sensitive {
// then we need to check whether there is a suffix that exactly
// matches in different variants.
// we could do that by finding the suffix variant that appears in the filename,
let file_str_suffix = &file_str[file_str_len - suffix_normalized.len()..];
// and use that to find the corresponding style.
if let Some((style, priority)) =
suffix_mapping_entry.variants.get(file_str_suffix)
{
if *priority >= last_matched_priority {
last_matched_style = style.as_ref();
last_matched_priority = *priority;
}
}
} else {
// case doesn't matter, but we need select the one with the biggest priority.
let (style, priority) = suffix_mapping_entry
.variants
.values()
.max_by(|x, y| x.1.cmp(&y.1))
// it okay to unwrap here, because there should exist atleast one variant.
.unwrap();
if *priority >= last_matched_priority {
last_matched_style = style.as_ref();
last_matched_priority = *priority;
}
}
}
}
// It was not found
None
last_matched_style
}

/// Get the ANSI style for a path, given the corresponding `Metadata` struct.
Expand Down Expand Up @@ -563,17 +622,34 @@ mod tests {
fn style_for_path_uses_correct_ordering() {
let lscolors = LsColors::from_string("*.foo=01;35:*README.foo=33;44");

// dummy.foo matches to *.foo without getting overriden.
let style_foo = lscolors.style_for_path("some/folder/dummy.foo").unwrap();
assert_eq!(FontStyle::bold(), style_foo.font_style);
assert_eq!(Some(Color::Magenta), style_foo.foreground);
assert_eq!(None, style_foo.background);

// README.foo matches to *README.foo by overriding *.foo
let style_readme = lscolors
.style_for_path("some/other/folder/README.foo")
.unwrap();
assert_eq!(FontStyle::default(), style_readme.font_style);
assert_eq!(Some(Color::Yellow), style_readme.foreground);
assert_eq!(Some(Color::Blue), style_readme.background);

let lscolors = LsColors::from_string("*README.foo=33;44:*.foo=01;35");

let style_foo = lscolors.style_for_path("some/folder/dummy.foo").unwrap();
assert_eq!(FontStyle::bold(), style_foo.font_style);
assert_eq!(Some(Color::Magenta), style_foo.foreground);
assert_eq!(None, style_foo.background);

// README.foo matches to *.foo because *.foo overrides *README.foo
let style_readme = lscolors
.style_for_path("some/other/folder/README.foo")
.unwrap();
assert_eq!(FontStyle::bold(), style_readme.font_style);
assert_eq!(Some(Color::Magenta), style_readme.foreground);
assert_eq!(None, style_readme.background);
}

#[test]
Expand Down Expand Up @@ -842,4 +918,83 @@ mod tests {
let style = lscolors.style_for_path(&tmp_file_path);
assert_eq!(None, style);
}
#[test]
fn file_suffix_case() {
let assert_bold_fg_magenta = |style: Option<&Style>| {
Some(Color::Magenta) == style.and_then(|v| v.foreground)
&& matches!(style,Some(sty) if sty.font_style.bold )
};
let assert_bold_fg_green = |style: Option<&Style>| {
Some(Color::Green) == style.and_then(|v| v.foreground)
&& matches!(style,Some(sty) if sty.font_style.bold )
};

// *.jpg is specified only once so any suffix that has .jpg should match
// without caring about the letter case
let lscolors = LsColors::from_string("*.jpg=01;35:*.Z=01;31");
let lowercase_suffix = lscolors.style_for_str("img1.jpg");
assert!(assert_bold_fg_magenta(lowercase_suffix));
let uppercase_suffix = lscolors.style_for_str("img1.JPG");
assert!(assert_bold_fg_magenta(uppercase_suffix));
let mixedcase_suffix = lscolors.style_for_str("img1.JpG");
assert!(assert_bold_fg_magenta(mixedcase_suffix));

// *.jpg is specified more than once with different cases and style, so
// case should matter here
let lscolors = LsColors::from_string("*.jpg=01;35:*.JPG=01;32");
let lowercase_suffix = lscolors.style_for_str("img1.jpg");
assert!(assert_bold_fg_magenta(lowercase_suffix));
let uppercase_suffix = lscolors.style_for_str("img1.JPG");
assert!(assert_bold_fg_green(uppercase_suffix));
let mixedcase_suffix = lscolors.style_for_str("img1.JpG");
assert!(mixedcase_suffix.is_none());

// *.jpg is specified more than once with different cases but style is same, so
// case can ignored
let lscolors = LsColors::from_string("*.jpg=01;35:*.JPG=01;35");
let lowercase_suffix = lscolors.style_for_str("img1.jpg");
assert!(assert_bold_fg_magenta(lowercase_suffix));
let uppercase_suffix = lscolors.style_for_str("img1.JPG");
assert!(assert_bold_fg_magenta(uppercase_suffix));
let mixedcase_suffix = lscolors.style_for_str("img1.JpG");
assert!(assert_bold_fg_magenta(mixedcase_suffix));

// last *.jpg gets more priority resulting in same style across
// different cases specified, so case can ignored
let lscolors = LsColors::from_string("*.jpg=01;32:*.jpg=01;35:*.JPG=01;35");
let lowercase_suffix = lscolors.style_for_str("img1.jpg");
assert!(assert_bold_fg_magenta(lowercase_suffix));
let uppercase_suffix = lscolors.style_for_str("img1.JPG");
assert!(assert_bold_fg_magenta(uppercase_suffix));
let mixedcase_suffix = lscolors.style_for_str("img1.JpG");
assert!(assert_bold_fg_magenta(mixedcase_suffix));

// same as above with different combinations
let lscolors = LsColors::from_string("*.jpg=01;32:*.jpg=01;35:*.JPG=01;32:*.jpg=01;32");
let lowercase_suffix = lscolors.style_for_str("img1.jpg");
assert!(assert_bold_fg_green(lowercase_suffix));
let uppercase_suffix = lscolors.style_for_str("img1.JPG");
assert!(assert_bold_fg_green(uppercase_suffix));
let mixedcase_suffix = lscolors.style_for_str("img1.JpG");
assert!(assert_bold_fg_green(mixedcase_suffix));

// last *.jpg gets more priority resulting in different style across
// different cases specified, so case matters
let lscolors = LsColors::from_string("*.jpg=01;32:*.jpg=01;35:*.JPG=01;32");
let lowercase_suffix = lscolors.style_for_str("img1.jpg");
assert!(assert_bold_fg_magenta(lowercase_suffix));
let uppercase_suffix = lscolors.style_for_str("img1.JPG");
assert!(assert_bold_fg_green(uppercase_suffix));
let mixedcase_suffix = lscolors.style_for_str("img1.JpG");
assert!(mixedcase_suffix.is_none());

// same as above with different combinations
let lscolors = LsColors::from_string("*.jpg=01;32:*.jpg=01;35:*.JPG=01;35:*.jpg=01;32");
let lowercase_suffix = lscolors.style_for_str("img1.jpg");
assert!(assert_bold_fg_green(lowercase_suffix));
let uppercase_suffix = lscolors.style_for_str("img1.JPG");
assert!(assert_bold_fg_magenta(uppercase_suffix));
let mixedcase_suffix = lscolors.style_for_str("img1.JpG");
assert!(mixedcase_suffix.is_none());
}
}
Loading