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