| // Copyright 2016 Google Inc. 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 main |
| |
| import ( |
| "flag" |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/google/blueprint/pathtools" |
| |
| "android/soong/jar" |
| "android/soong/third_party/zip" |
| ) |
| |
| var ( |
| input = flag.String("i", "", "zip file to read from") |
| output = flag.String("o", "", "output file") |
| sortGlobs = flag.Bool("s", false, "sort matches from each glob (defaults to the order from the input zip file)") |
| sortJava = flag.Bool("j", false, "sort using jar ordering within each glob (META-INF/MANIFEST.MF first)") |
| setTime = flag.Bool("t", false, "set timestamps to 2009-01-01 00:00:00") |
| |
| staticTime = time.Date(2009, 1, 1, 0, 0, 0, 0, time.UTC) |
| |
| excludes multiFlag |
| includes multiFlag |
| uncompress multiFlag |
| ) |
| |
| func init() { |
| flag.Var(&excludes, "x", "exclude a filespec from the output") |
| flag.Var(&includes, "X", "include a filespec in the output that was previously excluded") |
| flag.Var(&uncompress, "0", "convert a filespec to uncompressed in the output") |
| } |
| |
| func main() { |
| flag.Usage = func() { |
| fmt.Fprintln(os.Stderr, "usage: zip2zip -i zipfile -o zipfile [-s|-j] [-t] [filespec]...") |
| flag.PrintDefaults() |
| fmt.Fprintln(os.Stderr, " filespec:") |
| fmt.Fprintln(os.Stderr, " <name>") |
| fmt.Fprintln(os.Stderr, " <in_name>:<out_name>") |
| fmt.Fprintln(os.Stderr, " <glob>[:<out_dir>]") |
| fmt.Fprintln(os.Stderr, "") |
| fmt.Fprintln(os.Stderr, "<glob> uses the rules at https://godoc.org/github.com/google/blueprint/pathtools/#Match") |
| fmt.Fprintln(os.Stderr, "") |
| fmt.Fprintln(os.Stderr, "Files will be copied with their existing compression from the input zipfile to") |
| fmt.Fprintln(os.Stderr, "the output zipfile, in the order of filespec arguments.") |
| fmt.Fprintln(os.Stderr, "") |
| fmt.Fprintln(os.Stderr, "If no filepsec is provided all files and directories are copied.") |
| } |
| |
| flag.Parse() |
| |
| if *input == "" || *output == "" { |
| flag.Usage() |
| os.Exit(1) |
| } |
| |
| log.SetFlags(log.Lshortfile) |
| |
| reader, err := zip.OpenReader(*input) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer reader.Close() |
| |
| output, err := os.Create(*output) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer output.Close() |
| |
| writer := zip.NewWriter(output) |
| defer func() { |
| err := writer.Close() |
| if err != nil { |
| log.Fatal(err) |
| } |
| }() |
| |
| if err := zip2zip(&reader.Reader, writer, *sortGlobs, *sortJava, *setTime, |
| flag.Args(), excludes, includes, uncompress); err != nil { |
| |
| log.Fatal(err) |
| } |
| } |
| |
| type pair struct { |
| *zip.File |
| newName string |
| uncompress bool |
| } |
| |
| func zip2zip(reader *zip.Reader, writer *zip.Writer, sortOutput, sortJava, setTime bool, |
| args []string, excludes, includes multiFlag, uncompresses []string) error { |
| |
| matches := []pair{} |
| |
| sortMatches := func(matches []pair) { |
| if sortJava { |
| sort.SliceStable(matches, func(i, j int) bool { |
| return jar.EntryNamesLess(matches[i].newName, matches[j].newName) |
| }) |
| } else if sortOutput { |
| sort.SliceStable(matches, func(i, j int) bool { |
| return matches[i].newName < matches[j].newName |
| }) |
| } |
| } |
| |
| for _, arg := range args { |
| input, output := includeSplit(arg) |
| |
| var includeMatches []pair |
| |
| for _, file := range reader.File { |
| var newName string |
| if match, err := pathtools.Match(input, file.Name); err != nil { |
| return err |
| } else if match { |
| if output == "" { |
| newName = file.Name |
| } else { |
| if pathtools.IsGlob(input) { |
| // If the input is a glob then the output is a directory. |
| rel, err := filepath.Rel(constantPartOfPattern(input), file.Name) |
| if err != nil { |
| return err |
| } else if strings.HasPrefix("../", rel) { |
| return fmt.Errorf("globbed path %q was not in %q", file.Name, constantPartOfPattern(input)) |
| } |
| newName = filepath.Join(output, rel) |
| } else { |
| // Otherwise it is a file. |
| newName = output |
| } |
| } |
| includeMatches = append(includeMatches, pair{file, newName, false}) |
| } |
| } |
| |
| sortMatches(includeMatches) |
| matches = append(matches, includeMatches...) |
| } |
| |
| if len(args) == 0 { |
| // implicitly match everything |
| for _, file := range reader.File { |
| matches = append(matches, pair{file, file.Name, false}) |
| } |
| sortMatches(matches) |
| } |
| |
| var matchesAfterExcludes []pair |
| seen := make(map[string]*zip.File) |
| |
| for _, match := range matches { |
| // Filter out matches whose original file name matches an exclude filter, unless it also matches an |
| // include filter |
| if exclude, err := excludes.Match(match.File.Name); err != nil { |
| return err |
| } else if exclude { |
| if include, err := includes.Match(match.File.Name); err != nil { |
| return err |
| } else if !include { |
| continue |
| } |
| } |
| |
| // Check for duplicate output names, ignoring ones that come from the same input zip entry. |
| if prev, exists := seen[match.newName]; exists { |
| if prev != match.File { |
| return fmt.Errorf("multiple entries for %q with different contents", match.newName) |
| } |
| continue |
| } |
| seen[match.newName] = match.File |
| |
| for _, u := range uncompresses { |
| if uncompressMatch, err := pathtools.Match(u, match.newName); err != nil { |
| return err |
| } else if uncompressMatch { |
| match.uncompress = true |
| break |
| } |
| } |
| |
| matchesAfterExcludes = append(matchesAfterExcludes, match) |
| } |
| |
| for _, match := range matchesAfterExcludes { |
| if setTime { |
| match.File.SetModTime(staticTime) |
| } |
| if match.uncompress && match.File.FileHeader.Method != zip.Store { |
| fh := match.File.FileHeader |
| fh.Name = match.newName |
| fh.Method = zip.Store |
| fh.CompressedSize64 = fh.UncompressedSize64 |
| |
| zw, err := writer.CreateHeaderAndroid(&fh) |
| if err != nil { |
| return err |
| } |
| |
| zr, err := match.File.Open() |
| if err != nil { |
| return err |
| } |
| |
| _, err = io.Copy(zw, zr) |
| zr.Close() |
| if err != nil { |
| return err |
| } |
| } else { |
| err := writer.CopyFrom(match.File, match.newName) |
| if err != nil { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func includeSplit(s string) (string, string) { |
| split := strings.SplitN(s, ":", 2) |
| if len(split) == 2 { |
| return split[0], split[1] |
| } else { |
| return split[0], "" |
| } |
| } |
| |
| type multiFlag []string |
| |
| func (m *multiFlag) String() string { |
| return strings.Join(*m, " ") |
| } |
| |
| func (m *multiFlag) Set(s string) error { |
| *m = append(*m, s) |
| return nil |
| } |
| |
| func (m *multiFlag) Match(s string) (bool, error) { |
| if m == nil { |
| return false, nil |
| } |
| for _, f := range *m { |
| if match, err := pathtools.Match(f, s); err != nil { |
| return false, err |
| } else if match { |
| return true, nil |
| } |
| } |
| return false, nil |
| } |
| |
| func constantPartOfPattern(pattern string) string { |
| ret := "" |
| for pattern != "" { |
| var first string |
| first, pattern = splitFirst(pattern) |
| if pathtools.IsGlob(first) { |
| return ret |
| } |
| ret = filepath.Join(ret, first) |
| } |
| return ret |
| } |
| |
| func splitFirst(path string) (string, string) { |
| i := strings.IndexRune(path, filepath.Separator) |
| if i < 0 { |
| return path, "" |
| } |
| return path[:i], path[i+1:] |
| } |