|  | 
|  | 1 | +// Copyright 2025 The Gitea Authors. All rights reserved. | 
|  | 2 | +// SPDX-License-Identifier: MIT | 
|  | 3 | + | 
|  | 4 | +package glob | 
|  | 5 | + | 
|  | 6 | +import ( | 
|  | 7 | +	"errors" | 
|  | 8 | +	"fmt" | 
|  | 9 | +	"regexp" | 
|  | 10 | + | 
|  | 11 | +	"code.gitea.io/gitea/modules/util" | 
|  | 12 | +) | 
|  | 13 | + | 
|  | 14 | +// Reference: https://github.com/gobwas/glob/blob/master/glob.go | 
|  | 15 | + | 
|  | 16 | +type Glob interface { | 
|  | 17 | +	Match(string) bool | 
|  | 18 | +} | 
|  | 19 | + | 
|  | 20 | +type globCompiler struct { | 
|  | 21 | +	nonSeparatorChars string | 
|  | 22 | +	globPattern       []rune | 
|  | 23 | +	regexpPattern     string | 
|  | 24 | +	regexp            *regexp.Regexp | 
|  | 25 | +	pos               int | 
|  | 26 | +} | 
|  | 27 | + | 
|  | 28 | +// compileChars compiles character class patterns like [abc] or [!abc] | 
|  | 29 | +func (g *globCompiler) compileChars() (string, error) { | 
|  | 30 | +	result := "" | 
|  | 31 | +	if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '!' { | 
|  | 32 | +		g.pos++ | 
|  | 33 | +		result += "^" | 
|  | 34 | +	} | 
|  | 35 | + | 
|  | 36 | +	for g.pos < len(g.globPattern) { | 
|  | 37 | +		c := g.globPattern[g.pos] | 
|  | 38 | +		g.pos++ | 
|  | 39 | + | 
|  | 40 | +		if c == ']' { | 
|  | 41 | +			return "[" + result + "]", nil | 
|  | 42 | +		} | 
|  | 43 | + | 
|  | 44 | +		if c == '\\' { | 
|  | 45 | +			if g.pos >= len(g.globPattern) { | 
|  | 46 | +				return "", errors.New("unterminated character class escape") | 
|  | 47 | +			} | 
|  | 48 | +			result += "\\" + string(g.globPattern[g.pos]) | 
|  | 49 | +			g.pos++ | 
|  | 50 | +		} else { | 
|  | 51 | +			result += string(c) | 
|  | 52 | +		} | 
|  | 53 | +	} | 
|  | 54 | + | 
|  | 55 | +	return "", errors.New("unterminated character class") | 
|  | 56 | +} | 
|  | 57 | + | 
|  | 58 | +// compile compiles the glob pattern into a regular expression | 
|  | 59 | +func (g *globCompiler) compile(subPattern bool) (string, error) { | 
|  | 60 | +	result := "" | 
|  | 61 | + | 
|  | 62 | +	for g.pos < len(g.globPattern) { | 
|  | 63 | +		c := g.globPattern[g.pos] | 
|  | 64 | +		g.pos++ | 
|  | 65 | + | 
|  | 66 | +		if subPattern && c == '}' { | 
|  | 67 | +			return "(" + result + ")", nil | 
|  | 68 | +		} | 
|  | 69 | + | 
|  | 70 | +		switch c { | 
|  | 71 | +		case '*': | 
|  | 72 | +			if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '*' { | 
|  | 73 | +				g.pos++ | 
|  | 74 | +				result += ".*" // match any sequence of characters | 
|  | 75 | +			} else { | 
|  | 76 | +				result += g.nonSeparatorChars + "*" // match any sequence of non-separator characters | 
|  | 77 | +			} | 
|  | 78 | +		case '?': | 
|  | 79 | +			result += g.nonSeparatorChars // match any single non-separator character | 
|  | 80 | +		case '[': | 
|  | 81 | +			chars, err := g.compileChars() | 
|  | 82 | +			if err != nil { | 
|  | 83 | +				return "", err | 
|  | 84 | +			} | 
|  | 85 | +			result += chars | 
|  | 86 | +		case '{': | 
|  | 87 | +			subResult, err := g.compile(true) | 
|  | 88 | +			if err != nil { | 
|  | 89 | +				return "", err | 
|  | 90 | +			} | 
|  | 91 | +			result += subResult | 
|  | 92 | +		case ',': | 
|  | 93 | +			if subPattern { | 
|  | 94 | +				result += "|" | 
|  | 95 | +			} else { | 
|  | 96 | +				result += "," | 
|  | 97 | +			} | 
|  | 98 | +		case '\\': | 
|  | 99 | +			if g.pos >= len(g.globPattern) { | 
|  | 100 | +				return "", errors.New("no character to escape") | 
|  | 101 | +			} | 
|  | 102 | +			result += "\\" + string(g.globPattern[g.pos]) | 
|  | 103 | +			g.pos++ | 
|  | 104 | +		case '.', '+', '^', '$', '(', ')', '|': | 
|  | 105 | +			result += "\\" + string(c) // escape regexp special characters | 
|  | 106 | +		default: | 
|  | 107 | +			result += string(c) | 
|  | 108 | +		} | 
|  | 109 | +	} | 
|  | 110 | + | 
|  | 111 | +	return result, nil | 
|  | 112 | +} | 
|  | 113 | + | 
|  | 114 | +func newGlobCompiler(pattern string, separators ...rune) (Glob, error) { | 
|  | 115 | +	g := &globCompiler{globPattern: []rune(pattern)} | 
|  | 116 | + | 
|  | 117 | +	// Escape separators for use in character class | 
|  | 118 | +	escapedSeparators := regexp.QuoteMeta(string(separators)) | 
|  | 119 | +	if escapedSeparators != "" { | 
|  | 120 | +		g.nonSeparatorChars = "[^" + escapedSeparators + "]" | 
|  | 121 | +	} else { | 
|  | 122 | +		g.nonSeparatorChars = "." | 
|  | 123 | +	} | 
|  | 124 | + | 
|  | 125 | +	compiled, err := g.compile(false) | 
|  | 126 | +	if err != nil { | 
|  | 127 | +		return nil, err | 
|  | 128 | +	} | 
|  | 129 | + | 
|  | 130 | +	g.regexpPattern = "^" + compiled + "$" | 
|  | 131 | + | 
|  | 132 | +	regex, err := regexp.Compile(g.regexpPattern) | 
|  | 133 | +	if err != nil { | 
|  | 134 | +		return nil, fmt.Errorf("failed to compile regexp: %w", err) | 
|  | 135 | +	} | 
|  | 136 | + | 
|  | 137 | +	g.regexp = regex | 
|  | 138 | +	return g, nil | 
|  | 139 | +} | 
|  | 140 | + | 
|  | 141 | +func (g *globCompiler) Match(s string) bool { | 
|  | 142 | +	return g.regexp.MatchString(s) | 
|  | 143 | +} | 
|  | 144 | + | 
|  | 145 | +func Compile(pattern string, separators ...rune) (Glob, error) { | 
|  | 146 | +	return newGlobCompiler(pattern, separators...) | 
|  | 147 | +} | 
|  | 148 | + | 
|  | 149 | +func MustCompile(pattern string, separators ...rune) Glob { | 
|  | 150 | +	g, err := Compile(pattern, separators...) | 
|  | 151 | +	if err != nil { | 
|  | 152 | +		panic(err) | 
|  | 153 | +	} | 
|  | 154 | +	return g | 
|  | 155 | +} | 
|  | 156 | + | 
|  | 157 | +func IsSpecialByte(c byte) bool { | 
|  | 158 | +	return c == '*' || c == '?' || c == '\\' || c == '[' || c == ']' || c == '{' || c == '}' | 
|  | 159 | +} | 
|  | 160 | + | 
|  | 161 | +// QuoteMeta returns a string that quotes all glob pattern meta characters | 
|  | 162 | +// inside the argument text; For example, QuoteMeta(`{foo*}`) returns `\[foo\*\]`. | 
|  | 163 | +// Reference: https://github.com/gobwas/glob/blob/master/glob.go | 
|  | 164 | +func QuoteMeta(s string) string { | 
|  | 165 | +	pos := 0 | 
|  | 166 | +	for pos < len(s) && !IsSpecialByte(s[pos]) { | 
|  | 167 | +		pos++ | 
|  | 168 | +	} | 
|  | 169 | +	if pos == len(s) { | 
|  | 170 | +		return s | 
|  | 171 | +	} | 
|  | 172 | +	b := make([]byte, pos+2*(len(s)-pos)) | 
|  | 173 | +	copy(b, s[0:pos]) | 
|  | 174 | +	to := pos | 
|  | 175 | +	for ; pos < len(s); pos++ { | 
|  | 176 | +		if IsSpecialByte(s[pos]) { | 
|  | 177 | +			b[to] = '\\' | 
|  | 178 | +			to++ | 
|  | 179 | +		} | 
|  | 180 | +		b[to] = s[pos] | 
|  | 181 | +		to++ | 
|  | 182 | +	} | 
|  | 183 | +	return util.UnsafeBytesToString(b[0:to]) | 
|  | 184 | +} | 
0 commit comments