gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/utils/path_patterns.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package utils
    21  
    22  import (
    23  	"fmt"
    24  	"regexp"
    25  )
    26  
    27  type PathPattern struct {
    28  	pattern string
    29  	regex   *regexp.Regexp
    30  }
    31  
    32  const maxGroupDepth = 50
    33  
    34  type GlobFlags int
    35  
    36  const (
    37  	globDefault GlobFlags = 1 << iota
    38  	globNull
    39  )
    40  
    41  // createRegex converts the apparmor-like glob sequence into a regex. Loosely
    42  // using this as reference:
    43  // https://gitlab.com/apparmor/apparmor/-/blob/master/parser/parser_regex.c#L107
    44  func createRegex(pattern string, glob GlobFlags) (string, error) {
    45  	regex := "^"
    46  
    47  	appendGlob := func(defaultGlob, nullGlob string) {
    48  		var pattern string
    49  		switch glob {
    50  		case globDefault:
    51  			pattern = defaultGlob
    52  		case globNull:
    53  			pattern = nullGlob
    54  		}
    55  		regex += pattern
    56  	}
    57  
    58  	const (
    59  		noSlashOrNull = `[^/\x00]`
    60  		noSlash       = `[^/]`
    61  	)
    62  
    63  	escapeNext := false
    64  	currentGroupLevel := 0
    65  	inCharClass := false
    66  	skipNext := false
    67  	itemCountInGroup := new([maxGroupDepth + 1]int)
    68  	for i, ch := range pattern {
    69  		if escapeNext {
    70  			regex += regexp.QuoteMeta(string(ch))
    71  			escapeNext = false
    72  			continue
    73  		}
    74  		if skipNext {
    75  			skipNext = false
    76  			continue
    77  		}
    78  		if inCharClass && ch != '\\' && ch != ']' {
    79  			// no characters are special other than '\' and ']'
    80  			regex += string(ch)
    81  			continue
    82  		}
    83  		switch ch {
    84  		case '\\':
    85  			escapeNext = true
    86  		case '*':
    87  			if regex[len(regex)-1] == '/' {
    88  				// if the * is at the end of the pattern or is followed by a
    89  				// '/' we don't want it to match an empty string:
    90  				// /foo/* -> should not match /foo/
    91  				// /foo/*bar -> should match /foo/bar
    92  				// /*/foo -> should not match //foo
    93  				pos := i + 1
    94  				for len(pattern) > pos && pattern[pos] == '*' {
    95  					pos++
    96  				}
    97  				if len(pattern) <= pos || pattern[pos] == '/' {
    98  					appendGlob(noSlashOrNull, noSlash)
    99  				}
   100  			}
   101  
   102  			if len(pattern) > i+1 && pattern[i+1] == '*' {
   103  				// Handle **
   104  				appendGlob("[^\\x00]*", ".*")
   105  				skipNext = true
   106  			} else {
   107  				appendGlob(noSlashOrNull+"*", noSlash+"*")
   108  			}
   109  		case '?':
   110  			appendGlob(noSlashOrNull, noSlash)
   111  		case '[':
   112  			inCharClass = true
   113  			regex += string(ch)
   114  		case ']':
   115  			if !inCharClass {
   116  				return "", fmt.Errorf("pattern contains unmatching ']': %q", pattern)
   117  			}
   118  			inCharClass = false
   119  			regex += string(ch)
   120  		case '{':
   121  			currentGroupLevel++
   122  			if currentGroupLevel > maxGroupDepth {
   123  				return "", fmt.Errorf("maximum group depth exceeded: %q", pattern)
   124  			}
   125  			itemCountInGroup[currentGroupLevel] = 0
   126  			regex += "("
   127  		case '}':
   128  			if currentGroupLevel <= 0 {
   129  				return "", fmt.Errorf("invalid closing brace, no matching open { found: %q", pattern)
   130  			}
   131  			if itemCountInGroup[currentGroupLevel] == 0 {
   132  				return "", fmt.Errorf("invalid number of items between {}: %q", pattern)
   133  			}
   134  			currentGroupLevel--
   135  			regex += ")"
   136  		case ',':
   137  			if currentGroupLevel > 0 {
   138  				itemCountInGroup[currentGroupLevel]++
   139  				regex += "|"
   140  			} else {
   141  				return "", fmt.Errorf("cannot use ',' outside of group or character class")
   142  			}
   143  		default:
   144  			// take literal character (with quoting if needed)
   145  			regex += regexp.QuoteMeta(string(ch))
   146  		}
   147  	}
   148  
   149  	if currentGroupLevel > 0 {
   150  		return "", fmt.Errorf("missing %d closing brace(s): %q", currentGroupLevel, pattern)
   151  	}
   152  	if inCharClass {
   153  		return "", fmt.Errorf("missing closing bracket ']': %q", pattern)
   154  	}
   155  	if escapeNext {
   156  		return "", fmt.Errorf("expected character after '\\': %q", pattern)
   157  	}
   158  
   159  	regex += "$"
   160  	return regex, nil
   161  }
   162  
   163  func NewPathPattern(pattern string) (*PathPattern, error) {
   164  	regexPattern, err := createRegex(pattern, globDefault)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	regex := regexp.MustCompile(regexPattern)
   170  
   171  	pp := &PathPattern{pattern, regex}
   172  	return pp, nil
   173  }
   174  
   175  func (pp *PathPattern) Matches(path string) bool {
   176  	return pp.regex.MatchString(path)
   177  }