1// Copyright 2019 Google Inc. All rights reserved.
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
15package main
16
17import (
18	"bufio"
19	"bytes"
20	"encoding/json"
21	"io"
22	"os"
23	"regexp"
24	"strings"
25	"unicode"
26)
27
28type allowList struct {
29	path                string
30	ignoreMatchingLines []string
31}
32
33func parseAllowLists(allowLists []string, allowListFiles []string) ([]allowList, error) {
34	var ret []allowList
35
36	add := func(path string, ignoreMatchingLines []string) {
37		for _, x := range ret {
38			if x.path == path {
39				x.ignoreMatchingLines = append(x.ignoreMatchingLines, ignoreMatchingLines...)
40				return
41			}
42		}
43
44		ret = append(ret, allowList{
45			path:                path,
46			ignoreMatchingLines: ignoreMatchingLines,
47		})
48	}
49
50	for _, file := range allowListFiles {
51		newAllowlists, err := parseAllowListFile(file)
52		if err != nil {
53			return nil, err
54		}
55
56		for _, w := range newAllowlists {
57			add(w.path, w.ignoreMatchingLines)
58		}
59	}
60
61	for _, s := range allowLists {
62		colon := strings.IndexRune(s, ':')
63		var ignoreMatchingLines []string
64		if colon >= 0 {
65			ignoreMatchingLines = []string{s[colon+1:]}
66		}
67		add(s, ignoreMatchingLines)
68	}
69
70	return ret, nil
71}
72
73func parseAllowListFile(file string) ([]allowList, error) {
74	r, err := os.Open(file)
75	if err != nil {
76		return nil, err
77	}
78	defer r.Close()
79
80	d := json.NewDecoder(newJSONCommentStripper(r))
81
82	var jsonAllowLists []struct {
83		Paths               []string
84		IgnoreMatchingLines []string
85	}
86
87	if err := d.Decode(&jsonAllowLists); err != nil {
88		return nil, err
89	}
90
91	var allowLists []allowList
92	for _, w := range jsonAllowLists {
93		for _, p := range w.Paths {
94			allowLists = append(allowLists, allowList{
95				path:                p,
96				ignoreMatchingLines: w.IgnoreMatchingLines,
97			})
98		}
99	}
100
101	return allowLists, err
102}
103
104func filterModifiedPaths(l [][2]*ZipArtifactFile, allowLists []allowList) ([][2]*ZipArtifactFile, error) {
105outer:
106	for i := 0; i < len(l); i++ {
107		for _, w := range allowLists {
108			if match, err := Match(w.path, l[i][0].Name); err != nil {
109				return l, err
110			} else if match {
111				if match, err := diffIgnoringMatchingLines(l[i][0], l[i][1], w.ignoreMatchingLines); err != nil {
112					return l, err
113				} else if match || len(w.ignoreMatchingLines) == 0 {
114					l = append(l[:i], l[i+1:]...)
115					i--
116				}
117				continue outer
118			}
119		}
120	}
121
122	if len(l) == 0 {
123		l = nil
124	}
125
126	return l, nil
127}
128
129func filterNewPaths(l []*ZipArtifactFile, allowLists []allowList) ([]*ZipArtifactFile, error) {
130outer:
131	for i := 0; i < len(l); i++ {
132		for _, w := range allowLists {
133			if match, err := Match(w.path, l[i].Name); err != nil {
134				return l, err
135			} else if match && len(w.ignoreMatchingLines) == 0 {
136				l = append(l[:i], l[i+1:]...)
137				i--
138			}
139			continue outer
140		}
141	}
142
143	if len(l) == 0 {
144		l = nil
145	}
146
147	return l, nil
148}
149
150func diffIgnoringMatchingLines(a *ZipArtifactFile, b *ZipArtifactFile, ignoreMatchingLines []string) (match bool, err error) {
151	lineMatchesIgnores := func(b []byte) (bool, error) {
152		for _, m := range ignoreMatchingLines {
153			if match, err := regexp.Match(m, b); err != nil {
154				return false, err
155			} else if match {
156				return match, nil
157			}
158		}
159		return false, nil
160	}
161
162	filter := func(z *ZipArtifactFile) ([]byte, error) {
163		var ret []byte
164
165		r, err := z.Open()
166		if err != nil {
167			return nil, err
168		}
169		s := bufio.NewScanner(r)
170
171		for s.Scan() {
172			if match, err := lineMatchesIgnores(s.Bytes()); err != nil {
173				return nil, err
174			} else if !match {
175				ret = append(ret, "\n"...)
176				ret = append(ret, s.Bytes()...)
177			}
178		}
179
180		return ret, nil
181	}
182
183	bufA, err := filter(a)
184	if err != nil {
185		return false, err
186	}
187	bufB, err := filter(b)
188	if err != nil {
189		return false, err
190	}
191
192	return bytes.Compare(bufA, bufB) == 0, nil
193}
194
195func applyAllowLists(diff zipDiff, allowLists []allowList) (zipDiff, error) {
196	var err error
197
198	diff.modified, err = filterModifiedPaths(diff.modified, allowLists)
199	if err != nil {
200		return diff, err
201	}
202	diff.onlyInA, err = filterNewPaths(diff.onlyInA, allowLists)
203	if err != nil {
204		return diff, err
205	}
206	diff.onlyInB, err = filterNewPaths(diff.onlyInB, allowLists)
207	if err != nil {
208		return diff, err
209	}
210
211	return diff, nil
212}
213
214func newJSONCommentStripper(r io.Reader) *jsonCommentStripper {
215	return &jsonCommentStripper{
216		r: bufio.NewReader(r),
217	}
218}
219
220type jsonCommentStripper struct {
221	r   *bufio.Reader
222	b   []byte
223	err error
224}
225
226func (j *jsonCommentStripper) Read(buf []byte) (int, error) {
227	for len(j.b) == 0 {
228		if j.err != nil {
229			return 0, j.err
230		}
231
232		j.b, j.err = j.r.ReadBytes('\n')
233
234		if isComment(j.b) {
235			j.b = nil
236		}
237	}
238
239	n := copy(buf, j.b)
240	j.b = j.b[n:]
241	return n, nil
242}
243
244var commentPrefix = []byte("//")
245
246func isComment(b []byte) bool {
247	for len(b) > 0 && unicode.IsSpace(rune(b[0])) {
248		b = b[1:]
249	}
250	return bytes.HasPrefix(b, commentPrefix)
251}
252