| // Copyright 2017 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 main | 
 |  | 
 | import ( | 
 | 	"errors" | 
 | 	"flag" | 
 | 	"fmt" | 
 | 	"hash/crc32" | 
 | 	"io" | 
 | 	"io/ioutil" | 
 | 	"log" | 
 | 	"os" | 
 | 	"path/filepath" | 
 | 	"sort" | 
 |  | 
 | 	"github.com/google/blueprint/pathtools" | 
 |  | 
 | 	"android/soong/jar" | 
 | 	"android/soong/third_party/zip" | 
 | ) | 
 |  | 
 | type fileList []string | 
 |  | 
 | func (f *fileList) String() string { | 
 | 	return `""` | 
 | } | 
 |  | 
 | func (f *fileList) Set(name string) error { | 
 | 	*f = append(*f, filepath.Clean(name)) | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | type zipsToNotStripSet map[string]bool | 
 |  | 
 | func (s zipsToNotStripSet) String() string { | 
 | 	return `""` | 
 | } | 
 |  | 
 | func (s zipsToNotStripSet) Set(zip_path string) error { | 
 | 	s[zip_path] = true | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | var ( | 
 | 	sortEntries      = flag.Bool("s", false, "sort entries (defaults to the order from the input zip files)") | 
 | 	emulateJar       = flag.Bool("j", false, "sort zip entries using jar ordering (META-INF first)") | 
 | 	emulatePar       = flag.Bool("p", false, "merge zip entries based on par format") | 
 | 	stripDirs        fileList | 
 | 	stripFiles       fileList | 
 | 	zipsToNotStrip   = make(zipsToNotStripSet) | 
 | 	stripDirEntries  = flag.Bool("D", false, "strip directory entries from the output zip file") | 
 | 	manifest         = flag.String("m", "", "manifest file to insert in jar") | 
 | 	pyMain           = flag.String("pm", "", "__main__.py file to insert in par") | 
 | 	prefix           = flag.String("prefix", "", "A file to prefix to the zip file") | 
 | 	ignoreDuplicates = flag.Bool("ignore-duplicates", false, "take each entry from the first zip it exists in and don't warn") | 
 | ) | 
 |  | 
 | func init() { | 
 | 	flag.Var(&stripDirs, "stripDir", "directories to be excluded from the output zip, accepts wildcards") | 
 | 	flag.Var(&stripFiles, "stripFile", "files to be excluded from the output zip, accepts wildcards") | 
 | 	flag.Var(&zipsToNotStrip, "zipToNotStrip", "the input zip file which is not applicable for stripping") | 
 | } | 
 |  | 
 | func main() { | 
 | 	flag.Usage = func() { | 
 | 		fmt.Fprintln(os.Stderr, "usage: merge_zips [-jpsD] [-m manifest] [--prefix script] [-pm __main__.py] output [inputs...]") | 
 | 		flag.PrintDefaults() | 
 | 	} | 
 |  | 
 | 	// parse args | 
 | 	flag.Parse() | 
 | 	args := flag.Args() | 
 | 	if len(args) < 1 { | 
 | 		flag.Usage() | 
 | 		os.Exit(1) | 
 | 	} | 
 | 	outputPath := args[0] | 
 | 	inputs := args[1:] | 
 |  | 
 | 	log.SetFlags(log.Lshortfile) | 
 |  | 
 | 	// make writer | 
 | 	output, err := os.Create(outputPath) | 
 | 	if err != nil { | 
 | 		log.Fatal(err) | 
 | 	} | 
 | 	defer output.Close() | 
 |  | 
 | 	var offset int64 | 
 | 	if *prefix != "" { | 
 | 		prefixFile, err := os.Open(*prefix) | 
 | 		if err != nil { | 
 | 			log.Fatal(err) | 
 | 		} | 
 | 		offset, err = io.Copy(output, prefixFile) | 
 | 		if err != nil { | 
 | 			log.Fatal(err) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	writer := zip.NewWriter(output) | 
 | 	defer func() { | 
 | 		err := writer.Close() | 
 | 		if err != nil { | 
 | 			log.Fatal(err) | 
 | 		} | 
 | 	}() | 
 | 	writer.SetOffset(offset) | 
 |  | 
 | 	// make readers | 
 | 	readers := []namedZipReader{} | 
 | 	for _, input := range inputs { | 
 | 		reader, err := zip.OpenReader(input) | 
 | 		if err != nil { | 
 | 			log.Fatal(err) | 
 | 		} | 
 | 		defer reader.Close() | 
 | 		namedReader := namedZipReader{path: input, reader: &reader.Reader} | 
 | 		readers = append(readers, namedReader) | 
 | 	} | 
 |  | 
 | 	if *manifest != "" && !*emulateJar { | 
 | 		log.Fatal(errors.New("must specify -j when specifying a manifest via -m")) | 
 | 	} | 
 |  | 
 | 	if *pyMain != "" && !*emulatePar { | 
 | 		log.Fatal(errors.New("must specify -p when specifying a Python __main__.py via -pm")) | 
 | 	} | 
 |  | 
 | 	// do merge | 
 | 	err = mergeZips(readers, writer, *manifest, *pyMain, *sortEntries, *emulateJar, *emulatePar, | 
 | 		*stripDirEntries, *ignoreDuplicates, []string(stripFiles), []string(stripDirs), map[string]bool(zipsToNotStrip)) | 
 | 	if err != nil { | 
 | 		log.Fatal(err) | 
 | 	} | 
 | } | 
 |  | 
 | // a namedZipReader reads a .zip file and can say which file it's reading | 
 | type namedZipReader struct { | 
 | 	path   string | 
 | 	reader *zip.Reader | 
 | } | 
 |  | 
 | // a zipEntryPath refers to a file contained in a zip | 
 | type zipEntryPath struct { | 
 | 	zipName   string | 
 | 	entryName string | 
 | } | 
 |  | 
 | func (p zipEntryPath) String() string { | 
 | 	return p.zipName + "/" + p.entryName | 
 | } | 
 |  | 
 | // a zipEntry is a zipSource that pulls its content from another zip | 
 | type zipEntry struct { | 
 | 	path    zipEntryPath | 
 | 	content *zip.File | 
 | } | 
 |  | 
 | func (ze zipEntry) String() string { | 
 | 	return ze.path.String() | 
 | } | 
 |  | 
 | func (ze zipEntry) IsDir() bool { | 
 | 	return ze.content.FileInfo().IsDir() | 
 | } | 
 |  | 
 | func (ze zipEntry) CRC32() uint32 { | 
 | 	return ze.content.FileHeader.CRC32 | 
 | } | 
 |  | 
 | func (ze zipEntry) Size() uint64 { | 
 | 	return ze.content.FileHeader.UncompressedSize64 | 
 | } | 
 |  | 
 | func (ze zipEntry) WriteToZip(dest string, zw *zip.Writer) error { | 
 | 	return zw.CopyFrom(ze.content, dest) | 
 | } | 
 |  | 
 | // a bufferEntry is a zipSource that pulls its content from a []byte | 
 | type bufferEntry struct { | 
 | 	fh      *zip.FileHeader | 
 | 	content []byte | 
 | } | 
 |  | 
 | func (be bufferEntry) String() string { | 
 | 	return "internal buffer" | 
 | } | 
 |  | 
 | func (be bufferEntry) IsDir() bool { | 
 | 	return be.fh.FileInfo().IsDir() | 
 | } | 
 |  | 
 | func (be bufferEntry) CRC32() uint32 { | 
 | 	return crc32.ChecksumIEEE(be.content) | 
 | } | 
 |  | 
 | func (be bufferEntry) Size() uint64 { | 
 | 	return uint64(len(be.content)) | 
 | } | 
 |  | 
 | func (be bufferEntry) WriteToZip(dest string, zw *zip.Writer) error { | 
 | 	w, err := zw.CreateHeader(be.fh) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	if !be.IsDir() { | 
 | 		_, err = w.Write(be.content) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | type zipSource interface { | 
 | 	String() string | 
 | 	IsDir() bool | 
 | 	CRC32() uint32 | 
 | 	Size() uint64 | 
 | 	WriteToZip(dest string, zw *zip.Writer) error | 
 | } | 
 |  | 
 | // a fileMapping specifies to copy a zip entry from one place to another | 
 | type fileMapping struct { | 
 | 	dest   string | 
 | 	source zipSource | 
 | } | 
 |  | 
 | func mergeZips(readers []namedZipReader, writer *zip.Writer, manifest, pyMain string, | 
 | 	sortEntries, emulateJar, emulatePar, stripDirEntries, ignoreDuplicates bool, | 
 | 	stripFiles, stripDirs []string, zipsToNotStrip map[string]bool) error { | 
 |  | 
 | 	sourceByDest := make(map[string]zipSource, 0) | 
 | 	orderedMappings := []fileMapping{} | 
 |  | 
 | 	// if dest already exists returns a non-null zipSource for the existing source | 
 | 	addMapping := func(dest string, source zipSource) zipSource { | 
 | 		mapKey := filepath.Clean(dest) | 
 | 		if existingSource, exists := sourceByDest[mapKey]; exists { | 
 | 			return existingSource | 
 | 		} | 
 |  | 
 | 		sourceByDest[mapKey] = source | 
 | 		orderedMappings = append(orderedMappings, fileMapping{source: source, dest: dest}) | 
 | 		return nil | 
 | 	} | 
 |  | 
 | 	if manifest != "" { | 
 | 		if !stripDirEntries { | 
 | 			dirHeader := jar.MetaDirFileHeader() | 
 | 			dirSource := bufferEntry{dirHeader, nil} | 
 | 			addMapping(jar.MetaDir, dirSource) | 
 | 		} | 
 |  | 
 | 		contents, err := ioutil.ReadFile(manifest) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 |  | 
 | 		fh, buf, err := jar.ManifestFileContents(contents) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 |  | 
 | 		fileSource := bufferEntry{fh, buf} | 
 | 		addMapping(jar.ManifestFile, fileSource) | 
 | 	} | 
 |  | 
 | 	if pyMain != "" { | 
 | 		buf, err := ioutil.ReadFile(pyMain) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 | 		fh := &zip.FileHeader{ | 
 | 			Name:               "__main__.py", | 
 | 			Method:             zip.Store, | 
 | 			UncompressedSize64: uint64(len(buf)), | 
 | 		} | 
 | 		fh.SetMode(0700) | 
 | 		fh.SetModTime(jar.DefaultTime) | 
 | 		fileSource := bufferEntry{fh, buf} | 
 | 		addMapping("__main__.py", fileSource) | 
 | 	} | 
 |  | 
 | 	if emulatePar { | 
 | 		// the runfiles packages needs to be populated with "__init__.py". | 
 | 		newPyPkgs := []string{} | 
 | 		// the runfiles dirs have been treated as packages. | 
 | 		existingPyPkgSet := make(map[string]bool) | 
 | 		// put existing __init__.py files to a set first. This set is used for preventing | 
 | 		// generated __init__.py files from overwriting existing ones. | 
 | 		for _, namedReader := range readers { | 
 | 			for _, file := range namedReader.reader.File { | 
 | 				if filepath.Base(file.Name) != "__init__.py" { | 
 | 					continue | 
 | 				} | 
 | 				pyPkg := pathBeforeLastSlash(file.Name) | 
 | 				if _, found := existingPyPkgSet[pyPkg]; found { | 
 | 					panic(fmt.Errorf("found __init__.py path duplicates during pars merging: %q.", file.Name)) | 
 | 				} else { | 
 | 					existingPyPkgSet[pyPkg] = true | 
 | 				} | 
 | 			} | 
 | 		} | 
 | 		for _, namedReader := range readers { | 
 | 			for _, file := range namedReader.reader.File { | 
 | 				var parentPath string /* the path after trimming last "/" */ | 
 | 				if filepath.Base(file.Name) == "__init__.py" { | 
 | 					// for existing __init__.py files, we should trim last "/" for twice. | 
 | 					// eg. a/b/c/__init__.py ---> a/b | 
 | 					parentPath = pathBeforeLastSlash(pathBeforeLastSlash(file.Name)) | 
 | 				} else { | 
 | 					parentPath = pathBeforeLastSlash(file.Name) | 
 | 				} | 
 | 				populateNewPyPkgs(parentPath, existingPyPkgSet, &newPyPkgs) | 
 | 			} | 
 | 		} | 
 | 		for _, pkg := range newPyPkgs { | 
 | 			var emptyBuf []byte | 
 | 			fh := &zip.FileHeader{ | 
 | 				Name:               filepath.Join(pkg, "__init__.py"), | 
 | 				Method:             zip.Store, | 
 | 				UncompressedSize64: uint64(len(emptyBuf)), | 
 | 			} | 
 | 			fh.SetMode(0700) | 
 | 			fh.SetModTime(jar.DefaultTime) | 
 | 			fileSource := bufferEntry{fh, emptyBuf} | 
 | 			addMapping(filepath.Join(pkg, "__init__.py"), fileSource) | 
 | 		} | 
 | 	} | 
 | 	for _, namedReader := range readers { | 
 | 		_, skipStripThisZip := zipsToNotStrip[namedReader.path] | 
 | 		for _, file := range namedReader.reader.File { | 
 | 			if !skipStripThisZip { | 
 | 				if skip, err := shouldStripEntry(emulateJar, stripFiles, stripDirs, file.Name); err != nil { | 
 | 					return err | 
 | 				} else if skip { | 
 | 					continue | 
 | 				} | 
 | 			} | 
 |  | 
 | 			if stripDirEntries && file.FileInfo().IsDir() { | 
 | 				continue | 
 | 			} | 
 |  | 
 | 			// check for other files or directories destined for the same path | 
 | 			dest := file.Name | 
 |  | 
 | 			// make a new entry to add | 
 | 			source := zipEntry{path: zipEntryPath{zipName: namedReader.path, entryName: file.Name}, content: file} | 
 |  | 
 | 			if existingSource := addMapping(dest, source); existingSource != nil { | 
 | 				// handle duplicates | 
 | 				if existingSource.IsDir() != source.IsDir() { | 
 | 					return fmt.Errorf("Directory/file mismatch at %v from %v and %v\n", | 
 | 						dest, existingSource, source) | 
 | 				} | 
 |  | 
 | 				if ignoreDuplicates { | 
 | 					continue | 
 | 				} | 
 |  | 
 | 				if emulateJar && | 
 | 					file.Name == jar.ManifestFile || file.Name == jar.ModuleInfoClass { | 
 | 					// Skip manifest and module info files that are not from the first input file | 
 | 					continue | 
 | 				} | 
 |  | 
 | 				if source.IsDir() { | 
 | 					continue | 
 | 				} | 
 |  | 
 | 				if existingSource.CRC32() == source.CRC32() && existingSource.Size() == source.Size() { | 
 | 					continue | 
 | 				} | 
 |  | 
 | 				return fmt.Errorf("Duplicate path %v found in %v and %v\n", | 
 | 					dest, existingSource, source) | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	if emulateJar { | 
 | 		jarSort(orderedMappings) | 
 | 	} else if sortEntries { | 
 | 		alphanumericSort(orderedMappings) | 
 | 	} | 
 |  | 
 | 	for _, entry := range orderedMappings { | 
 | 		if err := entry.source.WriteToZip(entry.dest, writer); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | // Sets the given directory and all its ancestor directories as Python packages. | 
 | func populateNewPyPkgs(pkgPath string, existingPyPkgSet map[string]bool, newPyPkgs *[]string) { | 
 | 	for pkgPath != "" { | 
 | 		if _, found := existingPyPkgSet[pkgPath]; !found { | 
 | 			existingPyPkgSet[pkgPath] = true | 
 | 			*newPyPkgs = append(*newPyPkgs, pkgPath) | 
 | 			// Gets its ancestor directory by trimming last slash. | 
 | 			pkgPath = pathBeforeLastSlash(pkgPath) | 
 | 		} else { | 
 | 			break | 
 | 		} | 
 | 	} | 
 | } | 
 |  | 
 | func pathBeforeLastSlash(path string) string { | 
 | 	ret := filepath.Dir(path) | 
 | 	// filepath.Dir("abc") -> "." and filepath.Dir("/abc") -> "/". | 
 | 	if ret == "." || ret == "/" { | 
 | 		return "" | 
 | 	} | 
 | 	return ret | 
 | } | 
 |  | 
 | func shouldStripEntry(emulateJar bool, stripFiles, stripDirs []string, name string) (bool, error) { | 
 | 	for _, dir := range stripDirs { | 
 | 		dir = filepath.Clean(dir) | 
 | 		patterns := []string{ | 
 | 			dir + "/",      // the directory itself | 
 | 			dir + "/**/*",  // files recursively in the directory | 
 | 			dir + "/**/*/", // directories recursively in the directory | 
 | 		} | 
 |  | 
 | 		for _, pattern := range patterns { | 
 | 			match, err := pathtools.Match(pattern, name) | 
 | 			if err != nil { | 
 | 				return false, fmt.Errorf("%s: %s", err.Error(), pattern) | 
 | 			} else if match { | 
 | 				if emulateJar { | 
 | 					// When merging jar files, don't strip META-INF/MANIFEST.MF even if stripping META-INF is | 
 | 					// requested. | 
 | 					// TODO(ccross): which files does this affect? | 
 | 					if name != jar.MetaDir && name != jar.ManifestFile { | 
 | 						return true, nil | 
 | 					} | 
 | 				} | 
 | 				return true, nil | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	for _, pattern := range stripFiles { | 
 | 		if match, err := pathtools.Match(pattern, name); err != nil { | 
 | 			return false, fmt.Errorf("%s: %s", err.Error(), pattern) | 
 | 		} else if match { | 
 | 			return true, nil | 
 | 		} | 
 | 	} | 
 | 	return false, nil | 
 | } | 
 |  | 
 | func jarSort(files []fileMapping) { | 
 | 	sort.SliceStable(files, func(i, j int) bool { | 
 | 		return jar.EntryNamesLess(files[i].dest, files[j].dest) | 
 | 	}) | 
 | } | 
 |  | 
 | func alphanumericSort(files []fileMapping) { | 
 | 	sort.SliceStable(files, func(i, j int) bool { | 
 | 		return files[i].dest < files[j].dest | 
 | 	}) | 
 | } |