@@ -18,6 +18,11 @@ const ANSI_RESET: &str = "\x1b[0m";
1818const ANSI_CLEAR_EOL : & str = "\x1b [K" ;
1919const 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+
477682fn 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