soong_zip: Add tests

Add test that cover basic command line usage of soong_zip.  -D
is not covered yet as the implementation will be replaced with
one that is also more easily testable in the next patch.

Bug: 116751500
Test: zip_test.go
Change-Id: I5a1bcee74ebc9cb3cf332c36f89bc12c0e807ad2
diff --git a/cmd/merge_zips/merge_zips.go b/cmd/merge_zips/merge_zips.go
index 95ff70b..f383de9 100644
--- a/cmd/merge_zips/merge_zips.go
+++ b/cmd/merge_zips/merge_zips.go
@@ -250,7 +250,12 @@
 			addMapping(jar.MetaDir, dirSource)
 		}
 
-		fh, buf, err := jar.ManifestFileContents(manifest)
+		contents, err := ioutil.ReadFile(manifest)
+		if err != nil {
+			return err
+		}
+
+		fh, buf, err := jar.ManifestFileContents(contents)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/multiproduct_kati/main.go b/cmd/multiproduct_kati/main.go
index 374868c..4933f37 100644
--- a/cmd/multiproduct_kati/main.go
+++ b/cmd/multiproduct_kati/main.go
@@ -328,7 +328,7 @@
 			NumParallelJobs:  runtime.NumCPU(),
 			CompressionLevel: 5,
 		}
-		if err := zip.Run(args); err != nil {
+		if err := zip.Zip(args); err != nil {
 			log.Fatalf("Error zipping logs: %v", err)
 		}
 	}
@@ -409,7 +409,7 @@
 				NumParallelJobs:  runtime.NumCPU(),
 				CompressionLevel: 5,
 			}
-			if err := zip.Run(args); err != nil {
+			if err := zip.Zip(args); err != nil {
 				log.Fatalf("Error zipping artifacts: %v", err)
 			}
 		}
diff --git a/jar/jar.go b/jar/jar.go
index 653e5ee..fa0e693 100644
--- a/jar/jar.go
+++ b/jar/jar.go
@@ -17,7 +17,6 @@
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"strings"
 	"time"
@@ -81,10 +80,9 @@
 	return dirHeader
 }
 
-// Convert manifest source path to zip header and contents.  If path is empty uses a default
-// manifest.
-func ManifestFileContents(src string) (*zip.FileHeader, []byte, error) {
-	b, err := manifestContents(src)
+// Create a manifest zip header and contents using the provided contents if any.
+func ManifestFileContents(contents []byte) (*zip.FileHeader, []byte, error) {
+	b, err := manifestContents(contents)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -100,26 +98,16 @@
 	return fh, b, nil
 }
 
-// Convert manifest source path to contents.  If path is empty uses a default manifest.
-func manifestContents(src string) ([]byte, error) {
-	var givenBytes []byte
-	var err error
-
-	if src != "" {
-		givenBytes, err = ioutil.ReadFile(src)
-		if err != nil {
-			return nil, err
-		}
-	}
-
+// Create manifest contents, using the provided contents if any.
+func manifestContents(contents []byte) ([]byte, error) {
 	manifestMarker := []byte("Manifest-Version:")
 	header := append(manifestMarker, []byte(" 1.0\nCreated-By: soong_zip\n")...)
 
 	var finalBytes []byte
-	if !bytes.Contains(givenBytes, manifestMarker) {
-		finalBytes = append(append(header, givenBytes...), byte('\n'))
+	if !bytes.Contains(contents, manifestMarker) {
+		finalBytes = append(append(header, contents...), byte('\n'))
 	} else {
-		finalBytes = givenBytes
+		finalBytes = contents
 	}
 
 	return finalBytes, nil
diff --git a/java/java.go b/java/java.go
index 0bd7857..7fd5344 100644
--- a/java/java.go
+++ b/java/java.go
@@ -95,6 +95,9 @@
 	// list of java libraries that will be compiled into the resulting jar
 	Static_libs []string `android:"arch_variant"`
 
+	// list of native libraries that will be provided in or alongside the resulting jar
+	Jni_libs []string `android:"arch_variant"`
+
 	// manifest file to be included in resulting jar
 	Manifest *string
 
diff --git a/zip/cmd/main.go b/zip/cmd/main.go
index a2fbf41..1125602 100644
--- a/zip/cmd/main.go
+++ b/zip/cmd/main.go
@@ -187,7 +187,7 @@
 		os.Exit(1)
 	}
 
-	err := zip.Run(zip.ZipArgs{
+	err := zip.Zip(zip.ZipArgs{
 		FileArgs:                 fileArgsBuilder.FileArgs(),
 		OutputFilePath:           *out,
 		EmulateJar:               *emulateJar,
diff --git a/zip/zip.go b/zip/zip.go
index 96f4535..e7de6f8 100644
--- a/zip/zip.go
+++ b/zip/zip.go
@@ -22,7 +22,6 @@
 	"hash/crc32"
 	"io"
 	"io/ioutil"
-	"log"
 	"os"
 	"path/filepath"
 	"sort"
@@ -178,6 +177,8 @@
 
 	compressorPool sync.Pool
 	compLevel      int
+
+	fs pathtools.FileSystem
 }
 
 type zipEntry struct {
@@ -201,6 +202,7 @@
 	NumParallelJobs          int
 	NonDeflatedFiles         map[string]bool
 	WriteIfChanged           bool
+	Filesystem               pathtools.FileSystem
 }
 
 const NOQUOTE = '\x00'
@@ -246,22 +248,24 @@
 	return args
 }
 
-func Run(args ZipArgs) (err error) {
-	if args.OutputFilePath == "" {
-		return fmt.Errorf("output file path must be nonempty")
-	}
-
+func ZipTo(args ZipArgs, w io.Writer) error {
 	if args.EmulateJar {
 		args.AddDirectoryEntriesToZip = true
 	}
 
-	w := &ZipWriter{
+	z := &ZipWriter{
 		time:         jar.DefaultTime,
 		createdDirs:  make(map[string]string),
 		createdFiles: make(map[string]string),
 		directories:  args.AddDirectoryEntriesToZip,
 		compLevel:    args.CompressionLevel,
+		fs:           args.Filesystem,
 	}
+
+	if z.fs == nil {
+		z.fs = pathtools.OsFs
+	}
+
 	pathMappings := []pathMapping{}
 
 	noCompression := args.CompressionLevel == 0
@@ -274,11 +278,19 @@
 		for _, src := range srcs {
 			err := fillPathPairs(fa, src, &pathMappings, args.NonDeflatedFiles, noCompression)
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 		}
 	}
 
+	return z.write(w, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.NumParallelJobs)
+}
+
+func Zip(args ZipArgs) error {
+	if args.OutputFilePath == "" {
+		return fmt.Errorf("output file path must be nonempty")
+	}
+
 	buf := &bytes.Buffer{}
 	var out io.Writer = buf
 
@@ -298,7 +310,7 @@
 		out = f
 	}
 
-	err = w.write(out, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.NumParallelJobs)
+	err := ZipTo(args, out)
 	if err != nil {
 		return err
 	}
@@ -351,13 +363,6 @@
 	sort.SliceStable(mappings, less)
 }
 
-type readerSeekerCloser interface {
-	io.Reader
-	io.ReaderAt
-	io.Closer
-	io.Seeker
-}
-
 func (z *ZipWriter) write(f io.Writer, pathMappings []pathMapping, manifest string, emulateJar bool, parallelJobs int) error {
 	z.errors = make(chan error)
 	defer close(z.errors)
@@ -504,7 +509,7 @@
 	var fileSize int64
 	var executable bool
 
-	if s, err := os.Lstat(src); err != nil {
+	if s, err := z.fs.Lstat(src); err != nil {
 		return err
 	} else if s.IsDir() {
 		if z.directories {
@@ -535,7 +540,7 @@
 		executable = s.Mode()&0100 != 0
 	}
 
-	r, err := os.Open(src)
+	r, err := z.fs.Open(src)
 	if err != nil {
 		return err
 	}
@@ -565,7 +570,21 @@
 		return err
 	}
 
-	fh, buf, err := jar.ManifestFileContents(src)
+	var contents []byte
+	if src != "" {
+		f, err := z.fs.Open(src)
+		if err != nil {
+			return err
+		}
+
+		contents, err = ioutil.ReadAll(f)
+		f.Close()
+		if err != nil {
+			return err
+		}
+	}
+
+	fh, buf, err := jar.ManifestFileContents(contents)
 	if err != nil {
 		return err
 	}
@@ -575,7 +594,7 @@
 	return z.writeFileContents(fh, reader)
 }
 
-func (z *ZipWriter) writeFileContents(header *zip.FileHeader, r readerSeekerCloser) (err error) {
+func (z *ZipWriter) writeFileContents(header *zip.FileHeader, r pathtools.ReaderAtSeekerCloser) (err error) {
 
 	header.SetModTime(z.time)
 
@@ -845,7 +864,7 @@
 	fileHeader.SetModTime(z.time)
 	fileHeader.SetMode(0777 | os.ModeSymlink)
 
-	dest, err := os.Readlink(file)
+	dest, err := z.fs.Readlink(file)
 	if err != nil {
 		return err
 	}
diff --git a/zip/zip_test.go b/zip/zip_test.go
index 03e7958..0c2105c 100644
--- a/zip/zip_test.go
+++ b/zip/zip_test.go
@@ -15,10 +15,395 @@
 package zip
 
 import (
+	"bytes"
+	"hash/crc32"
+	"io"
+	"os"
 	"reflect"
+	"syscall"
 	"testing"
+
+	"android/soong/third_party/zip"
+
+	"github.com/google/blueprint/pathtools"
 )
 
+var (
+	fileA        = []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
+	fileB        = []byte("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
+	fileC        = []byte("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")
+	fileEmpty    = []byte("")
+	fileManifest = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\n\n")
+
+	fileCustomManifest  = []byte("Custom manifest: true\n")
+	customManifestAfter = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\nCustom manifest: true\n\n")
+)
+
+var mockFs = pathtools.MockFs(map[string][]byte{
+	"a/a/a":            fileA,
+	"a/a/b":            fileB,
+	"a/a/c -> ../../c": nil,
+	"a/a/d -> b":       nil,
+	"c":                fileC,
+	"l":                []byte("a/a/a\na/a/b\nc\n"),
+	"l2":               []byte("missing\n"),
+	"manifest.txt":     fileCustomManifest,
+})
+
+func fh(name string, contents []byte, method uint16) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               name,
+		Method:             method,
+		CRC32:              crc32.ChecksumIEEE(contents),
+		UncompressedSize64: uint64(len(contents)),
+		ExternalAttrs:      0,
+	}
+}
+
+func fhManifest(contents []byte) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               "META-INF/MANIFEST.MF",
+		Method:             zip.Store,
+		CRC32:              crc32.ChecksumIEEE(contents),
+		UncompressedSize64: uint64(len(contents)),
+		ExternalAttrs:      (syscall.S_IFREG | 0700) << 16,
+	}
+}
+
+func fhLink(name string, to string) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               name,
+		Method:             zip.Store,
+		CRC32:              crc32.ChecksumIEEE([]byte(to)),
+		UncompressedSize64: uint64(len(to)),
+		ExternalAttrs:      (syscall.S_IFLNK | 0777) << 16,
+	}
+}
+
+func fhDir(name string) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               name,
+		Method:             zip.Store,
+		CRC32:              crc32.ChecksumIEEE(nil),
+		UncompressedSize64: 0,
+		ExternalAttrs:      (syscall.S_IFDIR|0700)<<16 | 0x10,
+	}
+}
+
+func fileArgsBuilder() *FileArgsBuilder {
+	return &FileArgsBuilder{
+		fs: mockFs,
+	}
+}
+
+func TestZip(t *testing.T) {
+	testCases := []struct {
+		name             string
+		args             *FileArgsBuilder
+		compressionLevel int
+		emulateJar       bool
+		nonDeflatedFiles map[string]bool
+		dirEntries       bool
+		manifest         string
+
+		files []zip.FileHeader
+		err   error
+	}{
+		{
+			name: "empty args",
+			args: fileArgsBuilder(),
+
+			files: []zip.FileHeader{},
+		},
+		{
+			name: "files",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b").
+				File("c"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+				fh("c", fileC, zip.Deflate),
+			},
+		},
+		{
+			name: "stored files",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b").
+				File("c"),
+			compressionLevel: 0,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Store),
+				fh("a/a/b", fileB, zip.Store),
+				fh("c", fileC, zip.Store),
+			},
+		},
+		{
+			name: "symlinks in zip",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b").
+				File("a/a/c").
+				File("a/a/d"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+				fhLink("a/a/c", "../../c"),
+				fhLink("a/a/d", "b"),
+			},
+		},
+		{
+			name: "list",
+			args: fileArgsBuilder().
+				List("l"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+				fh("c", fileC, zip.Deflate),
+			},
+		},
+		{
+			name: "prefix in zip",
+			args: fileArgsBuilder().
+				PathPrefixInZip("foo").
+				File("a/a/a").
+				File("a/a/b").
+				File("c"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("foo/a/a/a", fileA, zip.Deflate),
+				fh("foo/a/a/b", fileB, zip.Deflate),
+				fh("foo/c", fileC, zip.Deflate),
+			},
+		},
+		{
+			name: "relative root",
+			args: fileArgsBuilder().
+				SourcePrefixToStrip("a").
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a", fileA, zip.Deflate),
+				fh("a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "multiple relative root",
+			args: fileArgsBuilder().
+				SourcePrefixToStrip("a").
+				File("a/a/a").
+				SourcePrefixToStrip("a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a", fileA, zip.Deflate),
+				fh("b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "emulate jar",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			emulateJar:       true,
+
+			files: []zip.FileHeader{
+				fhDir("META-INF/"),
+				fhManifest(fileManifest),
+				fhDir("a/"),
+				fhDir("a/a/"),
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "emulate jar with manifest",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			emulateJar:       true,
+			manifest:         "manifest.txt",
+
+			files: []zip.FileHeader{
+				fhDir("META-INF/"),
+				fhManifest(customManifestAfter),
+				fhDir("a/"),
+				fhDir("a/a/"),
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "dir entries",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			dirEntries:       true,
+
+			files: []zip.FileHeader{
+				fhDir("a/"),
+				fhDir("a/a/"),
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "junk paths",
+			args: fileArgsBuilder().
+				JunkPaths(true).
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a", fileA, zip.Deflate),
+				fh("b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "non deflated files",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			nonDeflatedFiles: map[string]bool{"a/a/a": true},
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Store),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+
+		// errors
+		{
+			name: "error missing file",
+			args: fileArgsBuilder().
+				File("missing"),
+			err: os.ErrNotExist,
+		},
+		{
+			name: "error missing file in list",
+			args: fileArgsBuilder().
+				List("l2"),
+			err: os.ErrNotExist,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			if test.args.Error() != nil {
+				t.Fatal(test.args.Error())
+			}
+
+			args := ZipArgs{}
+			args.FileArgs = test.args.FileArgs()
+			args.CompressionLevel = test.compressionLevel
+			args.EmulateJar = test.emulateJar
+			args.AddDirectoryEntriesToZip = test.dirEntries
+			args.NonDeflatedFiles = test.nonDeflatedFiles
+			args.ManifestSourcePath = test.manifest
+			args.Filesystem = mockFs
+
+			buf := &bytes.Buffer{}
+			err := ZipTo(args, buf)
+
+			if (err != nil) != (test.err != nil) {
+				t.Fatalf("want error %v, got %v", test.err, err)
+			} else if test.err != nil {
+				if os.IsNotExist(test.err) {
+					if !os.IsNotExist(test.err) {
+						t.Fatalf("want error %v, got %v", test.err, err)
+					}
+				} else {
+					t.Fatalf("want error %v, got %v", test.err, err)
+				}
+				return
+			}
+
+			br := bytes.NewReader(buf.Bytes())
+			zr, err := zip.NewReader(br, int64(br.Len()))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			var files []zip.FileHeader
+			for _, f := range zr.File {
+				r, err := f.Open()
+				if err != nil {
+					t.Fatalf("error when opening %s: %s", f.Name, err)
+				}
+
+				crc := crc32.NewIEEE()
+				len, err := io.Copy(crc, r)
+				r.Close()
+				if err != nil {
+					t.Fatalf("error when reading %s: %s", f.Name, err)
+				}
+
+				if uint64(len) != f.UncompressedSize64 {
+					t.Errorf("incorrect length for %s, want %d got %d", f.Name, f.UncompressedSize64, len)
+				}
+
+				if crc.Sum32() != f.CRC32 {
+					t.Errorf("incorrect crc for %s, want %x got %x", f.Name, f.CRC32, crc)
+				}
+
+				files = append(files, f.FileHeader)
+			}
+
+			if len(files) != len(test.files) {
+				t.Fatalf("want %d files, got %d", len(test.files), len(files))
+			}
+
+			for i := range files {
+				want := test.files[i]
+				got := files[i]
+
+				if want.Name != got.Name {
+					t.Errorf("incorrect file %d want %q got %q", i, want.Name, got.Name)
+					continue
+				}
+
+				if want.UncompressedSize64 != got.UncompressedSize64 {
+					t.Errorf("incorrect file %s length want %v got %v", want.Name,
+						want.UncompressedSize64, got.UncompressedSize64)
+				}
+
+				if want.ExternalAttrs != got.ExternalAttrs {
+					t.Errorf("incorrect file %s attrs want %x got %x", want.Name,
+						want.ExternalAttrs, got.ExternalAttrs)
+				}
+
+				if want.CRC32 != got.CRC32 {
+					t.Errorf("incorrect file %s crc want %v got %v", want.Name,
+						want.CRC32, got.CRC32)
+				}
+
+				if want.Method != got.Method {
+					t.Errorf("incorrect file %s method want %v got %v", want.Name,
+						want.Method, got.Method)
+				}
+			}
+		})
+	}
+}
+
 func TestReadRespFile(t *testing.T) {
 	testCases := []struct {
 		name, in string