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
7package imports
8
9import (
10	"bytes"
11	"strings"
12	"unicode"
13)
14
15var slashslash = []byte("//")
16
17// ShouldBuild reports whether it is okay to use this file,
18// The rule is that in the file's leading run of // comments
19// and blank lines, which must be followed by a blank line
20// (to avoid including a Go package clause doc comment),
21// lines beginning with '// +build' are taken as build directives.
22//
23// The file is accepted only if each such line lists something
24// matching the file. For example:
25//
26//	// +build windows linux
27//
28// marks the file as applicable only on Windows and Linux.
29//
30// If tags["*"] is true, then ShouldBuild will consider every
31// build tag except "ignore" to be both true and false for
32// the purpose of satisfying build tags, in order to estimate
33// (conservatively) whether a file could ever possibly be used
34// in any build.
35//
36func ShouldBuild(content []byte, tags map[string]bool) bool {
37	// Pass 1. Identify leading run of // comments and blank lines,
38	// which must be followed by a blank line.
39	end := 0
40	p := content
41	for len(p) > 0 {
42		line := p
43		if i := bytes.IndexByte(line, '\n'); i >= 0 {
44			line, p = line[:i], p[i+1:]
45		} else {
46			p = p[len(p):]
47		}
48		line = bytes.TrimSpace(line)
49		if len(line) == 0 { // Blank line
50			end = len(content) - len(p)
51			continue
52		}
53		if !bytes.HasPrefix(line, slashslash) { // Not comment line
54			break
55		}
56	}
57	content = content[:end]
58
59	// Pass 2.  Process each line in the run.
60	p = content
61	allok := true
62	for len(p) > 0 {
63		line := p
64		if i := bytes.IndexByte(line, '\n'); i >= 0 {
65			line, p = line[:i], p[i+1:]
66		} else {
67			p = p[len(p):]
68		}
69		line = bytes.TrimSpace(line)
70		if !bytes.HasPrefix(line, slashslash) {
71			continue
72		}
73		line = bytes.TrimSpace(line[len(slashslash):])
74		if len(line) > 0 && line[0] == '+' {
75			// Looks like a comment +line.
76			f := strings.Fields(string(line))
77			if f[0] == "+build" {
78				ok := false
79				for _, tok := range f[1:] {
80					if matchTags(tok, tags) {
81						ok = true
82					}
83				}
84				if !ok {
85					allok = false
86				}
87			}
88		}
89	}
90
91	return allok
92}
93
94// matchTags reports whether the name is one of:
95//
96//	tag (if tags[tag] is true)
97//	!tag (if tags[tag] is false)
98//	a comma-separated list of any of these
99//
100func matchTags(name string, tags map[string]bool) bool {
101	if name == "" {
102		return false
103	}
104	if i := strings.Index(name, ","); i >= 0 {
105		// comma-separated list
106		ok1 := matchTags(name[:i], tags)
107		ok2 := matchTags(name[i+1:], tags)
108		return ok1 && ok2
109	}
110	if strings.HasPrefix(name, "!!") { // bad syntax, reject always
111		return false
112	}
113	if strings.HasPrefix(name, "!") { // negation
114		return len(name) > 1 && matchTag(name[1:], tags, false)
115	}
116	return matchTag(name, tags, true)
117}
118
119// matchTag reports whether the tag name is valid and satisfied by tags[name]==want.
120func matchTag(name string, tags map[string]bool, want bool) bool {
121	// Tags must be letters, digits, underscores or dots.
122	// Unlike in Go identifiers, all digits are fine (e.g., "386").
123	for _, c := range name {
124		if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
125			return false
126		}
127	}
128
129	if tags["*"] && name != "" && name != "ignore" {
130		// Special case for gathering all possible imports:
131		// if we put * in the tags map then all tags
132		// except "ignore" are considered both present and not
133		// (so we return true no matter how 'want' is set).
134		return true
135	}
136
137	have := tags[name]
138	if name == "linux" {
139		have = have || tags["android"]
140	}
141	if name == "solaris" {
142		have = have || tags["illumos"]
143	}
144	return have == want
145}
146
147// MatchFile returns false if the name contains a $GOOS or $GOARCH
148// suffix which does not match the current system.
149// The recognized name formats are:
150//
151//     name_$(GOOS).*
152//     name_$(GOARCH).*
153//     name_$(GOOS)_$(GOARCH).*
154//     name_$(GOOS)_test.*
155//     name_$(GOARCH)_test.*
156//     name_$(GOOS)_$(GOARCH)_test.*
157//
158// Exceptions:
159//     if GOOS=android, then files with GOOS=linux are also matched.
160//     if GOOS=illumos, then files with GOOS=solaris are also matched.
161//
162// If tags["*"] is true, then MatchFile will consider all possible
163// GOOS and GOARCH to be available and will consequently
164// always return true.
165func MatchFile(name string, tags map[string]bool) bool {
166	if tags["*"] {
167		return true
168	}
169	if dot := strings.Index(name, "."); dot != -1 {
170		name = name[:dot]
171	}
172
173	// Before Go 1.4, a file called "linux.go" would be equivalent to having a
174	// build tag "linux" in that file. For Go 1.4 and beyond, we require this
175	// auto-tagging to apply only to files with a non-empty prefix, so
176	// "foo_linux.go" is tagged but "linux.go" is not. This allows new operating
177	// systems, such as android, to arrive without breaking existing code with
178	// innocuous source code in "android.go". The easiest fix: cut everything
179	// in the name before the initial _.
180	i := strings.Index(name, "_")
181	if i < 0 {
182		return true
183	}
184	name = name[i:] // ignore everything before first _
185
186	l := strings.Split(name, "_")
187	if n := len(l); n > 0 && l[n-1] == "test" {
188		l = l[:n-1]
189	}
190	n := len(l)
191	if n >= 2 && KnownOS[l[n-2]] && KnownArch[l[n-1]] {
192		return matchTag(l[n-2], tags, true) && matchTag(l[n-1], tags, true)
193	}
194	if n >= 1 && KnownOS[l[n-1]] {
195		return matchTag(l[n-1], tags, true)
196	}
197	if n >= 1 && KnownArch[l[n-1]] {
198		return matchTag(l[n-1], tags, true)
199	}
200	return true
201}
202
203var KnownOS = map[string]bool{
204	"aix":       true,
205	"android":   true,
206	"darwin":    true,
207	"dragonfly": true,
208	"freebsd":   true,
209	"hurd":      true,
210	"illumos":   true,
211	"js":        true,
212	"linux":     true,
213	"nacl":      true, // legacy; don't remove
214	"netbsd":    true,
215	"openbsd":   true,
216	"plan9":     true,
217	"solaris":   true,
218	"windows":   true,
219	"zos":       true,
220}
221
222var KnownArch = map[string]bool{
223	"386":         true,
224	"amd64":       true,
225	"amd64p32":    true, // legacy; don't remove
226	"arm":         true,
227	"armbe":       true,
228	"arm64":       true,
229	"arm64be":     true,
230	"ppc64":       true,
231	"ppc64le":     true,
232	"mips":        true,
233	"mipsle":      true,
234	"mips64":      true,
235	"mips64le":    true,
236	"mips64p32":   true,
237	"mips64p32le": true,
238	"ppc":         true,
239	"riscv":       true,
240	"riscv64":     true,
241	"s390":        true,
242	"s390x":       true,
243	"sparc":       true,
244	"sparc64":     true,
245	"wasm":        true,
246}
247