go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/authdb/internal/globset/globset.go (about) 1 // Copyright 2019 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package globset preprocesses []identity.Glob for faster querying. 16 package globset 17 18 import ( 19 "regexp" 20 "sort" 21 "strings" 22 23 "go.chromium.org/luci/auth/identity" 24 "go.chromium.org/luci/common/data/stringset" 25 ) 26 27 // GlobSet encodes same set of identities as some []identity.Glob. 28 // 29 // For each possible identity type it has a single regexp against an identity 30 // value which tests whether it is in the set or not. 31 // 32 // Construct it via Builder. 33 type GlobSet map[identity.Kind]*regexp.Regexp 34 35 // Has checks whether 'id' is in the set. 36 // 37 // Malformed identities are considered to be not in the set. 'nil' GlobSet is 38 // considered empty. 39 func (gs GlobSet) Has(id identity.Identity) bool { 40 if gs == nil { 41 return false 42 } 43 sepIdx := strings.IndexRune(string(id), ':') 44 if sepIdx == -1 { 45 return false 46 } 47 kind, name := identity.Kind(id[:sepIdx]), string(id[sepIdx+1:]) 48 if strings.ContainsRune(name, '\n') { 49 // Our regexps are not in multi-line mode, reject '\n' explicitly. Note that 50 // there should not be '\n' there in the first place, this is just an extra 51 // precaution. 52 return false 53 } 54 if re := gs[kind]; re != nil { 55 return re.MatchString(name) 56 } 57 return false 58 } 59 60 // Builder builds GlobSet from individual globs. 61 // 62 // Keeps the cache of compiled regexps internally. 63 type Builder struct { 64 kinds map[identity.Kind]stringset.Set // identity kind => set of matching regexps 65 reCache map[string]*regexp.Regexp // cache of compiled regexps 66 } 67 68 // NewBuilder constructs new Builder instance. 69 func NewBuilder() *Builder { 70 return &Builder{ 71 kinds: map[identity.Kind]stringset.Set{}, 72 reCache: map[string]*regexp.Regexp{}, 73 } 74 } 75 76 // Reset prepares the builder for building a new GlobSet. 77 // 78 // Keeps the cache of compiled regexps. 79 func (b *Builder) Reset() { 80 b.kinds = map[identity.Kind]stringset.Set{} 81 } 82 83 // Add adds a glob to the set being constructed. 84 // 85 // Returns an error if the glob is malformed. 86 func (b *Builder) Add(g identity.Glob) error { 87 kind, regexp, err := g.Preprocess() 88 if err != nil { 89 return err 90 } 91 ss, ok := b.kinds[kind] 92 if !ok { 93 ss = stringset.New(1) 94 b.kinds[kind] = ss 95 } 96 ss.Add(regexp) 97 return nil 98 } 99 100 // Build returns the fully constructed set or nil if it is empty. 101 // 102 // Returns an error if some glob can't be compiled into a regexp. 103 func (b *Builder) Build() (GlobSet, error) { 104 if len(b.kinds) == 0 { 105 return nil, nil 106 } 107 gs := make(GlobSet, len(b.kinds)) 108 for kind, ss := range b.kinds { 109 var err error 110 if gs[kind], err = b.toRegexp(ss); err != nil { 111 return nil, err 112 } 113 } 114 return gs, nil 115 } 116 117 // toRegexp returns compiled "|".join(sorted(elems)). 118 func (b *Builder) toRegexp(elems stringset.Set) (*regexp.Regexp, error) { 119 // Note that s[i] has form '^...$' (this is also checked by tests). 120 s := elems.ToSlice() 121 sort.Strings(s) 122 123 uberRe := "" 124 switch len(s) { 125 case 0: 126 panic("impossible") 127 case 1: 128 uberRe = s[0] 129 default: 130 sb := strings.Builder{} 131 sb.WriteString("^(") 132 for i, r := range s { 133 if i != 0 { 134 sb.WriteRune('|') 135 } 136 sb.WriteRune('(') 137 sb.WriteString(r[1 : len(r)-1]) 138 sb.WriteRune(')') 139 } 140 sb.WriteString(")$") 141 uberRe = sb.String() 142 } 143 144 if re := b.reCache[uberRe]; re != nil { 145 return re, nil 146 } 147 148 re, err := regexp.Compile(uberRe) 149 if err != nil { 150 return nil, err 151 } 152 153 b.reCache[uberRe] = re 154 return re, nil 155 }