1// Copyright 2016 Google Inc. 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
15package main
16
17import (
18	"flag"
19	"fmt"
20	"io"
21	"log"
22	"os"
23	"path/filepath"
24	"sort"
25	"strings"
26	"time"
27
28	"github.com/google/blueprint/pathtools"
29
30	"android/soong/jar"
31	"android/soong/third_party/zip"
32)
33
34var (
35	input     = flag.String("i", "", "zip file to read from")
36	output    = flag.String("o", "", "output file")
37	sortGlobs = flag.Bool("s", false, "sort matches from each glob (defaults to the order from the input zip file)")
38	sortJava  = flag.Bool("j", false, "sort using jar ordering within each glob (META-INF/MANIFEST.MF first)")
39	setTime   = flag.Bool("t", false, "set timestamps to 2009-01-01 00:00:00")
40
41	staticTime = time.Date(2009, 1, 1, 0, 0, 0, 0, time.UTC)
42
43	excludes   multiFlag
44	includes   multiFlag
45	uncompress multiFlag
46)
47
48func init() {
49	flag.Var(&excludes, "x", "exclude a filespec from the output")
50	flag.Var(&includes, "X", "include a filespec in the output that was previously excluded")
51	flag.Var(&uncompress, "0", "convert a filespec to uncompressed in the output")
52}
53
54func main() {
55	flag.Usage = func() {
56		fmt.Fprintln(os.Stderr, "usage: zip2zip -i zipfile -o zipfile [-s|-j] [-t] [filespec]...")
57		flag.PrintDefaults()
58		fmt.Fprintln(os.Stderr, "  filespec:")
59		fmt.Fprintln(os.Stderr, "    <name>")
60		fmt.Fprintln(os.Stderr, "    <in_name>:<out_name>")
61		fmt.Fprintln(os.Stderr, "    <glob>[:<out_dir>]")
62		fmt.Fprintln(os.Stderr, "")
63		fmt.Fprintln(os.Stderr, "<glob> uses the rules at https://godoc.org/github.com/google/blueprint/pathtools/#Match")
64		fmt.Fprintln(os.Stderr, "")
65		fmt.Fprintln(os.Stderr, "Files will be copied with their existing compression from the input zipfile to")
66		fmt.Fprintln(os.Stderr, "the output zipfile, in the order of filespec arguments.")
67		fmt.Fprintln(os.Stderr, "")
68		fmt.Fprintln(os.Stderr, "If no filepsec is provided all files and directories are copied.")
69	}
70
71	flag.Parse()
72
73	if *input == "" || *output == "" {
74		flag.Usage()
75		os.Exit(1)
76	}
77
78	log.SetFlags(log.Lshortfile)
79
80	reader, err := zip.OpenReader(*input)
81	if err != nil {
82		log.Fatal(err)
83	}
84	defer reader.Close()
85
86	output, err := os.Create(*output)
87	if err != nil {
88		log.Fatal(err)
89	}
90	defer output.Close()
91
92	writer := zip.NewWriter(output)
93	defer func() {
94		err := writer.Close()
95		if err != nil {
96			log.Fatal(err)
97		}
98	}()
99
100	if err := zip2zip(&reader.Reader, writer, *sortGlobs, *sortJava, *setTime,
101		flag.Args(), excludes, includes, uncompress); err != nil {
102
103		log.Fatal(err)
104	}
105}
106
107type pair struct {
108	*zip.File
109	newName    string
110	uncompress bool
111}
112
113func zip2zip(reader *zip.Reader, writer *zip.Writer, sortOutput, sortJava, setTime bool,
114	args []string, excludes, includes multiFlag, uncompresses []string) error {
115
116	matches := []pair{}
117
118	sortMatches := func(matches []pair) {
119		if sortJava {
120			sort.SliceStable(matches, func(i, j int) bool {
121				return jar.EntryNamesLess(matches[i].newName, matches[j].newName)
122			})
123		} else if sortOutput {
124			sort.SliceStable(matches, func(i, j int) bool {
125				return matches[i].newName < matches[j].newName
126			})
127		}
128	}
129
130	for _, arg := range args {
131		// Reserve escaping for future implementation, so make sure no
132		// one is using \ and expecting a certain behavior.
133		if strings.Contains(arg, "\\") {
134			return fmt.Errorf("\\ characters are not currently supported")
135		}
136
137		input, output := includeSplit(arg)
138
139		var includeMatches []pair
140
141		for _, file := range reader.File {
142			var newName string
143			if match, err := pathtools.Match(input, file.Name); err != nil {
144				return err
145			} else if match {
146				if output == "" {
147					newName = file.Name
148				} else {
149					if pathtools.IsGlob(input) {
150						// If the input is a glob then the output is a directory.
151						rel, err := filepath.Rel(constantPartOfPattern(input), file.Name)
152						if err != nil {
153							return err
154						} else if strings.HasPrefix("../", rel) {
155							return fmt.Errorf("globbed path %q was not in %q", file.Name, constantPartOfPattern(input))
156						}
157						newName = filepath.Join(output, rel)
158					} else {
159						// Otherwise it is a file.
160						newName = output
161					}
162				}
163				includeMatches = append(includeMatches, pair{file, newName, false})
164			}
165		}
166
167		sortMatches(includeMatches)
168		matches = append(matches, includeMatches...)
169	}
170
171	if len(args) == 0 {
172		// implicitly match everything
173		for _, file := range reader.File {
174			matches = append(matches, pair{file, file.Name, false})
175		}
176		sortMatches(matches)
177	}
178
179	var matchesAfterExcludes []pair
180	seen := make(map[string]*zip.File)
181
182	for _, match := range matches {
183		// Filter out matches whose original file name matches an exclude filter, unless it also matches an
184		// include filter
185		if exclude, err := excludes.Match(match.File.Name); err != nil {
186			return err
187		} else if exclude {
188			if include, err := includes.Match(match.File.Name); err != nil {
189				return err
190			} else if !include {
191				continue
192			}
193		}
194
195		// Check for duplicate output names, ignoring ones that come from the same input zip entry.
196		if prev, exists := seen[match.newName]; exists {
197			if prev != match.File {
198				return fmt.Errorf("multiple entries for %q with different contents", match.newName)
199			}
200			continue
201		}
202		seen[match.newName] = match.File
203
204		for _, u := range uncompresses {
205			if uncompressMatch, err := pathtools.Match(u, match.newName); err != nil {
206				return err
207			} else if uncompressMatch {
208				match.uncompress = true
209				break
210			}
211		}
212
213		matchesAfterExcludes = append(matchesAfterExcludes, match)
214	}
215
216	for _, match := range matchesAfterExcludes {
217		if setTime {
218			match.File.SetModTime(staticTime)
219		}
220		if match.uncompress && match.File.FileHeader.Method != zip.Store {
221			fh := match.File.FileHeader
222			fh.Name = match.newName
223			fh.Method = zip.Store
224			fh.CompressedSize64 = fh.UncompressedSize64
225
226			zw, err := writer.CreateHeaderAndroid(&fh)
227			if err != nil {
228				return err
229			}
230
231			zr, err := match.File.Open()
232			if err != nil {
233				return err
234			}
235
236			_, err = io.Copy(zw, zr)
237			zr.Close()
238			if err != nil {
239				return err
240			}
241		} else {
242			err := writer.CopyFrom(match.File, match.newName)
243			if err != nil {
244				return err
245			}
246		}
247	}
248
249	return nil
250}
251
252func includeSplit(s string) (string, string) {
253	split := strings.SplitN(s, ":", 2)
254	if len(split) == 2 {
255		return split[0], split[1]
256	} else {
257		return split[0], ""
258	}
259}
260
261type multiFlag []string
262
263func (m *multiFlag) String() string {
264	return strings.Join(*m, " ")
265}
266
267func (m *multiFlag) Set(s string) error {
268	*m = append(*m, s)
269	return nil
270}
271
272func (m *multiFlag) Match(s string) (bool, error) {
273	if m == nil {
274		return false, nil
275	}
276	for _, f := range *m {
277		if match, err := pathtools.Match(f, s); err != nil {
278			return false, err
279		} else if match {
280			return true, nil
281		}
282	}
283	return false, nil
284}
285
286func constantPartOfPattern(pattern string) string {
287	ret := ""
288	for pattern != "" {
289		var first string
290		first, pattern = splitFirst(pattern)
291		if pathtools.IsGlob(first) {
292			return ret
293		}
294		ret = filepath.Join(ret, first)
295	}
296	return ret
297}
298
299func splitFirst(path string) (string, string) {
300	i := strings.IndexRune(path, filepath.Separator)
301	if i < 0 {
302		return path, ""
303	}
304	return path[:i], path[i+1:]
305}
306