1// Copyright 2020 Google LLC 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// https://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 glob provides file globbing utilities 16package glob 17 18import ( 19 "bytes" 20 "encoding/json" 21 "fmt" 22 "io/ioutil" 23 "os" 24 "path/filepath" 25 "strings" 26 27 "dawn.googlesource.com/tint/tools/src/match" 28) 29 30// Scan walks all files and subdirectories from root, returning those 31// that Config.shouldExamine() returns true for. 32func Scan(root string, cfg Config) ([]string, error) { 33 files := []string{} 34 err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 35 rel, err := filepath.Rel(root, path) 36 if err != nil { 37 rel = path 38 } 39 40 if rel == ".git" { 41 return filepath.SkipDir 42 } 43 44 if !cfg.shouldExamine(root, path) { 45 return nil 46 } 47 48 if !info.IsDir() { 49 files = append(files, rel) 50 } 51 52 return nil 53 }) 54 if err != nil { 55 return nil, err 56 } 57 return files, nil 58} 59 60// Configs is a slice of Config. 61type Configs []Config 62 63// Config is used to parse the JSON configuration file. 64type Config struct { 65 // Paths holds a number of JSON objects that contain either a "includes" or 66 // "excludes" key to an array of path patterns. 67 // Each path pattern is considered in turn to either include or exclude the 68 // file path for license scanning. Pattern use forward-slashes '/' for 69 // directory separators, and may use the following wildcards: 70 // ? - matches any single non-separator character 71 // * - matches any sequence of non-separator characters 72 // ** - matches any sequence of characters including separators 73 // 74 // Rules are processed in the order in which they are declared, with later 75 // rules taking precedence over earlier rules. 76 // 77 // All files are excluded before the first rule is evaluated. 78 // 79 // Example: 80 // 81 // { 82 // "paths": [ 83 // { "exclude": [ "out/*", "build/*" ] }, 84 // { "include": [ "out/foo.txt" ] } 85 // ], 86 // } 87 Paths searchRules 88} 89 90// LoadConfig loads a config file at path. 91func LoadConfig(path string) (Config, error) { 92 cfgBody, err := ioutil.ReadFile(path) 93 if err != nil { 94 return Config{}, err 95 } 96 return ParseConfig(string(cfgBody)) 97} 98 99// ParseConfig parses the config from a JSON string. 100func ParseConfig(config string) (Config, error) { 101 d := json.NewDecoder(strings.NewReader(config)) 102 cfg := Config{} 103 if err := d.Decode(&cfg); err != nil { 104 return Config{}, err 105 } 106 return cfg, nil 107} 108 109// MustParseConfig parses the config from a JSON string, panicing if the config 110// does not parse 111func MustParseConfig(config string) Config { 112 d := json.NewDecoder(strings.NewReader(config)) 113 cfg := Config{} 114 if err := d.Decode(&cfg); err != nil { 115 panic(fmt.Errorf("Failed to parse config: %w\nConfig:\n%v", err, config)) 116 } 117 return cfg 118} 119 120// rule is a search path predicate. 121// root is the project relative path. 122// cond is the value to return if the rule doesn't either include or exclude. 123type rule func(path string, cond bool) bool 124 125// searchRules is a ordered list of search rules. 126// searchRules is its own type as it has to perform custom JSON unmarshalling. 127type searchRules []rule 128 129// UnmarshalJSON unmarshals the array of rules in the form: 130// { "include": [ ... ] } or { "exclude": [ ... ] } 131func (l *searchRules) UnmarshalJSON(body []byte) error { 132 type parsed struct { 133 Include []string 134 Exclude []string 135 } 136 137 p := []parsed{} 138 if err := json.NewDecoder(bytes.NewReader(body)).Decode(&p); err != nil { 139 return err 140 } 141 142 *l = searchRules{} 143 for _, rule := range p { 144 rule := rule 145 switch { 146 case len(rule.Include) > 0 && len(rule.Exclude) > 0: 147 return fmt.Errorf("Rule cannot contain both include and exclude") 148 case len(rule.Include) > 0: 149 tests := make([]match.Test, len(rule.Include)) 150 for i, pattern := range rule.Include { 151 test, err := match.New(pattern) 152 if err != nil { 153 return err 154 } 155 tests[i] = test 156 } 157 *l = append(*l, func(path string, cond bool) bool { 158 for _, test := range tests { 159 if test(path) { 160 return true 161 } 162 } 163 return cond 164 }) 165 case len(rule.Exclude) > 0: 166 tests := make([]match.Test, len(rule.Exclude)) 167 for i, pattern := range rule.Exclude { 168 test, err := match.New(pattern) 169 if err != nil { 170 return err 171 } 172 tests[i] = test 173 } 174 *l = append(*l, func(path string, cond bool) bool { 175 for _, test := range tests { 176 if test(path) { 177 return false 178 } 179 } 180 return cond 181 }) 182 } 183 } 184 return nil 185} 186 187// shouldExamine returns true if the file at absPath should be scanned. 188func (c Config) shouldExamine(root, absPath string) bool { 189 root = filepath.ToSlash(root) // Canonicalize 190 absPath = filepath.ToSlash(absPath) // Canonicalize 191 relPath, err := filepath.Rel(root, absPath) 192 if err != nil { 193 return false 194 } 195 relPath = filepath.ToSlash(relPath) // Canonicalize 196 197 res := false 198 for _, rule := range c.Paths { 199 res = rule(relPath, res) 200 } 201 202 return res 203} 204