1// Copyright 2014 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 blueprint
16
17import (
18	"bytes"
19	"fmt"
20	"io"
21	"slices"
22	"strings"
23)
24
25const eof = -1
26
27var (
28	defaultEscaper = strings.NewReplacer(
29		"\n", "$\n")
30	inputEscaper = strings.NewReplacer(
31		"\n", "$\n",
32		" ", "$ ")
33	outputEscaper = strings.NewReplacer(
34		"\n", "$\n",
35		" ", "$ ",
36		":", "$:")
37)
38
39// ninjaString contains the parsed result of a string that can contain references to variables (e.g. $cflags) that will
40// be propagated to the build.ninja file.  For literal strings with no variable references, the variables field will be
41// nil. For strings with variable references str contains the original, unparsed string, and variables contains a
42// pointer to a list of references, each with a span of bytes they should replace and a Variable interface.
43type ninjaString struct {
44	str       string
45	variables *[]variableReference
46}
47
48// variableReference contains information about a single reference to a variable (e.g. $cflags) inside a parsed
49// ninjaString.  start and end are int32 to reduce memory usage.  A nil variable is a special case of an inserted '$'
50// at the beginning of the string to handle leading whitespace that must not be stripped by ninja.
51type variableReference struct {
52	// start is the offset of the '$' character from the beginning of the unparsed string.
53	start int32
54
55	// end is the offset of the character _after_ the final character of the variable name (or '}' if using the
56	//'${}'  syntax)
57	end int32
58
59	variable Variable
60}
61
62type scope interface {
63	LookupVariable(name string) (Variable, error)
64	IsRuleVisible(rule Rule) bool
65	IsPoolVisible(pool Pool) bool
66}
67
68func simpleNinjaString(str string) *ninjaString {
69	return &ninjaString{str: str}
70}
71
72type parseState struct {
73	scope        scope
74	str          string
75	varStart     int
76	varNameStart int
77	result       *ninjaString
78}
79
80func (ps *parseState) pushVariable(start, end int, v Variable) {
81	if ps.result.variables == nil {
82		ps.result.variables = &[]variableReference{{start: int32(start), end: int32(end), variable: v}}
83	} else {
84		*ps.result.variables = append(*ps.result.variables, variableReference{start: int32(start), end: int32(end), variable: v})
85	}
86}
87
88type stateFunc func(*parseState, int, rune) (stateFunc, error)
89
90// parseNinjaString parses an unescaped ninja string (i.e. all $<something>
91// occurrences are expected to be variables or $$) and returns a *ninjaString
92// that contains the original string and a list of the referenced variables.
93func parseNinjaString(scope scope, str string) (*ninjaString, error) {
94	ninjaString, str, err := parseNinjaOrSimpleString(scope, str)
95	if err != nil {
96		return nil, err
97	}
98	if ninjaString != nil {
99		return ninjaString, nil
100	}
101	return simpleNinjaString(str), nil
102}
103
104// parseNinjaOrSimpleString parses an unescaped ninja string (i.e. all $<something>
105// occurrences are expected to be variables or $$) and returns either a *ninjaString
106// if the string contains ninja variable references, or the original string and nil
107// for the *ninjaString if it doesn't.
108func parseNinjaOrSimpleString(scope scope, str string) (*ninjaString, string, error) {
109	// naively pre-allocate slice by counting $ signs
110	n := strings.Count(str, "$")
111	if n == 0 {
112		if len(str) > 0 && str[0] == ' ' {
113			str = "$" + str
114		}
115		return nil, str, nil
116	}
117	variableReferences := make([]variableReference, 0, n)
118	result := &ninjaString{
119		str:       str,
120		variables: &variableReferences,
121	}
122
123	parseState := &parseState{
124		scope:  scope,
125		str:    str,
126		result: result,
127	}
128
129	state := parseFirstRuneState
130	var err error
131	for i := 0; i < len(str); i++ {
132		r := rune(str[i])
133		state, err = state(parseState, i, r)
134		if err != nil {
135			return nil, "", fmt.Errorf("error parsing ninja string %q: %s", str, err)
136		}
137	}
138
139	_, err = state(parseState, len(parseState.str), eof)
140	if err != nil {
141		return nil, "", err
142	}
143
144	// All the '$' characters counted initially could have been "$$" escapes, leaving no
145	// variable references.  Deallocate the variables slice if so.
146	if len(*result.variables) == 0 {
147		result.variables = nil
148	}
149
150	return result, "", nil
151}
152
153func parseFirstRuneState(state *parseState, i int, r rune) (stateFunc, error) {
154	if r == ' ' {
155		state.pushVariable(0, 1, nil)
156	}
157	return parseStringState(state, i, r)
158}
159
160func parseStringState(state *parseState, i int, r rune) (stateFunc, error) {
161	switch {
162	case r == '$':
163		state.varStart = i
164		return parseDollarStartState, nil
165
166	case r == eof:
167		return nil, nil
168
169	default:
170		return parseStringState, nil
171	}
172}
173
174func parseDollarStartState(state *parseState, i int, r rune) (stateFunc, error) {
175	switch {
176	case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
177		r >= '0' && r <= '9', r == '_', r == '-':
178		// The beginning of a of the variable name.
179		state.varNameStart = i
180		return parseDollarState, nil
181
182	case r == '$':
183		// Just a "$$".  Go back to parseStringState.
184		return parseStringState, nil
185
186	case r == '{':
187		// This is a bracketted variable name (e.g. "${blah.blah}").
188		state.varNameStart = i + 1
189		return parseBracketsState, nil
190
191	case r == eof:
192		return nil, fmt.Errorf("unexpected end of string after '$'")
193
194	default:
195		// This was some arbitrary character following a dollar sign,
196		// which is not allowed.
197		return nil, fmt.Errorf("invalid character after '$' at byte "+
198			"offset %d", i)
199	}
200}
201
202func parseDollarState(state *parseState, i int, r rune) (stateFunc, error) {
203	switch {
204	case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
205		r >= '0' && r <= '9', r == '_', r == '-':
206		// A part of the variable name.  Keep going.
207		return parseDollarState, nil
208	}
209
210	// The variable name has ended, output what we have.
211	v, err := state.scope.LookupVariable(state.str[state.varNameStart:i])
212	if err != nil {
213		return nil, err
214	}
215
216	state.pushVariable(state.varStart, i, v)
217
218	switch {
219	case r == '$':
220		// A dollar after the variable name (e.g. "$blah$").  Start a new one.
221		state.varStart = i
222		return parseDollarStartState, nil
223
224	case r == eof:
225		return nil, nil
226
227	default:
228		return parseStringState, nil
229	}
230}
231
232func parseBracketsState(state *parseState, i int, r rune) (stateFunc, error) {
233	switch {
234	case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
235		r >= '0' && r <= '9', r == '_', r == '-', r == '.':
236		// A part of the variable name.  Keep going.
237		return parseBracketsState, nil
238
239	case r == '}':
240		if state.varNameStart == i {
241			// The brackets were immediately closed.  That's no good.
242			return nil, fmt.Errorf("empty variable name at byte offset %d",
243				i)
244		}
245
246		// This is the end of the variable name.
247		v, err := state.scope.LookupVariable(state.str[state.varNameStart:i])
248		if err != nil {
249			return nil, err
250		}
251
252		state.pushVariable(state.varStart, i+1, v)
253		return parseStringState, nil
254
255	case r == eof:
256		return nil, fmt.Errorf("unexpected end of string in variable name")
257
258	default:
259		// This character isn't allowed in a variable name.
260		return nil, fmt.Errorf("invalid character in variable name at "+
261			"byte offset %d", i)
262	}
263}
264
265// parseNinjaStrings converts a list of strings to *ninjaStrings by finding the references
266// to ninja variables contained in the strings.
267func parseNinjaStrings(scope scope, strs []string) ([]*ninjaString,
268	error) {
269
270	if len(strs) == 0 {
271		return nil, nil
272	}
273	result := make([]*ninjaString, len(strs))
274	for i, str := range strs {
275		ninjaStr, err := parseNinjaString(scope, str)
276		if err != nil {
277			return nil, fmt.Errorf("error parsing element %d: %s", i, err)
278		}
279		result[i] = ninjaStr
280	}
281	return result, nil
282}
283
284// parseNinjaOrSimpleStrings splits a list of strings into *ninjaStrings if they have ninja
285// variable references or a list of strings if they don't.  If none of the input strings contain
286// ninja variable references (a very common case) then it returns the unmodified input slice as
287// the output slice.
288func parseNinjaOrSimpleStrings(scope scope, strs []string) ([]*ninjaString, []string, error) {
289	if len(strs) == 0 {
290		return nil, strs, nil
291	}
292
293	// allSimpleStrings is true until the first time a string with ninja variable references is found.
294	allSimpleStrings := true
295	var simpleStrings []string
296	var ninjaStrings []*ninjaString
297
298	for i, str := range strs {
299		ninjaStr, simpleStr, err := parseNinjaOrSimpleString(scope, str)
300		if err != nil {
301			return nil, nil, fmt.Errorf("error parsing element %d: %s", i, err)
302		} else if ninjaStr != nil {
303			ninjaStrings = append(ninjaStrings, ninjaStr)
304			if allSimpleStrings && i > 0 {
305				// If all previous strings had no ninja variable references then they weren't copied into
306				// simpleStrings to avoid allocating it if the input slice is reused as the output.  Allocate
307				// simpleStrings and copy all the previous strings into it.
308				simpleStrings = make([]string, i, len(strs))
309				copy(simpleStrings, strs[:i])
310			}
311			allSimpleStrings = false
312		} else {
313			if !allSimpleStrings {
314				// Only copy into the output slice if at least one string with ninja variable references
315				// was found.  Skipped strings will be copied the first time a string with ninja variable
316				// is found.
317				simpleStrings = append(simpleStrings, simpleStr)
318			}
319		}
320	}
321	if allSimpleStrings {
322		// None of the input strings had ninja variable references, return the input slice as the output.
323		return nil, strs, nil
324	}
325	return ninjaStrings, simpleStrings, nil
326}
327
328func (n *ninjaString) Value(nameTracker *nameTracker) string {
329	if n.variables == nil || len(*n.variables) == 0 {
330		return defaultEscaper.Replace(n.str)
331	}
332	str := &strings.Builder{}
333	n.ValueWithEscaper(str, nameTracker, defaultEscaper)
334	return str.String()
335}
336
337func (n *ninjaString) ValueWithEscaper(w io.StringWriter, nameTracker *nameTracker, escaper *strings.Replacer) {
338
339	if n.variables == nil || len(*n.variables) == 0 {
340		w.WriteString(escaper.Replace(n.str))
341		return
342	}
343
344	i := 0
345	for _, v := range *n.variables {
346		w.WriteString(escaper.Replace(n.str[i:v.start]))
347		if v.variable == nil {
348			w.WriteString("$ ")
349		} else {
350			w.WriteString("${")
351			w.WriteString(nameTracker.Variable(v.variable))
352			w.WriteString("}")
353		}
354		i = int(v.end)
355	}
356	w.WriteString(escaper.Replace(n.str[i:len(n.str)]))
357}
358
359func (n *ninjaString) Eval(variables map[Variable]*ninjaString) (string, error) {
360	if n.variables == nil || len(*n.variables) == 0 {
361		return n.str, nil
362	}
363
364	w := &strings.Builder{}
365	i := 0
366	for _, v := range *n.variables {
367		w.WriteString(n.str[i:v.start])
368		if v.variable == nil {
369			w.WriteString(" ")
370		} else {
371			variable, ok := variables[v.variable]
372			if !ok {
373				return "", fmt.Errorf("no such global variable: %s", v.variable)
374			}
375			value, err := variable.Eval(variables)
376			if err != nil {
377				return "", err
378			}
379			w.WriteString(value)
380		}
381		i = int(v.end)
382	}
383	w.WriteString(n.str[i:len(n.str)])
384	return w.String(), nil
385}
386
387func (n *ninjaString) Variables() []Variable {
388	if n.variables == nil || len(*n.variables) == 0 {
389		return nil
390	}
391
392	variables := make([]Variable, 0, len(*n.variables))
393	for _, v := range *n.variables {
394		if v.variable != nil {
395			variables = append(variables, v.variable)
396		}
397	}
398	return variables
399}
400
401func validateNinjaName(name string) error {
402	for i, r := range name {
403		valid := (r >= 'a' && r <= 'z') ||
404			(r >= 'A' && r <= 'Z') ||
405			(r >= '0' && r <= '9') ||
406			(r == '_') ||
407			(r == '-') ||
408			(r == '.')
409		if !valid {
410
411			return fmt.Errorf("%q contains an invalid Ninja name character "+
412				"%q at byte offset %d", name, r, i)
413		}
414	}
415	return nil
416}
417
418func toNinjaName(name string) string {
419	ret := bytes.Buffer{}
420	ret.Grow(len(name))
421	for _, r := range name {
422		valid := (r >= 'a' && r <= 'z') ||
423			(r >= 'A' && r <= 'Z') ||
424			(r >= '0' && r <= '9') ||
425			(r == '_') ||
426			(r == '-') ||
427			(r == '.')
428		if valid {
429			ret.WriteRune(r)
430		} else {
431			// TODO(jeffrygaston): do escaping so that toNinjaName won't ever output duplicate
432			// names for two different input names
433			ret.WriteRune('_')
434		}
435	}
436
437	return ret.String()
438}
439
440var builtinRuleArgs = []string{"out", "in"}
441
442func validateArgName(argName string) error {
443	err := validateNinjaName(argName)
444	if err != nil {
445		return err
446	}
447
448	// We only allow globals within the rule's package to be used as rule
449	// arguments.  A global in another package can always be mirrored into
450	// the rule's package by defining a new variable, so this doesn't limit
451	// what's possible.  This limitation prevents situations where a Build
452	// invocation in another package must use the rule-defining package's
453	// import name for a 3rd package in order to set the rule's arguments.
454	if strings.ContainsRune(argName, '.') {
455		return fmt.Errorf("%q contains a '.' character", argName)
456	}
457
458	if argName == "tags" {
459		return fmt.Errorf("\"tags\" is a reserved argument name")
460	}
461
462	for _, builtin := range builtinRuleArgs {
463		if argName == builtin {
464			return fmt.Errorf("%q conflicts with Ninja built-in", argName)
465		}
466	}
467
468	return nil
469}
470
471func validateArgNames(argNames []string) error {
472	for _, argName := range argNames {
473		err := validateArgName(argName)
474		if err != nil {
475			return err
476		}
477	}
478
479	return nil
480}
481
482func ninjaStringsEqual(a, b *ninjaString) bool {
483	return a.str == b.str &&
484		(a.variables == nil) == (b.variables == nil) &&
485		(a.variables == nil ||
486			slices.Equal(*a.variables, *b.variables))
487}
488