1// Copyright 2016 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 proptools
16
17import (
18	"slices"
19	"strings"
20	"unsafe"
21)
22
23// NinjaEscapeList takes a slice of strings that may contain characters that are meaningful to ninja
24// ($), and escapes each string so they will be passed to bash.  It is not necessary on input,
25// output, or dependency names, those are handled by ModuleContext.Build.  It is generally required
26// on strings from properties in Blueprint files that are used as Args to ModuleContext.Build.  If
27// escaping modified any of the strings then a new slice containing the escaped strings is returned,
28// otherwise the original slice is returned.
29func NinjaEscapeList(slice []string) []string {
30	sliceCopied := false
31	for i, s := range slice {
32		escaped := NinjaEscape(s)
33		if unsafe.StringData(s) != unsafe.StringData(escaped) {
34			if !sliceCopied {
35				// If this was the first string that was modified by escaping then make a copy of the
36				// input slice to use as the output slice.
37				slice = slices.Clone(slice)
38				sliceCopied = true
39			}
40			slice[i] = escaped
41		}
42	}
43	return slice
44}
45
46// NinjaEscape takes a string that may contain characters that are meaningful to ninja
47// ($), and escapes it so it will be passed to bash.  It is not necessary on input,
48// output, or dependency names, those are handled by ModuleContext.Build.  It is generally required
49// on strings from properties in Blueprint files that are used as Args to ModuleContext.Build.
50func NinjaEscape(s string) string {
51	return ninjaEscaper.Replace(s)
52}
53
54var ninjaEscaper = strings.NewReplacer(
55	"$", "$$")
56
57// ShellEscapeList takes a slice of strings that may contain characters that are meaningful to bash and
58// escapes them if necessary by wrapping them in single quotes, and replacing internal single quotes with
59// one single quote to end the quoting, a shell-escaped single quote to insert a real single
60// quote, and then a single quote to restarting quoting.  If escaping modified any of the strings then a
61// new slice containing the escaped strings is returned, otherwise the original slice is returned.
62func ShellEscapeList(slice []string) []string {
63	sliceCopied := false
64	for i, s := range slice {
65		escaped := ShellEscape(s)
66		if unsafe.StringData(s) != unsafe.StringData(escaped) {
67			if !sliceCopied {
68				// If this was the first string that was modified by escaping then make a copy of the
69				// input slice to use as the output slice.
70				slice = slices.Clone(slice)
71				sliceCopied = true
72			}
73			slice[i] = escaped
74		}
75	}
76	return slice
77}
78
79func ShellEscapeListIncludingSpaces(slice []string) []string {
80	sliceCopied := false
81	for i, s := range slice {
82		escaped := ShellEscapeIncludingSpaces(s)
83		if unsafe.StringData(s) != unsafe.StringData(escaped) {
84			if !sliceCopied {
85				// If this was the first string that was modified by escaping then make a copy of the
86				// input slice to use as the output slice.
87				slice = slices.Clone(slice)
88				sliceCopied = true
89			}
90			slice[i] = escaped
91		}
92	}
93	return slice
94}
95
96func shellUnsafeChar(r rune) bool {
97	switch {
98	case 'A' <= r && r <= 'Z',
99		'a' <= r && r <= 'z',
100		'0' <= r && r <= '9',
101		r == '_',
102		r == '+',
103		r == '-',
104		r == '=',
105		r == '.',
106		r == ',',
107		r == '/':
108		return false
109	default:
110		return true
111	}
112}
113
114// ShellEscape takes string that may contain characters that are meaningful to bash and
115// escapes it if necessary by wrapping it in single quotes, and replacing internal single quotes with
116// one single quote to end the quoting, a shell-escaped single quote to insert a real single
117// quote, and then a single quote to restarting quoting.
118func ShellEscape(s string) string {
119	shellUnsafeCharNotSpace := func(r rune) bool {
120		return r != ' ' && shellUnsafeChar(r)
121	}
122
123	if strings.IndexFunc(s, shellUnsafeCharNotSpace) == -1 {
124		// No escaping necessary
125		return s
126	}
127
128	return `'` + singleQuoteReplacer.Replace(s) + `'`
129}
130
131// ShellEscapeIncludingSpaces escapes the input `s` in a similar way to ShellEscape except that
132// this treats spaces as meaningful characters.
133func ShellEscapeIncludingSpaces(s string) string {
134	if strings.IndexFunc(s, shellUnsafeChar) == -1 {
135		// No escaping necessary
136		return s
137	}
138
139	return `'` + singleQuoteReplacer.Replace(s) + `'`
140}
141
142func NinjaAndShellEscapeList(slice []string) []string {
143	return ShellEscapeList(NinjaEscapeList(slice))
144}
145
146func NinjaAndShellEscapeListIncludingSpaces(slice []string) []string {
147	return ShellEscapeListIncludingSpaces(NinjaEscapeList(slice))
148}
149
150func NinjaAndShellEscape(s string) string {
151	return ShellEscape(NinjaEscape(s))
152}
153
154func NinjaAndShellEscapeIncludingSpaces(s string) string {
155	return ShellEscapeIncludingSpaces(NinjaEscape(s))
156}
157
158var singleQuoteReplacer = strings.NewReplacer(`'`, `'\''`)
159