Optimize NinjaEscapeList to avoid allocating an output slice am: 42b2e906ef am: d6b9e69e76

Original change: https://android-review.googlesource.com/c/platform/build/blueprint/+/2813841

Change-Id: I21300a6179ecaaaaa7fe20cf9c9eabfcc4be776b
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Colin Cross 2023-11-02 17:45:42 +00:00 committed by Automerger Merge Worker
commit 2a50415af1
2 changed files with 139 additions and 18 deletions

View file

@ -14,26 +14,38 @@
package proptools package proptools
import "strings" import (
"strings"
"unsafe"
)
// NinjaEscapeList takes a slice of strings that may contain characters that are meaningful to ninja // NinjaEscapeList takes a slice of strings that may contain characters that are meaningful to ninja
// ($), and escapes each string so they will be passed to bash. It is not necessary on input, // ($), and escapes each string so they will be passed to bash. It is not necessary on input,
// output, or dependency names, those are handled by ModuleContext.Build. It is generally required // output, or dependency names, those are handled by ModuleContext.Build. It is generally required
// on strings from properties in Blueprint files that are used as Args to ModuleContext.Build. A // on strings from properties in Blueprint files that are used as Args to ModuleContext.Build. If
// new slice containing the escaped strings is returned. // escaping modified any of the strings then a new slice containing the escaped strings is returned,
// otherwise the original slice is returned.
func NinjaEscapeList(slice []string) []string { func NinjaEscapeList(slice []string) []string {
slice = append([]string(nil), slice...) sliceCopied := false
for i, s := range slice { for i, s := range slice {
slice[i] = NinjaEscape(s) escaped := NinjaEscape(s)
if unsafe.StringData(s) != unsafe.StringData(escaped) {
if !sliceCopied {
// If this was the first string that was modified by escaping then make a copy of the
// input slice to use as the output slice.
slice = append([]string(nil), slice...)
sliceCopied = true
}
slice[i] = escaped
}
} }
return slice return slice
} }
// NinjaEscapeList takes a string that may contain characters that are meaningful to ninja // NinjaEscape takes a string that may contain characters that are meaningful to ninja
// ($), and escapes it so it will be passed to bash. It is not necessary on input, // ($), and escapes it so it will be passed to bash. It is not necessary on input,
// output, or dependency names, those are handled by ModuleContext.Build. It is generally required // output, or dependency names, those are handled by ModuleContext.Build. It is generally required
// on strings from properties in Blueprint files that are used as Args to ModuleContext.Build. A // on strings from properties in Blueprint files that are used as Args to ModuleContext.Build.
// new slice containing the escaped strings is returned.
func NinjaEscape(s string) string { func NinjaEscape(s string) string {
return ninjaEscaper.Replace(s) return ninjaEscaper.Replace(s)
} }
@ -43,23 +55,39 @@ var ninjaEscaper = strings.NewReplacer(
// ShellEscapeList takes a slice of strings that may contain characters that are meaningful to bash and // ShellEscapeList takes a slice of strings that may contain characters that are meaningful to bash and
// escapes them if necessary by wrapping them in single quotes, and replacing internal single quotes with // escapes them if necessary by wrapping them in single quotes, and replacing internal single quotes with
// '\'' (one single quote to end the quoting, a shell-escaped single quote to insert a real single // one single quote to end the quoting, a shell-escaped single quote to insert a real single
// quote, and then a single quote to restarting quoting. A new slice containing the escaped strings // quote, and then a single quote to restarting quoting. If escaping modified any of the strings then a
// is returned. // new slice containing the escaped strings is returned, otherwise the original slice is returned.
func ShellEscapeList(slice []string) []string { func ShellEscapeList(slice []string) []string {
slice = append([]string(nil), slice...) sliceCopied := false
for i, s := range slice { for i, s := range slice {
slice[i] = ShellEscape(s) escaped := ShellEscape(s)
if unsafe.StringData(s) != unsafe.StringData(escaped) {
if !sliceCopied {
// If this was the first string that was modified by escaping then make a copy of the
// input slice to use as the output slice.
slice = append([]string(nil), slice...)
sliceCopied = true
}
slice[i] = escaped
}
} }
return slice return slice
} }
func ShellEscapeListIncludingSpaces(slice []string) []string { func ShellEscapeListIncludingSpaces(slice []string) []string {
slice = append([]string(nil), slice...) sliceCopied := false
for i, s := range slice { for i, s := range slice {
slice[i] = ShellEscapeIncludingSpaces(s) escaped := ShellEscapeIncludingSpaces(s)
if unsafe.StringData(s) != unsafe.StringData(escaped) {
if !sliceCopied {
// If this was the first string that was modified by escaping then make a copy of the
// input slice to use as the output slice.
slice = append([]string(nil), slice...)
sliceCopied = true
}
slice[i] = escaped
}
} }
return slice return slice
} }
@ -84,7 +112,7 @@ func shellUnsafeChar(r rune) bool {
// ShellEscape takes string that may contain characters that are meaningful to bash and // ShellEscape takes string that may contain characters that are meaningful to bash and
// escapes it if necessary by wrapping it in single quotes, and replacing internal single quotes with // escapes it if necessary by wrapping it in single quotes, and replacing internal single quotes with
// '\'' (one single quote to end the quoting, a shell-escaped single quote to insert a real single // one single quote to end the quoting, a shell-escaped single quote to insert a real single
// quote, and then a single quote to restarting quoting. // quote, and then a single quote to restarting quoting.
func ShellEscape(s string) string { func ShellEscape(s string) string {
shellUnsafeCharNotSpace := func(r rune) bool { shellUnsafeCharNotSpace := func(r rune) bool {

View file

@ -16,7 +16,9 @@ package proptools
import ( import (
"os/exec" "os/exec"
"reflect"
"testing" "testing"
"unsafe"
) )
type escapeTestCase struct { type escapeTestCase struct {
@ -167,3 +169,94 @@ func TestExternalShellEscapeIncludingSpaces(t *testing.T) {
} }
} }
} }
func TestNinjaEscapeList(t *testing.T) {
type testCase struct {
name string
in []string
ninjaEscaped []string
shellEscaped []string
ninjaAndShellEscaped []string
sameSlice bool
}
testCases := []testCase{
{
name: "empty",
in: []string{},
sameSlice: true,
},
{
name: "nil",
in: nil,
sameSlice: true,
},
{
name: "no escaping",
in: []string{"abc", "def", "ghi"},
sameSlice: true,
},
{
name: "escape first",
in: []string{`$\abc`, "def", "ghi"},
ninjaEscaped: []string{`$$\abc`, "def", "ghi"},
shellEscaped: []string{`'$\abc'`, "def", "ghi"},
ninjaAndShellEscaped: []string{`'$$\abc'`, "def", "ghi"},
},
{
name: "escape middle",
in: []string{"abc", `$\def`, "ghi"},
ninjaEscaped: []string{"abc", `$$\def`, "ghi"},
shellEscaped: []string{"abc", `'$\def'`, "ghi"},
ninjaAndShellEscaped: []string{"abc", `'$$\def'`, "ghi"},
},
{
name: "escape last",
in: []string{"abc", "def", `$\ghi`},
ninjaEscaped: []string{"abc", "def", `$$\ghi`},
shellEscaped: []string{"abc", "def", `'$\ghi'`},
ninjaAndShellEscaped: []string{"abc", "def", `'$$\ghi'`},
},
}
testFuncs := []struct {
name string
f func([]string) []string
expected func(tt testCase) []string
}{
{name: "NinjaEscapeList", f: NinjaEscapeList, expected: func(tt testCase) []string { return tt.ninjaEscaped }},
{name: "ShellEscapeList", f: ShellEscapeList, expected: func(tt testCase) []string { return tt.shellEscaped }},
{name: "NinjaAndShellEscapeList", f: NinjaAndShellEscapeList, expected: func(tt testCase) []string { return tt.ninjaAndShellEscaped }},
}
for _, tf := range testFuncs {
t.Run(tf.name, func(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
inCopy := append([]string(nil), tt.in...)
got := tf.f(tt.in)
want := tf.expected(tt)
if tt.sameSlice {
want = tt.in
}
if !reflect.DeepEqual(got, want) {
t.Errorf("incorrect output, want %q got %q", want, got)
}
if len(inCopy) != len(tt.in) && (len(tt.in) == 0 || !reflect.DeepEqual(inCopy, tt.in)) {
t.Errorf("input modified, want %#v, got %#v", inCopy, tt.in)
}
if (unsafe.SliceData(tt.in) == unsafe.SliceData(got)) != tt.sameSlice {
if tt.sameSlice {
t.Errorf("expected input and output slices to have the same backing arrays")
} else {
t.Errorf("expected input and output slices to have different backing arrays")
}
}
})
}
})
}
}