• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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