xref: /aosp_15_r20/external/bazelbuild-rules_android/src/tools/ak/repack/repack.go (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
1// Copyright 2018 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package repack provides functionality to repack zip/jar/apk archives.
16package repack
17
18import (
19	"archive/zip"
20	"flag"
21	"io"
22	"log"
23	"os"
24	"path/filepath"
25	"strings"
26	"sync"
27
28	"src/common/golang/flags"
29	"src/tools/ak/types"
30)
31
32var (
33	// Cmd defines the command to run repack
34	Cmd = types.Command{
35		Init: Init,
36		Run:  Run,
37		Desc: desc,
38		Flags: []string{
39			"in",
40			"dir",
41			"out",
42			"filtered_out",
43			"filter_r",
44			"filter_jar_res",
45			"filter_manifest",
46			"compress",
47			"remove_dirs",
48		},
49	}
50
51	// Variables that hold flag values
52	in             flags.StringList
53	dir            flags.StringList
54	out            string
55	filteredOut    string
56	filterR        bool
57	filterJarRes   bool
58	filterManifest bool
59	compress       bool
60	removeDirs     bool
61
62	b2i      = map[bool]int8{false: 0, true: 1}
63	seen     = make(map[string]bool)
64	initOnce sync.Once
65)
66
67// Init initializes repack.
68func Init() {
69	initOnce.Do(func() {
70		flag.Var(&in, "in", "Path to input(s), must be a zip archive.")
71		flag.Var(&dir, "dir", "Path to directories to pack in the zip.")
72		flag.StringVar(&out, "out", "", "Path to output.")
73		flag.StringVar(&filteredOut, "filtered_out", "", "(optional) Path to output for filtered files.")
74		flag.BoolVar(&filterR, "filter_r", false, "Whether to filter R classes or not.")
75		flag.BoolVar(&filterJarRes, "filter_jar_res", false, "Whether to filter java resources or not.")
76		flag.BoolVar(&filterManifest, "filter_manifest", false, "Whether to filter AndroidManifest.xml or not.")
77		flag.BoolVar(&compress, "compress", false, "Whether to compress or just store files in all outputs.")
78		flag.BoolVar(&removeDirs, "remove_dirs", true, "Whether to remove directory entries or not.")
79	})
80}
81
82func desc() string {
83	return "Repack zip/jar/apk archives."
84}
85
86type filterFunc func(name string) bool
87
88func filterNone(name string) bool {
89	return false
90}
91
92func isRClass(name string) bool {
93	return strings.HasSuffix(name, "/R.class") || strings.Contains(name, "/R$")
94}
95
96func isJavaRes(name string) bool {
97	return !(strings.HasSuffix(name, ".class") || strings.HasPrefix(name, "META-INF/"))
98}
99
100func isManifest(name string) bool {
101	return name == "AndroidManifest.xml"
102}
103
104func repackZip(in *zip.Reader, out *zip.Writer, filteredZipOut *zip.Writer, filter filterFunc, method uint16) error {
105	for _, f := range in.File {
106		if removeDirs && strings.HasSuffix(f.Name, "/") {
107			continue
108		}
109		reader, err := f.Open()
110		if err != nil {
111			return err
112		}
113		if err := writeToZip(f.Name, reader, out, filteredZipOut, filter, method); err != nil {
114			return err
115		}
116		if err := reader.Close(); err != nil {
117			return err
118		}
119	}
120	return nil
121}
122
123func repackDir(dir string, out *zip.Writer, filteredZipOut *zip.Writer, filter filterFunc, method uint16) error {
124	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
125		if err != nil {
126			return err
127		}
128
129		name, err := filepath.Rel(dir, path)
130		if info.IsDir() {
131			if removeDirs {
132				return nil
133			}
134			name += "/"
135		}
136
137		reader, err := os.Open(path)
138		if err != nil {
139			return err
140		}
141		defer reader.Close()
142
143		return writeToZip(name, reader, out, filteredZipOut, filter, method)
144	})
145}
146
147func writeToZip(name string, in io.Reader, out, filteredZipOut *zip.Writer, filter filterFunc, method uint16) error {
148	if seen[name] {
149		return nil
150	}
151	seen[name] = true
152
153	if filter(name) {
154		if filteredZipOut != nil {
155			write(filteredZipOut, in, name, method)
156		}
157		return nil
158	}
159	return write(out, in, name, method)
160}
161
162func write(out *zip.Writer, in io.Reader, name string, method uint16) error {
163	writer, err := out.CreateHeader(&zip.FileHeader{
164		Name:   name,
165		Method: method,
166	})
167	if err != nil {
168		return err
169	}
170
171	// Only files have data, io.Copy will fail for directories. Header entry is required for both.
172	if !strings.HasSuffix(name, "/") {
173		if _, err := io.Copy(writer, in); err != nil {
174			return err
175		}
176	}
177	return nil
178}
179
180// Run is the entry point for repack.
181func Run() {
182	if in == nil && dir == nil {
183		log.Fatal("Flags -in or -dir must be specified.")
184	}
185	if out == "" {
186		log.Fatal("Flags -out must be specified.")
187	}
188
189	if b2i[filterR]+b2i[filterJarRes]+b2i[filterManifest] > 1 {
190		log.Fatal("Only one filter is allowed.")
191	}
192
193	filter := filterNone
194	if filterR {
195		filter = isRClass
196	} else if filterJarRes {
197		filter = isJavaRes
198	} else if filterManifest {
199		filter = isManifest
200	}
201
202	w, err := os.Create(out)
203	if err != nil {
204		log.Fatalf("os.Create(%q) failed: %v", out, err)
205	}
206	defer w.Close()
207
208	zipOut := zip.NewWriter(w)
209	defer zipOut.Close()
210
211	var filteredZipOut *zip.Writer
212	if filteredOut != "" {
213		w, err := os.Create(filteredOut)
214		if err != nil {
215			log.Fatalf("os.Create(%q) failed: %v", filteredOut, err)
216		}
217		defer w.Close()
218		filteredZipOut = zip.NewWriter(w)
219		defer filteredZipOut.Close()
220	}
221
222	method := zip.Store
223	if compress {
224		method = zip.Deflate
225	}
226
227	for _, d := range dir {
228		if err := repackDir(d, zipOut, filteredZipOut, filter, method); err != nil {
229			log.Fatal(err)
230		}
231	}
232
233	for _, f := range in {
234		file, err := os.Open(f)
235		if err != nil {
236			log.Fatalf("os.Open(%q) failed: %v", f, err)
237		}
238		fi, err := file.Stat()
239		if err != nil {
240			file.Close()
241			log.Fatalf("File.Stat() failed for %q: %v", f, err)
242		}
243		size := fi.Size()
244		// Skip empty Zip archives. An empty zip is 22 bytes contains only an EOCD.
245		// https://en.wikipedia.org/wiki/Zip_(file_format)#Limits
246		if size <= 22 {
247			continue
248		}
249		zipIn, err := zip.NewReader(file, size)
250		if err != nil {
251			file.Close()
252			log.Fatalf("zip.OpenReader(%q) failed: %v", f, err)
253		}
254
255		err = repackZip(zipIn, zipOut, filteredZipOut, filter, method)
256		file.Close()
257		if err != nil {
258			log.Fatal(err)
259		}
260	}
261}
262