Add proptools functions to escape strings

Blueprint properties that end up as command line arguments need to be
both ninja and shell escaped.  Provide helpers that primary builders can
use to appropriately escape them.

Change-Id: Ifd697d87edb1c6f0a910377835c391bbe8f95b42
This commit is contained in:
Colin Cross 2016-09-29 13:19:34 -07:00
parent f5909d9cb5
commit 41ca49ff91
4 changed files with 212 additions and 5 deletions

View file

@ -69,12 +69,14 @@ bootstrap_go_package(
pkgPath = "github.com/google/blueprint/proptools", pkgPath = "github.com/google/blueprint/proptools",
srcs = [ srcs = [
"proptools/clone.go", "proptools/clone.go",
"proptools/escape.go",
"proptools/extend.go", "proptools/extend.go",
"proptools/proptools.go", "proptools/proptools.go",
"proptools/typeequal.go", "proptools/typeequal.go",
], ],
testSrcs = [ testSrcs = [
"proptools/clone_test.go", "proptools/clone_test.go",
"proptools/escape_test.go",
"proptools/extend_test.go", "proptools/extend_test.go",
"proptools/typeequal_test.go", "proptools/typeequal_test.go",
], ],

View file

@ -73,7 +73,7 @@ default $
# Variant: # Variant:
# Type: bootstrap_go_package # Type: bootstrap_go_package
# Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1
# Defined: Blueprints:83:1 # Defined: Blueprints:85:1
build $ build $
${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap/pkg/github.com/google/blueprint/bootstrap.a $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap/pkg/github.com/google/blueprint/bootstrap.a $
@ -100,7 +100,7 @@ default $
# Variant: # Variant:
# Type: bootstrap_go_package # Type: bootstrap_go_package
# Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1
# Defined: Blueprints:102:1 # Defined: Blueprints:104:1
build $ build $
${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a $
@ -173,6 +173,7 @@ default $
build $ build $
${g.bootstrap.buildDir}/.bootstrap/blueprint-proptools/pkg/github.com/google/blueprint/proptools.a $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-proptools/pkg/github.com/google/blueprint/proptools.a $
: g.bootstrap.compile ${g.bootstrap.srcDir}/proptools/clone.go $ : g.bootstrap.compile ${g.bootstrap.srcDir}/proptools/clone.go $
${g.bootstrap.srcDir}/proptools/escape.go $
${g.bootstrap.srcDir}/proptools/extend.go $ ${g.bootstrap.srcDir}/proptools/extend.go $
${g.bootstrap.srcDir}/proptools/proptools.go $ ${g.bootstrap.srcDir}/proptools/proptools.go $
${g.bootstrap.srcDir}/proptools/typeequal.go | $ ${g.bootstrap.srcDir}/proptools/typeequal.go | $
@ -186,7 +187,7 @@ default $
# Variant: # Variant:
# Type: bootstrap_core_go_binary # Type: bootstrap_core_go_binary
# Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1 # Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1
# Defined: Blueprints:135:1 # Defined: Blueprints:137:1
build ${g.bootstrap.buildDir}/.bootstrap/gotestmain/obj/gotestmain.a: $ build ${g.bootstrap.buildDir}/.bootstrap/gotestmain/obj/gotestmain.a: $
g.bootstrap.compile ${g.bootstrap.srcDir}/gotestmain/gotestmain.go | $ g.bootstrap.compile ${g.bootstrap.srcDir}/gotestmain/gotestmain.go | $
@ -209,7 +210,7 @@ default ${g.bootstrap.BinDir}/gotestmain
# Variant: # Variant:
# Type: bootstrap_core_go_binary # Type: bootstrap_core_go_binary
# Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1 # Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1
# Defined: Blueprints:140:1 # Defined: Blueprints:142:1
build ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/gotestrunner.a: $ build ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/gotestrunner.a: $
g.bootstrap.compile ${g.bootstrap.srcDir}/gotestrunner/gotestrunner.go $ g.bootstrap.compile ${g.bootstrap.srcDir}/gotestrunner/gotestrunner.go $
@ -232,7 +233,7 @@ default ${g.bootstrap.BinDir}/gotestrunner
# Variant: # Variant:
# Type: bootstrap_core_go_binary # Type: bootstrap_core_go_binary
# Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1 # Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1
# Defined: Blueprints:114:1 # Defined: Blueprints:116:1
build ${g.bootstrap.buildDir}/.bootstrap/minibp/obj/minibp.a: $ build ${g.bootstrap.buildDir}/.bootstrap/minibp/obj/minibp.a: $
g.bootstrap.compile ${g.bootstrap.srcDir}/bootstrap/minibp/main.go | $ g.bootstrap.compile ${g.bootstrap.srcDir}/bootstrap/minibp/main.go | $

78
proptools/escape.go Normal file
View file

@ -0,0 +1,78 @@
// Copyright 2016 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proptools
import "strings"
// NinjaEscape 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,
// 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
// new slice containing the escaped strings is returned.
func NinjaEscape(slice []string) []string {
slice = append([]string(nil), slice...)
for i, s := range slice {
slice[i] = ninjaEscaper.Replace(s)
}
return slice
}
var ninjaEscaper = strings.NewReplacer(
"$", "$$")
// ShellEscape takes a slice of strings that may contain characters that are meaningful to bash and
// escapes 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
// quote, and then a single quote to restarting quoting. A new slice containing the escaped strings
// is returned.
func ShellEscape(slice []string) []string {
shellUnsafeChar := func(r rune) bool {
switch {
case 'A' <= r && r <= 'Z',
'a' <= r && r <= 'z',
'0' <= r && r <= '9',
r == '_',
r == '+',
r == '-',
r == '=',
r == '.',
r == ',',
r == '/',
r == ' ':
return false
default:
return true
}
}
slice = append([]string(nil), slice...)
for i, s := range slice {
if strings.IndexFunc(s, shellUnsafeChar) == -1 {
// No escaping necessary
continue
}
slice[i] = `'` + singleQuoteReplacer.Replace(s) + `'`
}
return slice
}
func NinjaAndShellEscape(slice []string) []string {
return ShellEscape(NinjaEscape(slice))
}
var singleQuoteReplacer = strings.NewReplacer(`'`, `'\''`)

126
proptools/escape_test.go Normal file
View file

@ -0,0 +1,126 @@
// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proptools
import (
"os/exec"
"testing"
)
type escapeTestCase struct {
name string
in string
out string
}
var ninjaEscapeTestCase = []escapeTestCase{
{
name: "no escaping",
in: `test`,
out: `test`,
},
{
name: "leading $",
in: `$test`,
out: `$$test`,
},
{
name: "trailing $",
in: `test$`,
out: `test$$`,
},
{
name: "leading and trailing $",
in: `$test$`,
out: `$$test$$`,
},
}
var shellEscapeTestCase = []escapeTestCase{
{
name: "no escaping",
in: `test`,
out: `test`,
},
{
name: "leading $",
in: `$test`,
out: `'$test'`,
},
{
name: "trailing $",
in: `test$`,
out: `'test$'`,
},
{
name: "leading and trailing $",
in: `$test$`,
out: `'$test$'`,
},
{
name: "single quote",
in: `'`,
out: `''\'''`,
},
{
name: "multiple single quote",
in: `''`,
out: `''\'''\'''`,
},
{
name: "double quote",
in: `""`,
out: `'""'`,
},
{
name: "ORIGIN",
in: `-Wl,--rpath,${ORIGIN}/../bionic-loader-test-libs`,
out: `'-Wl,--rpath,${ORIGIN}/../bionic-loader-test-libs'`,
},
}
func TestNinjaEscaping(t *testing.T) {
for _, testCase := range ninjaEscapeTestCase {
got := NinjaEscape([]string{testCase.in})[0]
if got != testCase.out {
t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.out, got)
}
}
}
func TestShellEscaping(t *testing.T) {
for _, testCase := range shellEscapeTestCase {
got := ShellEscape([]string{testCase.in})[0]
if got != testCase.out {
t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.out, got)
}
}
}
func TestExternalShellEscaping(t *testing.T) {
if testing.Short() {
return
}
for _, testCase := range shellEscapeTestCase {
cmd := "echo -n " + ShellEscape([]string{testCase.in})[0]
got, err := exec.Command("/bin/sh", "-c", cmd).Output()
if err != nil {
t.Error(err)
}
if string(got) != testCase.in {
t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.in, got)
}
}
}