mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
178 lines
4.3 KiB
Go
178 lines
4.3 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2021 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package utils
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
)
|
|
|
|
type PathPattern struct {
|
|
pattern string
|
|
regex *regexp.Regexp
|
|
}
|
|
|
|
const maxGroupDepth = 50
|
|
|
|
type GlobFlags int
|
|
|
|
const (
|
|
globDefault GlobFlags = 1 << iota
|
|
globNull
|
|
)
|
|
|
|
// createRegex converts the apparmor-like glob sequence into a regex. Loosely
|
|
// using this as reference:
|
|
// https://gitlab.com/apparmor/apparmor/-/blob/master/parser/parser_regex.c#L107
|
|
func createRegex(pattern string, glob GlobFlags) (string, error) {
|
|
regex := "^"
|
|
|
|
appendGlob := func(defaultGlob, nullGlob string) {
|
|
var pattern string
|
|
switch glob {
|
|
case globDefault:
|
|
pattern = defaultGlob
|
|
case globNull:
|
|
pattern = nullGlob
|
|
}
|
|
regex += pattern
|
|
}
|
|
|
|
const (
|
|
noSlashOrNull = `[^/\x00]`
|
|
noSlash = `[^/]`
|
|
)
|
|
|
|
escapeNext := false
|
|
currentGroupLevel := 0
|
|
inCharClass := false
|
|
skipNext := false
|
|
itemCountInGroup := new([maxGroupDepth + 1]int)
|
|
for i, ch := range pattern {
|
|
if escapeNext {
|
|
regex += regexp.QuoteMeta(string(ch))
|
|
escapeNext = false
|
|
continue
|
|
}
|
|
if skipNext {
|
|
skipNext = false
|
|
continue
|
|
}
|
|
if inCharClass && ch != '\\' && ch != ']' {
|
|
// no characters are special other than '\' and ']'
|
|
regex += string(ch)
|
|
continue
|
|
}
|
|
switch ch {
|
|
case '\\':
|
|
escapeNext = true
|
|
case '*':
|
|
if regex[len(regex)-1] == '/' {
|
|
// if the * is at the end of the pattern or is followed by a
|
|
// '/' we don't want it to match an empty string:
|
|
// /foo/* -> should not match /foo/
|
|
// /foo/*bar -> should match /foo/bar
|
|
// /*/foo -> should not match //foo
|
|
pos := i + 1
|
|
for len(pattern) > pos && pattern[pos] == '*' {
|
|
pos++
|
|
}
|
|
if len(pattern) <= pos || pattern[pos] == '/' {
|
|
appendGlob(noSlashOrNull, noSlash)
|
|
}
|
|
}
|
|
|
|
if len(pattern) > i+1 && pattern[i+1] == '*' {
|
|
// Handle **
|
|
appendGlob("[^\\x00]*", ".*")
|
|
skipNext = true
|
|
} else {
|
|
appendGlob(noSlashOrNull+"*", noSlash+"*")
|
|
}
|
|
case '?':
|
|
appendGlob(noSlashOrNull, noSlash)
|
|
case '[':
|
|
inCharClass = true
|
|
regex += string(ch)
|
|
case ']':
|
|
if !inCharClass {
|
|
return "", fmt.Errorf("pattern contains unmatching ']': %q", pattern)
|
|
}
|
|
inCharClass = false
|
|
regex += string(ch)
|
|
case '{':
|
|
currentGroupLevel++
|
|
if currentGroupLevel > maxGroupDepth {
|
|
return "", fmt.Errorf("maximum group depth exceeded: %q", pattern)
|
|
}
|
|
itemCountInGroup[currentGroupLevel] = 0
|
|
regex += "("
|
|
case '}':
|
|
if currentGroupLevel <= 0 {
|
|
return "", fmt.Errorf("invalid closing brace, no matching open { found: %q", pattern)
|
|
}
|
|
if itemCountInGroup[currentGroupLevel] == 0 {
|
|
return "", fmt.Errorf("invalid number of items between {}: %q", pattern)
|
|
}
|
|
currentGroupLevel--
|
|
regex += ")"
|
|
case ',':
|
|
if currentGroupLevel > 0 {
|
|
itemCountInGroup[currentGroupLevel]++
|
|
regex += "|"
|
|
} else {
|
|
return "", fmt.Errorf("cannot use ',' outside of group or character class")
|
|
}
|
|
default:
|
|
// take literal character (with quoting if needed)
|
|
regex += regexp.QuoteMeta(string(ch))
|
|
}
|
|
}
|
|
|
|
if currentGroupLevel > 0 {
|
|
return "", fmt.Errorf("missing %d closing brace(s): %q", currentGroupLevel, pattern)
|
|
}
|
|
if inCharClass {
|
|
return "", fmt.Errorf("missing closing bracket ']': %q", pattern)
|
|
}
|
|
if escapeNext {
|
|
return "", fmt.Errorf("expected character after '\\': %q", pattern)
|
|
}
|
|
|
|
regex += "$"
|
|
return regex, nil
|
|
}
|
|
|
|
func NewPathPattern(pattern string) (*PathPattern, error) {
|
|
regexPattern, err := createRegex(pattern, globDefault)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
regex := regexp.MustCompile(regexPattern)
|
|
|
|
pp := &PathPattern{pattern, regex}
|
|
return pp, nil
|
|
}
|
|
|
|
func (pp *PathPattern) Matches(path string) bool {
|
|
return pp.regex.MatchString(path)
|
|
}
|