1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
|
// -*- 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, allowCommas bool) (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 if allowCommas {
// treat commas outside of groups as literal commas
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, allowCommas bool) (*PathPattern, error) {
regexPattern, err := createRegex(pattern, globDefault, allowCommas)
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)
}
|