diff --git a/Blueprints b/Blueprints index 188767b..f0a0f87 100644 --- a/Blueprints +++ b/Blueprints @@ -69,12 +69,14 @@ bootstrap_go_package( pkgPath = "github.com/google/blueprint/proptools", srcs = [ "proptools/clone.go", + "proptools/escape.go", "proptools/extend.go", "proptools/proptools.go", "proptools/typeequal.go", ], testSrcs = [ "proptools/clone_test.go", + "proptools/escape_test.go", "proptools/extend_test.go", "proptools/typeequal_test.go", ], diff --git a/build.ninja.in b/build.ninja.in index 584ae90..26f9b98 100644 --- a/build.ninja.in +++ b/build.ninja.in @@ -73,7 +73,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:83:1 +# Defined: Blueprints:85:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap/pkg/github.com/google/blueprint/bootstrap.a $ @@ -100,7 +100,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:102:1 +# Defined: Blueprints:104:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a $ @@ -173,6 +173,7 @@ default $ build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-proptools/pkg/github.com/google/blueprint/proptools.a $ : 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/proptools.go $ ${g.bootstrap.srcDir}/proptools/typeequal.go | $ @@ -186,7 +187,7 @@ default $ # Variant: # Type: bootstrap_core_go_binary # 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: $ g.bootstrap.compile ${g.bootstrap.srcDir}/gotestmain/gotestmain.go | $ @@ -209,7 +210,7 @@ default ${g.bootstrap.BinDir}/gotestmain # Variant: # Type: bootstrap_core_go_binary # 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: $ g.bootstrap.compile ${g.bootstrap.srcDir}/gotestrunner/gotestrunner.go $ @@ -232,7 +233,7 @@ default ${g.bootstrap.BinDir}/gotestrunner # Variant: # Type: bootstrap_core_go_binary # 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: $ g.bootstrap.compile ${g.bootstrap.srcDir}/bootstrap/minibp/main.go | $ diff --git a/proptools/escape.go b/proptools/escape.go new file mode 100644 index 0000000..1cd9feb --- /dev/null +++ b/proptools/escape.go @@ -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(`'`, `'\''`) diff --git a/proptools/escape_test.go b/proptools/escape_test.go new file mode 100644 index 0000000..f65b9b0 --- /dev/null +++ b/proptools/escape_test.go @@ -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) + } + } +}