blob: 0d6ce81f874cce8d1a4e92d43c4deec1f7f5b7a7 [file]
// Copyright 2018 The Bazel Authors. 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 repack provides functionality to repack zip/jar/apk archives.
package repack
import (
"archive/zip"
"flag"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync"
"src/common/golang/flags"
"src/tools/ak/types"
)
var (
// Cmd defines the command to run repack
Cmd = types.Command{
Init: Init,
Run: Run,
Desc: desc,
Flags: []string{
"in",
"dir",
"out",
"filtered_out",
"filter_r",
"filter_jar_res",
"filter_manifest",
"compress",
"remove_dirs",
},
}
// Variables that hold flag values
in flags.StringList
dir flags.StringList
out string
filteredOut string
filterR bool
filterJarRes bool
filterManifest bool
compress bool
removeDirs bool
b2i = map[bool]int8{false: 0, true: 1}
seen = make(map[string]bool)
initOnce sync.Once
)
// Init initializes repack.
func Init() {
initOnce.Do(func() {
flag.Var(&in, "in", "Path to input(s), must be a zip archive.")
flag.Var(&dir, "dir", "Path to directories to pack in the zip.")
flag.StringVar(&out, "out", "", "Path to output.")
flag.StringVar(&filteredOut, "filtered_out", "", "(optional) Path to output for filtered files.")
flag.BoolVar(&filterR, "filter_r", false, "Whether to filter R classes or not.")
flag.BoolVar(&filterJarRes, "filter_jar_res", false, "Whether to filter java resources or not.")
flag.BoolVar(&filterManifest, "filter_manifest", false, "Whether to filter AndroidManifest.xml or not.")
flag.BoolVar(&compress, "compress", false, "Whether to compress or just store files in all outputs.")
flag.BoolVar(&removeDirs, "remove_dirs", true, "Whether to remove directory entries or not.")
})
}
func desc() string {
return "Repack zip/jar/apk archives."
}
type filterFunc func(name string) bool
func filterNone(name string) bool {
return false
}
func isRClass(name string) bool {
return strings.HasSuffix(name, "/R.class") || strings.Contains(name, "/R$")
}
func isJavaRes(name string) bool {
return !(strings.HasSuffix(name, ".class") || strings.HasPrefix(name, "META-INF/"))
}
func isManifest(name string) bool {
return name == "AndroidManifest.xml"
}
func repackZip(in *zip.Reader, out *zip.Writer, filteredZipOut *zip.Writer, filter filterFunc, method uint16) error {
for _, f := range in.File {
if removeDirs && strings.HasSuffix(f.Name, "/") {
continue
}
reader, err := f.Open()
if err != nil {
return err
}
if err := writeToZip(f.Name, reader, out, filteredZipOut, filter, method); err != nil {
return err
}
if err := reader.Close(); err != nil {
return err
}
}
return nil
}
func repackDir(dir string, out *zip.Writer, filteredZipOut *zip.Writer, filter filterFunc, method uint16) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
name, err := filepath.Rel(dir, path)
if info.IsDir() {
if removeDirs {
return nil
}
name += "/"
}
reader, err := os.Open(path)
if err != nil {
return err
}
defer reader.Close()
return writeToZip(name, reader, out, filteredZipOut, filter, method)
})
}
func writeToZip(name string, in io.Reader, out, filteredZipOut *zip.Writer, filter filterFunc, method uint16) error {
if seen[name] {
return nil
}
seen[name] = true
if filter(name) {
if filteredZipOut != nil {
write(filteredZipOut, in, name, method)
}
return nil
}
return write(out, in, name, method)
}
func write(out *zip.Writer, in io.Reader, name string, method uint16) error {
writer, err := out.CreateHeader(&zip.FileHeader{
Name: name,
Method: method,
})
if err != nil {
return err
}
// Only files have data, io.Copy will fail for directories. Header entry is required for both.
if !strings.HasSuffix(name, "/") {
if _, err := io.Copy(writer, in); err != nil {
return err
}
}
return nil
}
// Run is the entry point for repack.
func Run() {
if in == nil && dir == nil {
log.Fatal("Flags -in or -dir must be specified.")
}
if out == "" {
log.Fatal("Flags -out must be specified.")
}
if b2i[filterR]+b2i[filterJarRes]+b2i[filterManifest] > 1 {
log.Fatal("Only one filter is allowed.")
}
filter := filterNone
if filterR {
filter = isRClass
} else if filterJarRes {
filter = isJavaRes
} else if filterManifest {
filter = isManifest
}
w, err := os.Create(out)
if err != nil {
log.Fatalf("os.Create(%q) failed: %v", out, err)
}
defer w.Close()
zipOut := zip.NewWriter(w)
defer zipOut.Close()
var filteredZipOut *zip.Writer
if filteredOut != "" {
w, err := os.Create(filteredOut)
if err != nil {
log.Fatalf("os.Create(%q) failed: %v", filteredOut, err)
}
defer w.Close()
filteredZipOut = zip.NewWriter(w)
defer filteredZipOut.Close()
}
method := zip.Store
if compress {
method = zip.Deflate
}
for _, d := range dir {
if err := repackDir(d, zipOut, filteredZipOut, filter, method); err != nil {
log.Fatal(err)
}
}
for _, f := range in {
file, err := os.Open(f)
if err != nil {
log.Fatalf("os.Open(%q) failed: %v", f, err)
}
fi, err := file.Stat()
if err != nil {
file.Close()
log.Fatalf("File.Stat() failed for %q: %v", f, err)
}
size := fi.Size()
// Skip empty Zip archives. An empty zip is 22 bytes contains only an EOCD.
// https://en.wikipedia.org/wiki/Zip_(file_format)#Limits
if size <= 22 {
continue
}
zipIn, err := zip.NewReader(file, size)
if err != nil {
file.Close()
log.Fatalf("zip.OpenReader(%q) failed: %v", f, err)
}
err = repackZip(zipIn, zipOut, filteredZipOut, filter, method)
file.Close()
if err != nil {
log.Fatal(err)
}
}
}