From 41ca49ff91e2c7beb4d39b44ee6131aad7bb4106 Mon Sep 17 00:00:00 2001 From: Colin Cross Date: Thu, 29 Sep 2016 13:19:34 -0700 Subject: [PATCH] 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 --- Blueprints | 2 + build.ninja.in | 11 ++-- proptools/escape.go | 78 ++++++++++++++++++++++++ proptools/escape_test.go | 126 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 proptools/escape.go create mode 100644 proptools/escape_test.go 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) + } + } +}