Add Respfile support for soong_zip.

Sometime the size of our command line passed to soong_zip go program
exceeds the cmdline size limit. So add an RespFile support with "@" special
character prefix.

The args in the cmdline will be considered together with the
args in RespFile during soong_zip running.

Test: real tests in my local machine, and compare the
res/libphonenumber.jar before and after changes.

./cmd -o test.zip '""'-C -> [./cmd,-o,test.zip,""-C]
./cmd -o test.zip '-C -f -> [./cmd,-o,test.zip,-C -f]
./cmd -o test.zip '\"'-C -f -> [./cmd,-o,test.zip,\"-C -f]
./cmd -o test.zip '\\'-C -f -> [./cmd,-o,test.zip,\\-C -f]
./cmd -o test.zip '\a'-C -f -> [./cmd,-o,test.zip,\a-C -f]

./cmd -o test.zip \'-C -> [./cmd,-o,test.zip,'-C]
./cmd -o test.zip \\-C -> [./cmd,-o,test.zip,\-C]
./cmd -o test.zip \"-C -> [./cmd,-o,test.zip,"-C]

./cmd -o test.zip "'"-C -> [./cmd,-o,test.zip,'-C]
./cmd -o test.zip "\\"-C -f -> [./cmd,-o,test.zip,\a-C -f]
./cmd -o test.zip "\""-C -f -> [./cmd,-o,test.zip,"a-C -f]

Bug: b/72484223

Change-Id: I83c3630b70c8396c8e8a3f266244d868d754c4e8
diff --git a/java/builder.go b/java/builder.go
index 2fd4ac0..72574f1 100644
--- a/java/builder.go
+++ b/java/builder.go
@@ -123,8 +123,10 @@
 
 	jar = pctx.AndroidStaticRule("jar",
 		blueprint.RuleParams{
-			Command:     `${config.SoongZipCmd} -jar -o $out $jarArgs`,
-			CommandDeps: []string{"${config.SoongZipCmd}"},
+			Command:        `${config.SoongZipCmd} -jar -o $out @$out.rsp`,
+			CommandDeps:    []string{"${config.SoongZipCmd}"},
+			Rspfile:        "$out.rsp",
+			RspfileContent: "$jarArgs",
 		},
 		"jarArgs")
 
diff --git a/zip/Android.bp b/zip/Android.bp
index 3bb4f25..259e010 100644
--- a/zip/Android.bp
+++ b/zip/Android.bp
@@ -26,5 +26,8 @@
         "zip.go",
         "rate_limit.go",
     ],
+    testSrcs: [
+      "zip_test.go",
+    ],
 }
 
diff --git a/zip/cmd/main.go b/zip/cmd/main.go
index c0418f7..60017aa 100644
--- a/zip/cmd/main.go
+++ b/zip/cmd/main.go
@@ -120,30 +120,12 @@
 }
 
 var (
-	out            = flag.String("o", "", "file to write zip file to")
-	manifest       = flag.String("m", "", "input jar manifest file name")
-	directories    = flag.Bool("d", false, "include directories in zip")
-	rootPrefix     = flag.String("P", "", "path prefix within the zip at which to place files")
-	relativeRoot   = flag.String("C", "", "path to use as relative root of files in following -f, -l, or -D arguments")
-	parallelJobs   = flag.Int("j", runtime.NumCPU(), "number of parallel threads to use")
-	compLevel      = flag.Int("L", 5, "deflate compression level (0-9)")
-	emulateJar     = flag.Bool("jar", false, "modify the resultant .zip to emulate the output of 'jar'")
-	writeIfChanged = flag.Bool("write_if_changed", false, "only update resultant .zip if it has changed")
+	rootPrefix, relativeRoot *string
 
 	fArgs            zip.FileArgs
 	nonDeflatedFiles = make(uniqueSet)
-
-	cpuProfile = flag.String("cpuprofile", "", "write cpu profile to file")
-	traceFile  = flag.String("trace", "", "write trace to file")
 )
 
-func init() {
-	flag.Var(&listFiles{}, "l", "file containing list of .class files")
-	flag.Var(&dir{}, "D", "directory to include in zip")
-	flag.Var(&file{}, "f", "file to include in zip")
-	flag.Var(&nonDeflatedFiles, "s", "file path to be stored within the zip without compression")
-}
-
 func usage() {
 	fmt.Fprintf(os.Stderr, "usage: zip -o zipfile [-m manifest] -C dir [-f|-l file]...\n")
 	flag.PrintDefaults()
@@ -151,7 +133,42 @@
 }
 
 func main() {
-	flag.Parse()
+	var expandedArgs []string
+	for _, arg := range os.Args {
+		if strings.HasPrefix(arg, "@") {
+			bytes, err := ioutil.ReadFile(strings.TrimPrefix(arg, "@"))
+			if err != nil {
+				fmt.Fprintln(os.Stderr, err.Error())
+				os.Exit(1)
+			}
+			respArgs := zip.ReadRespFile(bytes)
+			expandedArgs = append(expandedArgs, respArgs...)
+		} else {
+			expandedArgs = append(expandedArgs, arg)
+		}
+	}
+
+	flags := flag.NewFlagSet("flags", flag.ExitOnError)
+
+	out := flags.String("o", "", "file to write zip file to")
+	manifest := flags.String("m", "", "input jar manifest file name")
+	directories := flags.Bool("d", false, "include directories in zip")
+	rootPrefix = flags.String("P", "", "path prefix within the zip at which to place files")
+	relativeRoot = flags.String("C", "", "path to use as relative root of files in following -f, -l, or -D arguments")
+	parallelJobs := flags.Int("j", runtime.NumCPU(), "number of parallel threads to use")
+	compLevel := flags.Int("L", 5, "deflate compression level (0-9)")
+	emulateJar := flags.Bool("jar", false, "modify the resultant .zip to emulate the output of 'jar'")
+	writeIfChanged := flags.Bool("write_if_changed", false, "only update resultant .zip if it has changed")
+
+	cpuProfile := flags.String("cpuprofile", "", "write cpu profile to file")
+	traceFile := flags.String("trace", "", "write trace to file")
+
+	flags.Var(&listFiles{}, "l", "file containing list of .class files")
+	flags.Var(&dir{}, "D", "directory to include in zip")
+	flags.Var(&file{}, "f", "file to include in zip")
+	flags.Var(&nonDeflatedFiles, "s", "file path to be stored within the zip without compression")
+
+	flags.Parse(expandedArgs[1:])
 
 	err := zip.Run(zip.ZipArgs{
 		FileArgs:                 fArgs,
diff --git a/zip/zip.go b/zip/zip.go
index c878a0c..b7e3764 100644
--- a/zip/zip.go
+++ b/zip/zip.go
@@ -31,6 +31,7 @@
 	"strings"
 	"sync"
 	"time"
+	"unicode"
 
 	"github.com/google/blueprint/pathtools"
 
@@ -132,6 +133,49 @@
 	WriteIfChanged           bool
 }
 
+const NOQUOTE = '\x00'
+
+func ReadRespFile(bytes []byte) []string {
+	var args []string
+	var arg []rune
+
+	isEscaping := false
+	quotingStart := NOQUOTE
+	for _, c := range string(bytes) {
+		switch {
+		case isEscaping:
+			if quotingStart == '"' {
+				if !(c == '"' || c == '\\') {
+					// '\"' or '\\' will be escaped under double quoting.
+					arg = append(arg, '\\')
+				}
+			}
+			arg = append(arg, c)
+			isEscaping = false
+		case c == '\\' && quotingStart != '\'':
+			isEscaping = true
+		case quotingStart == NOQUOTE && (c == '\'' || c == '"'):
+			quotingStart = c
+		case quotingStart != NOQUOTE && c == quotingStart:
+			quotingStart = NOQUOTE
+		case quotingStart == NOQUOTE && unicode.IsSpace(c):
+			// Current character is a space outside quotes
+			if len(arg) != 0 {
+				args = append(args, string(arg))
+			}
+			arg = arg[:0]
+		default:
+			arg = append(arg, c)
+		}
+	}
+
+	if len(arg) != 0 {
+		args = append(args, string(arg))
+	}
+
+	return args
+}
+
 func Run(args ZipArgs) (err error) {
 	if args.CpuProfileFilePath != "" {
 		f, err := os.Create(args.CpuProfileFilePath)
diff --git a/zip/zip_test.go b/zip/zip_test.go
new file mode 100644
index 0000000..03e7958
--- /dev/null
+++ b/zip/zip_test.go
@@ -0,0 +1,87 @@
+// Copyright 2018 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 zip
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestReadRespFile(t *testing.T) {
+	testCases := []struct {
+		name, in string
+		out      []string
+	}{
+		{
+			name: "single quoting test case 1",
+			in:   `./cmd '"'-C`,
+			out:  []string{"./cmd", `"-C`},
+		},
+		{
+			name: "single quoting test case 2",
+			in:   `./cmd '-C`,
+			out:  []string{"./cmd", `-C`},
+		},
+		{
+			name: "single quoting test case 3",
+			in:   `./cmd '\"'-C`,
+			out:  []string{"./cmd", `\"-C`},
+		},
+		{
+			name: "single quoting test case 4",
+			in:   `./cmd '\\'-C`,
+			out:  []string{"./cmd", `\\-C`},
+		},
+		{
+			name: "none quoting test case 1",
+			in:   `./cmd \'-C`,
+			out:  []string{"./cmd", `'-C`},
+		},
+		{
+			name: "none quoting test case 2",
+			in:   `./cmd \\-C`,
+			out:  []string{"./cmd", `\-C`},
+		},
+		{
+			name: "none quoting test case 3",
+			in:   `./cmd \"-C`,
+			out:  []string{"./cmd", `"-C`},
+		},
+		{
+			name: "double quoting test case 1",
+			in:   `./cmd "'"-C`,
+			out:  []string{"./cmd", `'-C`},
+		},
+		{
+			name: "double quoting test case 2",
+			in:   `./cmd "\\"-C`,
+			out:  []string{"./cmd", `\-C`},
+		},
+		{
+			name: "double quoting test case 3",
+			in:   `./cmd "\""-C`,
+			out:  []string{"./cmd", `"-C`},
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			got := ReadRespFile([]byte(testCase.in))
+			if !reflect.DeepEqual(got, testCase.out) {
+				t.Errorf("expected %q got %q", testCase.out, got)
+			}
+		})
+	}
+}