@@ -565,15 +565,55 @@ void git_attr_path__free(git_attr_path *info)
565565 */
566566static size_t trailing_space_length (const char * p , size_t len )
567567{
568- size_t n ;
568+ size_t n , i ;
569569 for (n = len ; n ; n -- ) {
570- if ((p [n - 1 ] != ' ' && p [n - 1 ] != '\t' ) ||
571- (n > 1 && p [n - 2 ] == '\\' ))
570+ if (p [n - 1 ] != ' ' && p [n - 1 ] != '\t' )
571+ break ;
572+
573+ /*
574+ * Count escape-characters before space. In case where it's an
575+ * even number of escape characters, then the escape char itself
576+ * is escaped and the whitespace is an unescaped whitespace.
577+ * Otherwise, the last escape char is not escaped and the
578+ * whitespace in an escaped whitespace.
579+ */
580+ i = n ;
581+ while (i > 1 && p [i - 2 ] == '\\' )
582+ i -- ;
583+ if ((n - i ) % 2 )
572584 break ;
573585 }
574586 return len - n ;
575587}
576588
589+ static size_t unescape_spaces (char * str )
590+ {
591+ char * scan , * pos = str ;
592+ bool escaped = false;
593+
594+ if (!str )
595+ return 0 ;
596+
597+ for (scan = str ; * scan ; scan ++ ) {
598+ if (!escaped && * scan == '\\' ) {
599+ escaped = true;
600+ continue ;
601+ }
602+
603+ /* Only insert the escape character for escaped non-spaces */
604+ if (escaped && !git__isspace (* scan ))
605+ * pos ++ = '\\' ;
606+
607+ * pos ++ = * scan ;
608+ escaped = false;
609+ }
610+
611+ if (pos != scan )
612+ * pos = '\0' ;
613+
614+ return (pos - str );
615+ }
616+
577617/*
578618 * This will return 0 if the spec was filled out,
579619 * GIT_ENOTFOUND if the fnmatch does not require matching, or
@@ -587,6 +627,7 @@ int git_attr_fnmatch__parse(
587627{
588628 const char * pattern , * scan ;
589629 int slash_count , allow_space ;
630+ bool escaped ;
590631
591632 assert (spec && base && * base );
592633
@@ -623,28 +664,29 @@ int git_attr_fnmatch__parse(
623664 }
624665
625666 slash_count = 0 ;
667+ escaped = false;
668+ /* Scan until a non-escaped whitespace. */
626669 for (scan = pattern ; * scan != '\0' ; ++ scan ) {
627- /*
628- * Scan until a non-escaped whitespace: find a whitespace, then look
629- * one char backward to ensure that it's not prefixed by a `\`.
630- * Only look backward if we're not at the first position (`pattern`).
631- */
632- if (git__isspace (* scan ) && scan > pattern && * (scan - 1 ) != '\\' ) {
633- if (!allow_space || (* scan != ' ' && * scan != '\t' && * scan != '\r' ))
634- break ;
635- }
670+ char c = * scan ;
636671
637- if (* scan == '/' ) {
672+ if (c == '\\' && !escaped ) {
673+ escaped = true;
674+ continue ;
675+ } else if (git__isspace (c ) && !escaped ) {
676+ if (!allow_space || (c != ' ' && c != '\t' && c != '\r' ))
677+ break ;
678+ } else if (c == '/' ) {
638679 spec -> flags = spec -> flags | GIT_ATTR_FNMATCH_FULLPATH ;
639680 slash_count ++ ;
640681
641682 if (slash_count == 1 && pattern == scan )
642683 pattern ++ ;
643- }
644- /* remember if we see an unescaped wildcard in pattern */
645- else if (git__iswildcard (* scan ) &&
646- (scan == pattern || (* (scan - 1 ) != '\\' )))
684+ } else if (git__iswildcard (c ) && !escaped ) {
685+ /* remember if we see an unescaped wildcard in pattern */
647686 spec -> flags = spec -> flags | GIT_ATTR_FNMATCH_HASWILD ;
687+ }
688+
689+ escaped = false;
648690 }
649691
650692 * base = scan ;
@@ -699,9 +741,8 @@ int git_attr_fnmatch__parse(
699741 * base = git__next_line (pattern );
700742 return -1 ;
701743 } else {
702- /* strip '\' that might have be used for internal whitespace */
703- spec -> length = git__unescape (spec -> pattern );
704- /* TODO: convert remaining '\' into '/' for POSIX ??? */
744+ /* strip '\' that might have been used for internal whitespace */
745+ spec -> length = unescape_spaces (spec -> pattern );
705746 }
706747
707748 return 0 ;
0 commit comments