Skip to content

Commit 788c0e8

Browse files
karanabesylvestre
authored andcommitted
ls: validate LS_COLORS and refactor styles
Add GNU-like LS_COLORS validation and disable color output on parse errors with warnings. Refactor raw style application and fallback handling to reduce complexity and document empty entries. De-duplicate locale escaping and add warning strings for LS_COLORS errors.
1 parent 8b03f18 commit 788c0e8

File tree

4 files changed

+280
-50
lines changed

4 files changed

+280
-50
lines changed

src/uu/ls/locales/en-US.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ ls-invalid-quoting-style = {$program}: Ignoring invalid value of environment var
123123
ls-invalid-columns-width = ignoring invalid width in environment variable COLUMNS: {$width}
124124
ls-invalid-ignore-pattern = Invalid pattern for ignore: {$pattern}
125125
ls-invalid-hide-pattern = Invalid pattern for hide: {$pattern}
126+
ls-warning-unrecognized-ls-colors-prefix = unrecognized prefix: {$prefix}
127+
ls-warning-unparsable-ls-colors = unparsable value for LS_COLORS environment variable
126128
ls-total = total {$size}
127129
128130
# Security context warnings

src/uu/ls/locales/fr-FR.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,6 @@ ls-invalid-quoting-style = {$program} : Ignorer la valeur invalide de la variabl
123123
ls-invalid-columns-width = ignorer la largeur invalide dans la variable d'environnement COLUMNS : {$width}
124124
ls-invalid-ignore-pattern = Motif invalide pour ignore : {$pattern}
125125
ls-invalid-hide-pattern = Motif invalide pour hide : {$pattern}
126+
ls-warning-unrecognized-ls-colors-prefix = préfixe non reconnu : {$prefix}
127+
ls-warning-unparsable-ls-colors = valeur illisible pour la variable d'environnement LS_COLORS
126128
ls-total = total {$size}

src/uu/ls/src/colors.rs

Lines changed: 249 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ const ANSI_RESET: &str = "\x1b[0m";
1818
const ANSI_CLEAR_EOL: &str = "\x1b[K";
1919
const EMPTY_STYLE: &str = "\x1b[m";
2020

21+
enum RawIndicatorStyle {
22+
Empty,
23+
Code(String),
24+
}
25+
2126
/// We need this struct to be able to store the previous style.
2227
/// This because we need to check the previous value in case we don't need
2328
/// the reset
@@ -66,46 +71,24 @@ impl<'a> StyleManager<'a> {
6671
// Fast-path: apply LS_COLORS raw SGR codes verbatim,
6772
// bypassing LsColors fallbacks so the entry from LS_COLORS
6873
// is honored exactly as specified.
69-
if let Some(indicator) = self.indicator_for_raw_code(path) {
70-
let should_skip = indicator == Indicator::SymbolicLink
71-
&& self.ln_color_from_target
72-
&& path.path().exists();
73-
74-
if !should_skip {
75-
if let Some(raw) = self.indicator_codes.get(&indicator).cloned() {
76-
if raw.is_empty() {
77-
return self.apply_empty_style(name, wrap);
78-
}
79-
style_code.push_str(self.reset(!self.initial_reset_is_done));
80-
style_code.push_str(ANSI_CSI);
81-
style_code.push_str(&raw);
82-
style_code.push_str(ANSI_SGR_END);
83-
applied_raw_code = true;
84-
self.current_style = None;
85-
force_suffix_reset = true;
86-
}
74+
match self.raw_indicator_style_for_path(path) {
75+
Some(RawIndicatorStyle::Empty) => {
76+
// An explicit empty entry (e.g. "or=") disables coloring and
77+
// bypasses fallbacks, matching GNU ls behavior.
78+
return self.apply_empty_style(name, wrap);
8779
}
80+
Some(RawIndicatorStyle::Code(raw)) => {
81+
style_code.push_str(&self.build_raw_style_code(&raw));
82+
applied_raw_code = true;
83+
self.current_style = None;
84+
force_suffix_reset = true;
85+
}
86+
None => {}
8887
}
8988
}
9089

9190
if !applied_raw_code {
92-
if let Some(new_style) = new_style {
93-
// we only need to apply a new style if it's not the same as the current
94-
// style for example if normal is the current style and a file with
95-
// normal style is to be printed we could skip printing new color
96-
// codes
97-
if !self.is_current_style(new_style) {
98-
style_code.push_str(self.reset(!self.initial_reset_is_done));
99-
style_code.push_str(&self.get_style_code(new_style));
100-
}
101-
}
102-
// if new style is None and current style is Normal we should reset it
103-
else if matches!(self.get_normal_style().copied(), Some(norm_style) if self.is_current_style(&norm_style))
104-
{
105-
style_code.push_str(self.reset(false));
106-
// even though this is an unnecessary reset for gnu compatibility we allow it here
107-
force_suffix_reset = true;
108-
}
91+
self.append_style_code_for_style(new_style, &mut style_code, &mut force_suffix_reset);
10992
}
11093

11194
// we need this clear to eol code in some terminals, for instance if the
@@ -122,6 +105,58 @@ impl<'a> StyleManager<'a> {
122105
ret
123106
}
124107

108+
fn raw_indicator_style_for_path(&self, path: &PathData) -> Option<RawIndicatorStyle> {
109+
let indicator = self.indicator_for_raw_code(path)?;
110+
let should_skip = indicator == Indicator::SymbolicLink
111+
&& self.ln_color_from_target
112+
&& path.path().exists();
113+
114+
if should_skip {
115+
return None;
116+
}
117+
118+
let raw = self.indicator_codes.get(&indicator)?;
119+
if raw.is_empty() {
120+
Some(RawIndicatorStyle::Empty)
121+
} else {
122+
Some(RawIndicatorStyle::Code(raw.clone()))
123+
}
124+
}
125+
126+
fn build_raw_style_code(&mut self, raw: &str) -> String {
127+
let mut style_code = String::new();
128+
style_code.push_str(self.reset(!self.initial_reset_is_done));
129+
style_code.push_str(ANSI_CSI);
130+
style_code.push_str(raw);
131+
style_code.push_str(ANSI_SGR_END);
132+
style_code
133+
}
134+
135+
fn append_style_code_for_style(
136+
&mut self,
137+
new_style: Option<&Style>,
138+
style_code: &mut String,
139+
force_suffix_reset: &mut bool,
140+
) {
141+
if let Some(new_style) = new_style {
142+
// we only need to apply a new style if it's not the same as the current
143+
// style for example if normal is the current style and a file with
144+
// normal style is to be printed we could skip printing new color
145+
// codes
146+
if !self.is_current_style(new_style) {
147+
style_code.push_str(self.reset(!self.initial_reset_is_done));
148+
style_code.push_str(&self.get_style_code(new_style));
149+
}
150+
}
151+
// if new style is None and current style is Normal we should reset it
152+
else if matches!(self.get_normal_style().copied(), Some(norm_style) if self.is_current_style(&norm_style))
153+
{
154+
style_code.push_str(self.reset(false));
155+
// even though this is an unnecessary reset for gnu compatibility we allow it here
156+
*force_suffix_reset = true;
157+
}
158+
}
159+
125160
/// Resets the current style and returns the default ANSI reset code to
126161
/// reset all text formatting attributes. If `force` is true, the reset is
127162
/// done even if the reset has been applied before.
@@ -201,13 +236,7 @@ impl<'a> StyleManager<'a> {
201236
return self.apply_empty_style(name, wrap);
202237
}
203238

204-
let mut style_code = String::new();
205-
style_code.push_str(self.reset(!self.initial_reset_is_done));
206-
style_code.push_str(ANSI_CSI);
207-
style_code.push_str(&raw);
208-
style_code.push_str(ANSI_SGR_END);
209-
210-
let mut ret: OsString = style_code.into();
239+
let mut ret: OsString = self.build_raw_style_code(&raw).into();
211240
ret.push(name);
212241
ret.push(self.reset(true));
213242
if wrap {
@@ -474,10 +503,188 @@ pub(crate) fn color_name(
474503
style_manager.apply_style_based_on_metadata(path, md_option.as_ref(), name, wrap)
475504
}
476505

506+
#[derive(Debug)]
507+
pub(crate) enum LsColorsParseError {
508+
UnrecognizedPrefix(String),
509+
InvalidSyntax,
510+
}
511+
512+
pub(crate) fn validate_ls_colors_env() -> Result<(), LsColorsParseError> {
513+
let Ok(ls_colors) = env::var("LS_COLORS") else {
514+
return Ok(());
515+
};
516+
517+
if ls_colors.is_empty() {
518+
return Ok(());
519+
}
520+
521+
validate_ls_colors(&ls_colors)
522+
}
523+
524+
fn validate_ls_colors(ls_colors: &str) -> Result<(), LsColorsParseError> {
525+
let bytes = ls_colors.as_bytes();
526+
let mut idx = 0;
527+
528+
while idx < bytes.len() {
529+
match bytes[idx] {
530+
b':' => {
531+
idx += 1;
532+
}
533+
b'*' => {
534+
idx += 1;
535+
idx = parse_funky_string(bytes, idx, true)?;
536+
if idx >= bytes.len() || bytes[idx] != b'=' {
537+
return Err(LsColorsParseError::InvalidSyntax);
538+
}
539+
idx += 1;
540+
idx = parse_funky_string(bytes, idx, false)?;
541+
if idx < bytes.len() && bytes[idx] == b':' {
542+
idx += 1;
543+
}
544+
}
545+
_ => {
546+
if idx + 1 >= bytes.len() {
547+
return Err(LsColorsParseError::InvalidSyntax);
548+
}
549+
let label = [bytes[idx], bytes[idx + 1]];
550+
idx += 2;
551+
if idx >= bytes.len() || bytes[idx] != b'=' {
552+
return Err(LsColorsParseError::InvalidSyntax);
553+
}
554+
if !is_valid_ls_colors_prefix(label) {
555+
let prefix = String::from_utf8_lossy(&label).into_owned();
556+
return Err(LsColorsParseError::UnrecognizedPrefix(prefix));
557+
}
558+
idx += 1;
559+
idx = parse_funky_string(bytes, idx, false)?;
560+
if idx < bytes.len() && bytes[idx] == b':' {
561+
idx += 1;
562+
}
563+
}
564+
}
565+
}
566+
567+
Ok(())
568+
}
569+
570+
fn parse_funky_string(
571+
bytes: &[u8],
572+
mut idx: usize,
573+
equals_end: bool,
574+
) -> Result<usize, LsColorsParseError> {
575+
enum State {
576+
Ground,
577+
Backslash,
578+
Octal(u8),
579+
Hex(u8),
580+
Caret,
581+
}
582+
583+
let mut state = State::Ground;
584+
loop {
585+
let byte = if idx < bytes.len() { bytes[idx] } else { 0 };
586+
match state {
587+
State::Ground => match byte {
588+
b':' | 0 => return Ok(idx),
589+
b'=' if equals_end => return Ok(idx),
590+
b'\\' => {
591+
state = State::Backslash;
592+
idx += 1;
593+
}
594+
b'^' => {
595+
state = State::Caret;
596+
idx += 1;
597+
}
598+
_ => idx += 1,
599+
},
600+
State::Backslash => match byte {
601+
0 => return Err(LsColorsParseError::InvalidSyntax),
602+
b'0'..=b'7' => {
603+
state = State::Octal(byte - b'0');
604+
idx += 1;
605+
}
606+
b'x' | b'X' => {
607+
state = State::Hex(0);
608+
idx += 1;
609+
}
610+
b'a' | b'b' | b'e' | b'f' | b'n' | b'r' | b't' | b'v' | b'?' | b'_' => {
611+
state = State::Ground;
612+
idx += 1;
613+
}
614+
_ => {
615+
state = State::Ground;
616+
idx += 1;
617+
}
618+
},
619+
State::Octal(num) => match byte {
620+
b'0'..=b'7' => {
621+
state = State::Octal(num.wrapping_mul(8).wrapping_add(byte - b'0'));
622+
idx += 1;
623+
}
624+
_ => state = State::Ground,
625+
},
626+
State::Hex(num) => match byte {
627+
b'0'..=b'9' => {
628+
state = State::Hex(num.wrapping_mul(16).wrapping_add(byte - b'0'));
629+
idx += 1;
630+
}
631+
b'a'..=b'f' => {
632+
state = State::Hex(num.wrapping_mul(16).wrapping_add(byte - b'a' + 10));
633+
idx += 1;
634+
}
635+
b'A'..=b'F' => {
636+
state = State::Hex(num.wrapping_mul(16).wrapping_add(byte - b'A' + 10));
637+
idx += 1;
638+
}
639+
_ => state = State::Ground,
640+
},
641+
State::Caret => match byte {
642+
b'@'..=b'~' | b'?' => {
643+
state = State::Ground;
644+
idx += 1;
645+
}
646+
_ => return Err(LsColorsParseError::InvalidSyntax),
647+
},
648+
}
649+
}
650+
}
651+
652+
fn is_valid_ls_colors_prefix(label: [u8; 2]) -> bool {
653+
matches!(
654+
label,
655+
[b'l', b'c']
656+
| [b'r', b'c']
657+
| [b'e', b'c']
658+
| [b'r', b's']
659+
| [b'n', b'o']
660+
| [b'f', b'i']
661+
| [b'd', b'i']
662+
| [b'l', b'n']
663+
| [b'p', b'i']
664+
| [b's', b'o']
665+
| [b'b', b'd']
666+
| [b'c', b'd']
667+
| [b'm', b'i']
668+
| [b'o', b'r']
669+
| [b'e', b'x']
670+
| [b'd', b'o']
671+
| [b's', b'u']
672+
| [b's', b'g']
673+
| [b's', b't']
674+
| [b'o', b'w']
675+
| [b't', b'w']
676+
| [b'c', b'a']
677+
| [b'm', b'h']
678+
| [b'c', b'l']
679+
)
680+
}
681+
477682
fn parse_indicator_codes() -> (HashMap<Indicator, String>, bool) {
478683
let mut indicator_codes = HashMap::new();
479684
let mut ln_color_from_target = false;
480685

686+
// LS_COLORS validity is checked before enabling color output, so parse
687+
// entries directly here for raw indicator overrides.
481688
if let Ok(ls_colors) = env::var("LS_COLORS") {
482689
for entry in ls_colors.split(':') {
483690
if entry.is_empty() {

0 commit comments

Comments
 (0)