mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 06:38:37 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			185 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			185 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2025 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package glob
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"regexp"
 | |
| 
 | |
| 	"code.gitea.io/gitea/modules/util"
 | |
| )
 | |
| 
 | |
| // Reference: https://github.com/gobwas/glob/blob/master/glob.go
 | |
| 
 | |
| type Glob interface {
 | |
| 	Match(string) bool
 | |
| }
 | |
| 
 | |
| type globCompiler struct {
 | |
| 	nonSeparatorChars string
 | |
| 	globPattern       []rune
 | |
| 	regexpPattern     string
 | |
| 	regexp            *regexp.Regexp
 | |
| 	pos               int
 | |
| }
 | |
| 
 | |
| // compileChars compiles character class patterns like [abc] or [!abc]
 | |
| func (g *globCompiler) compileChars() (string, error) {
 | |
| 	result := ""
 | |
| 	if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '!' {
 | |
| 		g.pos++
 | |
| 		result += "^"
 | |
| 	}
 | |
| 
 | |
| 	for g.pos < len(g.globPattern) {
 | |
| 		c := g.globPattern[g.pos]
 | |
| 		g.pos++
 | |
| 
 | |
| 		if c == ']' {
 | |
| 			return "[" + result + "]", nil
 | |
| 		}
 | |
| 
 | |
| 		if c == '\\' {
 | |
| 			if g.pos >= len(g.globPattern) {
 | |
| 				return "", errors.New("unterminated character class escape")
 | |
| 			}
 | |
| 			result += "\\" + string(g.globPattern[g.pos])
 | |
| 			g.pos++
 | |
| 		} else {
 | |
| 			result += string(c)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return "", errors.New("unterminated character class")
 | |
| }
 | |
| 
 | |
| // compile compiles the glob pattern into a regular expression
 | |
| func (g *globCompiler) compile(subPattern bool) (string, error) {
 | |
| 	result := ""
 | |
| 
 | |
| 	for g.pos < len(g.globPattern) {
 | |
| 		c := g.globPattern[g.pos]
 | |
| 		g.pos++
 | |
| 
 | |
| 		if subPattern && c == '}' {
 | |
| 			return "(" + result + ")", nil
 | |
| 		}
 | |
| 
 | |
| 		switch c {
 | |
| 		case '*':
 | |
| 			if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '*' {
 | |
| 				g.pos++
 | |
| 				result += ".*" // match any sequence of characters
 | |
| 			} else {
 | |
| 				result += g.nonSeparatorChars + "*" // match any sequence of non-separator characters
 | |
| 			}
 | |
| 		case '?':
 | |
| 			result += g.nonSeparatorChars // match any single non-separator character
 | |
| 		case '[':
 | |
| 			chars, err := g.compileChars()
 | |
| 			if err != nil {
 | |
| 				return "", err
 | |
| 			}
 | |
| 			result += chars
 | |
| 		case '{':
 | |
| 			subResult, err := g.compile(true)
 | |
| 			if err != nil {
 | |
| 				return "", err
 | |
| 			}
 | |
| 			result += subResult
 | |
| 		case ',':
 | |
| 			if subPattern {
 | |
| 				result += "|"
 | |
| 			} else {
 | |
| 				result += ","
 | |
| 			}
 | |
| 		case '\\':
 | |
| 			if g.pos >= len(g.globPattern) {
 | |
| 				return "", errors.New("no character to escape")
 | |
| 			}
 | |
| 			result += "\\" + string(g.globPattern[g.pos])
 | |
| 			g.pos++
 | |
| 		case '.', '+', '^', '$', '(', ')', '|':
 | |
| 			result += "\\" + string(c) // escape regexp special characters
 | |
| 		default:
 | |
| 			result += string(c)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| func newGlobCompiler(pattern string, separators ...rune) (Glob, error) {
 | |
| 	g := &globCompiler{globPattern: []rune(pattern)}
 | |
| 
 | |
| 	// Escape separators for use in character class
 | |
| 	escapedSeparators := regexp.QuoteMeta(string(separators))
 | |
| 	if escapedSeparators != "" {
 | |
| 		g.nonSeparatorChars = "[^" + escapedSeparators + "]"
 | |
| 	} else {
 | |
| 		g.nonSeparatorChars = "."
 | |
| 	}
 | |
| 
 | |
| 	compiled, err := g.compile(false)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	g.regexpPattern = "^" + compiled + "$"
 | |
| 
 | |
| 	regex, err := regexp.Compile(g.regexpPattern)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to compile regexp: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	g.regexp = regex
 | |
| 	return g, nil
 | |
| }
 | |
| 
 | |
| func (g *globCompiler) Match(s string) bool {
 | |
| 	return g.regexp.MatchString(s)
 | |
| }
 | |
| 
 | |
| func Compile(pattern string, separators ...rune) (Glob, error) {
 | |
| 	return newGlobCompiler(pattern, separators...)
 | |
| }
 | |
| 
 | |
| func MustCompile(pattern string, separators ...rune) Glob {
 | |
| 	g, err := Compile(pattern, separators...)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	return g
 | |
| }
 | |
| 
 | |
| func IsSpecialByte(c byte) bool {
 | |
| 	return c == '*' || c == '?' || c == '\\' || c == '[' || c == ']' || c == '{' || c == '}'
 | |
| }
 | |
| 
 | |
| // QuoteMeta returns a string that quotes all glob pattern meta characters
 | |
| // inside the argument text; For example, QuoteMeta(`{foo*}`) returns `\[foo\*\]`.
 | |
| // Reference: https://github.com/gobwas/glob/blob/master/glob.go
 | |
| func QuoteMeta(s string) string {
 | |
| 	pos := 0
 | |
| 	for pos < len(s) && !IsSpecialByte(s[pos]) {
 | |
| 		pos++
 | |
| 	}
 | |
| 	if pos == len(s) {
 | |
| 		return s
 | |
| 	}
 | |
| 	b := make([]byte, pos+2*(len(s)-pos))
 | |
| 	copy(b, s[0:pos])
 | |
| 	to := pos
 | |
| 	for ; pos < len(s); pos++ {
 | |
| 		if IsSpecialByte(s[pos]) {
 | |
| 			b[to] = '\\'
 | |
| 			to++
 | |
| 		}
 | |
| 		b[to] = s[pos]
 | |
| 		to++
 | |
| 	}
 | |
| 	return util.UnsafeBytesToString(b[0:to])
 | |
| }
 |