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
|
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2024 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 patterns
import (
"errors"
"fmt"
"io"
"regexp"
"strings"
)
type tokenType int
const (
tokEOF tokenType = iota
tokText
tokBraceOpen
tokBraceClose
tokComma
)
// String is used for debugging purposes only.
func (t tokenType) String() string {
switch t {
case tokEOF:
return "end-of-file"
case tokText:
return "text"
case tokBraceOpen:
return "brace-open"
case tokBraceClose:
return "brace-close"
case tokComma:
return "comma"
default:
return "?"
}
}
type token struct {
tType tokenType
text string
}
// relpathFinder matches `/./` and `/../` along with their trailing variants `/.` and `/..` in path patterns.
var relpathFinder = regexp.MustCompile(`/\.(\.)?(/|$)`)
func scan(text string) (tokens []token, err error) {
if len(text) == 0 {
return nil, errors.New("pattern has length 0")
}
if text[0] != '/' {
return nil, errors.New("pattern must start with '/'")
}
if relpathFinder.MatchString(text) {
return nil, errors.New("pattern cannot contain '/./' or '/../'")
}
var runes []rune
consumeText := func() {
if len(runes) > 0 {
tokens = append(tokens, token{text: string(runes), tType: tokText})
runes = nil
}
}
rr := strings.NewReader(text)
loop:
for {
r, _, err := rr.ReadRune()
if err != nil {
if errors.Is(err, io.EOF) {
break loop
}
// Should not occur, err is only set if no rune available to read
return nil, fmt.Errorf("internal error: failed to read rune while scanning path pattern: %w", err)
}
switch r {
case '{':
consumeText()
tokens = append(tokens, token{text: string(r), tType: tokBraceOpen})
case '}':
consumeText()
tokens = append(tokens, token{text: string(r), tType: tokBraceClose})
case ',':
consumeText()
tokens = append(tokens, token{text: string(r), tType: tokComma})
case '\\':
r2, _, err := rr.ReadRune()
if err != nil {
return nil, errors.New(`trailing unescaped '\' character`)
}
runes = append(runes, r, r2)
case '[', ']':
return nil, errors.New("cannot contain unescaped '[' or ']' character")
default:
runes = append(runes, r)
}
}
consumeText()
return tokens, nil
}
|