1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Copied from Go distribution src/go/build/build.go, syslist.go.
6// That package does not export the ability to process raw file data,
7// although we could fake it with an appropriate build.Context
8// and a lot of unwrapping.
9// More importantly, that package does not implement the tags["*"]
10// special case, in which both tag and !tag are considered to be true
11// for essentially all tags (except "ignore").
12//
13// If we added this API to go/build directly, we wouldn't need this
14// file anymore, but this API is not terribly general-purpose and we
15// don't really want to commit to any public form of it, nor do we
16// want to move the core parts of go/build into a top-level internal package.
17// These details change very infrequently, so the copy is fine.
18
19package imports
20
21import (
22	"bytes"
23	"cmd/go/internal/cfg"
24	"errors"
25	"fmt"
26	"go/build/constraint"
27	"strings"
28	"unicode"
29)
30
31var (
32	bSlashSlash = []byte("//")
33	bStarSlash  = []byte("*/")
34	bSlashStar  = []byte("/*")
35	bPlusBuild  = []byte("+build")
36
37	goBuildComment = []byte("//go:build")
38
39	errMultipleGoBuild = errors.New("multiple //go:build comments")
40)
41
42func isGoBuildComment(line []byte) bool {
43	if !bytes.HasPrefix(line, goBuildComment) {
44		return false
45	}
46	line = bytes.TrimSpace(line)
47	rest := line[len(goBuildComment):]
48	return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
49}
50
51// ShouldBuild reports whether it is okay to use this file,
52// The rule is that in the file's leading run of // comments
53// and blank lines, which must be followed by a blank line
54// (to avoid including a Go package clause doc comment),
55// lines beginning with '// +build' are taken as build directives.
56//
57// The file is accepted only if each such line lists something
58// matching the file. For example:
59//
60//	// +build windows linux
61//
62// marks the file as applicable only on Windows and Linux.
63//
64// If tags["*"] is true, then ShouldBuild will consider every
65// build tag except "ignore" to be both true and false for
66// the purpose of satisfying build tags, in order to estimate
67// (conservatively) whether a file could ever possibly be used
68// in any build.
69func ShouldBuild(content []byte, tags map[string]bool) bool {
70	// Identify leading run of // comments and blank lines,
71	// which must be followed by a blank line.
72	// Also identify any //go:build comments.
73	content, goBuild, _, err := parseFileHeader(content)
74	if err != nil {
75		return false
76	}
77
78	// If //go:build line is present, it controls.
79	// Otherwise fall back to +build processing.
80	var shouldBuild bool
81	switch {
82	case goBuild != nil:
83		x, err := constraint.Parse(string(goBuild))
84		if err != nil {
85			return false
86		}
87		shouldBuild = eval(x, tags, true)
88
89	default:
90		shouldBuild = true
91		p := content
92		for len(p) > 0 {
93			line := p
94			if i := bytes.IndexByte(line, '\n'); i >= 0 {
95				line, p = line[:i], p[i+1:]
96			} else {
97				p = p[len(p):]
98			}
99			line = bytes.TrimSpace(line)
100			if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
101				continue
102			}
103			text := string(line)
104			if !constraint.IsPlusBuild(text) {
105				continue
106			}
107			if x, err := constraint.Parse(text); err == nil {
108				if !eval(x, tags, true) {
109					shouldBuild = false
110				}
111			}
112		}
113	}
114
115	return shouldBuild
116}
117
118func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
119	end := 0
120	p := content
121	ended := false       // found non-blank, non-// line, so stopped accepting // +build lines
122	inSlashStar := false // in /* */ comment
123
124Lines:
125	for len(p) > 0 {
126		line := p
127		if i := bytes.IndexByte(line, '\n'); i >= 0 {
128			line, p = line[:i], p[i+1:]
129		} else {
130			p = p[len(p):]
131		}
132		line = bytes.TrimSpace(line)
133		if len(line) == 0 && !ended { // Blank line
134			// Remember position of most recent blank line.
135			// When we find the first non-blank, non-// line,
136			// this "end" position marks the latest file position
137			// where a // +build line can appear.
138			// (It must appear _before_ a blank line before the non-blank, non-// line.
139			// Yes, that's confusing, which is part of why we moved to //go:build lines.)
140			// Note that ended==false here means that inSlashStar==false,
141			// since seeing a /* would have set ended==true.
142			end = len(content) - len(p)
143			continue Lines
144		}
145		if !bytes.HasPrefix(line, bSlashSlash) { // Not comment line
146			ended = true
147		}
148
149		if !inSlashStar && isGoBuildComment(line) {
150			if goBuild != nil {
151				return nil, nil, false, errMultipleGoBuild
152			}
153			goBuild = line
154		}
155
156	Comments:
157		for len(line) > 0 {
158			if inSlashStar {
159				if i := bytes.Index(line, bStarSlash); i >= 0 {
160					inSlashStar = false
161					line = bytes.TrimSpace(line[i+len(bStarSlash):])
162					continue Comments
163				}
164				continue Lines
165			}
166			if bytes.HasPrefix(line, bSlashSlash) {
167				continue Lines
168			}
169			if bytes.HasPrefix(line, bSlashStar) {
170				inSlashStar = true
171				line = bytes.TrimSpace(line[len(bSlashStar):])
172				continue Comments
173			}
174			// Found non-comment text.
175			break Lines
176		}
177	}
178
179	return content[:end], goBuild, sawBinaryOnly, nil
180}
181
182// matchTag reports whether the tag name is valid and tags[name] is true.
183// As a special case, if tags["*"] is true and name is not empty or ignore,
184// then matchTag will return prefer instead of the actual answer,
185// which allows the caller to pretend in that case that most tags are
186// both true and false.
187func matchTag(name string, tags map[string]bool, prefer bool) bool {
188	// Tags must be letters, digits, underscores or dots.
189	// Unlike in Go identifiers, all digits are fine (e.g., "386").
190	for _, c := range name {
191		if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
192			return false
193		}
194	}
195
196	if tags["*"] && name != "" && name != "ignore" {
197		// Special case for gathering all possible imports:
198		// if we put * in the tags map then all tags
199		// except "ignore" are considered both present and not
200		// (so we return true no matter how 'want' is set).
201		return prefer
202	}
203
204	if tags[name] {
205		return true
206	}
207
208	switch name {
209	case "linux":
210		return tags["android"]
211	case "solaris":
212		return tags["illumos"]
213	case "darwin":
214		return tags["ios"]
215	case "unix":
216		return unixOS[cfg.BuildContext.GOOS]
217	default:
218		return false
219	}
220}
221
222// eval is like
223//
224//	x.Eval(func(tag string) bool { return matchTag(tag, tags) })
225//
226// except that it implements the special case for tags["*"] meaning
227// all tags are both true and false at the same time.
228func eval(x constraint.Expr, tags map[string]bool, prefer bool) bool {
229	switch x := x.(type) {
230	case *constraint.TagExpr:
231		return matchTag(x.Tag, tags, prefer)
232	case *constraint.NotExpr:
233		return !eval(x.X, tags, !prefer)
234	case *constraint.AndExpr:
235		return eval(x.X, tags, prefer) && eval(x.Y, tags, prefer)
236	case *constraint.OrExpr:
237		return eval(x.X, tags, prefer) || eval(x.Y, tags, prefer)
238	}
239	panic(fmt.Sprintf("unexpected constraint expression %T", x))
240}
241
242// Eval is like
243//
244//	x.Eval(func(tag string) bool { return matchTag(tag, tags) })
245//
246// except that it implements the special case for tags["*"] meaning
247// all tags are both true and false at the same time.
248func Eval(x constraint.Expr, tags map[string]bool, prefer bool) bool {
249	return eval(x, tags, prefer)
250}
251
252// MatchFile returns false if the name contains a $GOOS or $GOARCH
253// suffix which does not match the current system.
254// The recognized name formats are:
255//
256//	name_$(GOOS).*
257//	name_$(GOARCH).*
258//	name_$(GOOS)_$(GOARCH).*
259//	name_$(GOOS)_test.*
260//	name_$(GOARCH)_test.*
261//	name_$(GOOS)_$(GOARCH)_test.*
262//
263// Exceptions:
264//
265//	if GOOS=android, then files with GOOS=linux are also matched.
266//	if GOOS=illumos, then files with GOOS=solaris are also matched.
267//	if GOOS=ios, then files with GOOS=darwin are also matched.
268//
269// If tags["*"] is true, then MatchFile will consider all possible
270// GOOS and GOARCH to be available and will consequently
271// always return true.
272func MatchFile(name string, tags map[string]bool) bool {
273	if tags["*"] {
274		return true
275	}
276	if dot := strings.Index(name, "."); dot != -1 {
277		name = name[:dot]
278	}
279
280	// Before Go 1.4, a file called "linux.go" would be equivalent to having a
281	// build tag "linux" in that file. For Go 1.4 and beyond, we require this
282	// auto-tagging to apply only to files with a non-empty prefix, so
283	// "foo_linux.go" is tagged but "linux.go" is not. This allows new operating
284	// systems, such as android, to arrive without breaking existing code with
285	// innocuous source code in "android.go". The easiest fix: cut everything
286	// in the name before the initial _.
287	i := strings.Index(name, "_")
288	if i < 0 {
289		return true
290	}
291	name = name[i:] // ignore everything before first _
292
293	l := strings.Split(name, "_")
294	if n := len(l); n > 0 && l[n-1] == "test" {
295		l = l[:n-1]
296	}
297	n := len(l)
298	if n >= 2 && KnownOS[l[n-2]] && KnownArch[l[n-1]] {
299		return matchTag(l[n-2], tags, true) && matchTag(l[n-1], tags, true)
300	}
301	if n >= 1 && KnownOS[l[n-1]] {
302		return matchTag(l[n-1], tags, true)
303	}
304	if n >= 1 && KnownArch[l[n-1]] {
305		return matchTag(l[n-1], tags, true)
306	}
307	return true
308}
309
310var KnownOS = map[string]bool{
311	"aix":       true,
312	"android":   true,
313	"darwin":    true,
314	"dragonfly": true,
315	"freebsd":   true,
316	"hurd":      true,
317	"illumos":   true,
318	"ios":       true,
319	"js":        true,
320	"linux":     true,
321	"nacl":      true, // legacy; don't remove
322	"netbsd":    true,
323	"openbsd":   true,
324	"plan9":     true,
325	"solaris":   true,
326	"wasip1":    true,
327	"windows":   true,
328	"zos":       true,
329}
330
331// unixOS is the set of GOOS values matched by the "unix" build tag.
332// This is not used for filename matching.
333// This is the same list as in go/build/syslist.go and cmd/dist/build.go.
334var unixOS = map[string]bool{
335	"aix":       true,
336	"android":   true,
337	"darwin":    true,
338	"dragonfly": true,
339	"freebsd":   true,
340	"hurd":      true,
341	"illumos":   true,
342	"ios":       true,
343	"linux":     true,
344	"netbsd":    true,
345	"openbsd":   true,
346	"solaris":   true,
347}
348
349var KnownArch = map[string]bool{
350	"386":         true,
351	"amd64":       true,
352	"amd64p32":    true, // legacy; don't remove
353	"arm":         true,
354	"armbe":       true,
355	"arm64":       true,
356	"arm64be":     true,
357	"ppc64":       true,
358	"ppc64le":     true,
359	"mips":        true,
360	"mipsle":      true,
361	"mips64":      true,
362	"mips64le":    true,
363	"mips64p32":   true,
364	"mips64p32le": true,
365	"loong64":     true,
366	"ppc":         true,
367	"riscv":       true,
368	"riscv64":     true,
369	"s390":        true,
370	"s390x":       true,
371	"sparc":       true,
372	"sparc64":     true,
373	"wasm":        true,
374}
375