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		input, output := includeSplit(arg)
132
133		var includeMatches []pair
134
135		for _, file := range reader.File {
136			var newName string
137			if match, err := pathtools.Match(input, file.Name); err != nil {
138				return err
139			} else if match {
140				if output == "" {
141					newName = file.Name
142				} else {
143					if pathtools.IsGlob(input) {
144						// If the input is a glob then the output is a directory.
145						rel, err := filepath.Rel(constantPartOfPattern(input), file.Name)
146						if err != nil {
147							return err
148						} else if strings.HasPrefix("../", rel) {
149							return fmt.Errorf("globbed path %q was not in %q", file.Name, constantPartOfPattern(input))
150						}
151						newName = filepath.Join(output, rel)
152					} else {
153						// Otherwise it is a file.
154						newName = output
155					}
156				}
157				includeMatches = append(includeMatches, pair{file, newName, false})
158			}
159		}
160
161		sortMatches(includeMatches)
162		matches = append(matches, includeMatches...)
163	}
164
165	if len(args) == 0 {
166		// implicitly match everything
167		for _, file := range reader.File {
168			matches = append(matches, pair{file, file.Name, false})
169		}
170		sortMatches(matches)
171	}
172
173	var matchesAfterExcludes []pair
174	seen := make(map[string]*zip.File)
175
176	for _, match := range matches {
177		// Filter out matches whose original file name matches an exclude filter, unless it also matches an
178		// include filter
179		if exclude, err := excludes.Match(match.File.Name); err != nil {
180			return err
181		} else if exclude {
182			if include, err := includes.Match(match.File.Name); err != nil {
183				return err
184			} else if !include {
185				continue
186			}
187		}
188
189		// Check for duplicate output names, ignoring ones that come from the same input zip entry.
190		if prev, exists := seen[match.newName]; exists {
191			if prev != match.File {
192				return fmt.Errorf("multiple entries for %q with different contents", match.newName)
193			}
194			continue
195		}
196		seen[match.newName] = match.File
197
198		for _, u := range uncompresses {
199			if uncompressMatch, err := pathtools.Match(u, match.newName); err != nil {
200				return err
201			} else if uncompressMatch {
202				match.uncompress = true
203				break
204			}
205		}
206
207		matchesAfterExcludes = append(matchesAfterExcludes, match)
208	}
209
210	for _, match := range matchesAfterExcludes {
211		if setTime {
212			match.File.SetModTime(staticTime)
213		}
214		if match.uncompress && match.File.FileHeader.Method != zip.Store {
215			fh := match.File.FileHeader
216			fh.Name = match.newName
217			fh.Method = zip.Store
218			fh.CompressedSize64 = fh.UncompressedSize64
219
220			zw, err := writer.CreateHeaderAndroid(&fh)
221			if err != nil {
222				return err
223			}
224
225			zr, err := match.File.Open()
226			if err != nil {
227				return err
228			}
229
230			_, err = io.Copy(zw, zr)
231			zr.Close()
232			if err != nil {
233				return err
234			}
235		} else {
236			err := writer.CopyFrom(match.File, match.newName)
237			if err != nil {
238				return err
239			}
240		}
241	}
242
243	return nil
244}
245
246func includeSplit(s string) (string, string) {
247	split := strings.SplitN(s, ":", 2)
248	if len(split) == 2 {
249		return split[0], split[1]
250	} else {
251		return split[0], ""
252	}
253}
254
255type multiFlag []string
256
257func (m *multiFlag) String() string {
258	return strings.Join(*m, " ")
259}
260
261func (m *multiFlag) Set(s string) error {
262	*m = append(*m, s)
263	return nil
264}
265
266func (m *multiFlag) Match(s string) (bool, error) {
267	if m == nil {
268		return false, nil
269	}
270	for _, f := range *m {
271		if match, err := pathtools.Match(f, s); err != nil {
272			return false, err
273		} else if match {
274			return true, nil
275		}
276	}
277	return false, nil
278}
279
280func constantPartOfPattern(pattern string) string {
281	ret := ""
282	for pattern != "" {
283		var first string
284		first, pattern = splitFirst(pattern)
285		if pathtools.IsGlob(first) {
286			return ret
287		}
288		ret = filepath.Join(ret, first)
289	}
290	return ret
291}
292
293func splitFirst(path string) (string, string) {
294	i := strings.IndexRune(path, filepath.Separator)
295	if i < 0 {
296		return path, ""
297	}
298	return path[:i], path[i+1:]
299}
300