Home | History | Annotate | Download | only in zip2zip
      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 
     15 package main
     16 
     17 import (
     18 	"flag"
     19 	"fmt"
     20 	"log"
     21 	"os"
     22 	"path/filepath"
     23 	"sort"
     24 	"strings"
     25 	"time"
     26 
     27 	"github.com/google/blueprint/pathtools"
     28 
     29 	"android/soong/jar"
     30 	"android/soong/third_party/zip"
     31 )
     32 
     33 var (
     34 	input     = flag.String("i", "", "zip file to read from")
     35 	output    = flag.String("o", "", "output file")
     36 	sortGlobs = flag.Bool("s", false, "sort matches from each glob (defaults to the order from the input zip file)")
     37 	sortJava  = flag.Bool("j", false, "sort using jar ordering within each glob (META-INF/MANIFEST.MF first)")
     38 	setTime   = flag.Bool("t", false, "set timestamps to 2009-01-01 00:00:00")
     39 
     40 	staticTime = time.Date(2009, 1, 1, 0, 0, 0, 0, time.UTC)
     41 
     42 	excludes excludeArgs
     43 )
     44 
     45 func init() {
     46 	flag.Var(&excludes, "x", "exclude a filespec from the output")
     47 }
     48 
     49 func main() {
     50 	flag.Usage = func() {
     51 		fmt.Fprintln(os.Stderr, "usage: zip2zip -i zipfile -o zipfile [-s|-j] [-t] [filespec]...")
     52 		flag.PrintDefaults()
     53 		fmt.Fprintln(os.Stderr, "  filespec:")
     54 		fmt.Fprintln(os.Stderr, "    <name>")
     55 		fmt.Fprintln(os.Stderr, "    <in_name>:<out_name>")
     56 		fmt.Fprintln(os.Stderr, "    <glob>[:<out_dir>]")
     57 		fmt.Fprintln(os.Stderr, "")
     58 		fmt.Fprintln(os.Stderr, "<glob> uses the rules at https://godoc.org/github.com/google/blueprint/pathtools/#Match")
     59 		fmt.Fprintln(os.Stderr, "")
     60 		fmt.Fprintln(os.Stderr, "Files will be copied with their existing compression from the input zipfile to")
     61 		fmt.Fprintln(os.Stderr, "the output zipfile, in the order of filespec arguments.")
     62 		fmt.Fprintln(os.Stderr, "")
     63 		fmt.Fprintln(os.Stderr, "If no filepsec is provided all files and directories are copied.")
     64 	}
     65 
     66 	flag.Parse()
     67 
     68 	if *input == "" || *output == "" {
     69 		flag.Usage()
     70 		os.Exit(1)
     71 	}
     72 
     73 	log.SetFlags(log.Lshortfile)
     74 
     75 	reader, err := zip.OpenReader(*input)
     76 	if err != nil {
     77 		log.Fatal(err)
     78 	}
     79 	defer reader.Close()
     80 
     81 	output, err := os.Create(*output)
     82 	if err != nil {
     83 		log.Fatal(err)
     84 	}
     85 	defer output.Close()
     86 
     87 	writer := zip.NewWriter(output)
     88 	defer func() {
     89 		err := writer.Close()
     90 		if err != nil {
     91 			log.Fatal(err)
     92 		}
     93 	}()
     94 
     95 	if err := zip2zip(&reader.Reader, writer, *sortGlobs, *sortJava, *setTime,
     96 		flag.Args(), excludes); err != nil {
     97 
     98 		log.Fatal(err)
     99 	}
    100 }
    101 
    102 type pair struct {
    103 	*zip.File
    104 	newName string
    105 }
    106 
    107 func zip2zip(reader *zip.Reader, writer *zip.Writer, sortOutput, sortJava, setTime bool,
    108 	includes []string, excludes []string) error {
    109 
    110 	matches := []pair{}
    111 
    112 	sortMatches := func(matches []pair) {
    113 		if sortJava {
    114 			sort.SliceStable(matches, func(i, j int) bool {
    115 				return jar.EntryNamesLess(matches[i].newName, matches[j].newName)
    116 			})
    117 		} else if sortOutput {
    118 			sort.SliceStable(matches, func(i, j int) bool {
    119 				return matches[i].newName < matches[j].newName
    120 			})
    121 		}
    122 	}
    123 
    124 	for _, include := range includes {
    125 		// Reserve escaping for future implementation, so make sure no
    126 		// one is using \ and expecting a certain behavior.
    127 		if strings.Contains(include, "\\") {
    128 			return fmt.Errorf("\\ characters are not currently supported")
    129 		}
    130 
    131 		input, output := includeSplit(include)
    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 						_, name := filepath.Split(file.Name)
    146 						newName = filepath.Join(output, name)
    147 					} else {
    148 						// Otherwise it is a file.
    149 						newName = output
    150 					}
    151 				}
    152 				includeMatches = append(includeMatches, pair{file, newName})
    153 			}
    154 		}
    155 
    156 		sortMatches(includeMatches)
    157 		matches = append(matches, includeMatches...)
    158 	}
    159 
    160 	if len(includes) == 0 {
    161 		// implicitly match everything
    162 		for _, file := range reader.File {
    163 			matches = append(matches, pair{file, file.Name})
    164 		}
    165 		sortMatches(matches)
    166 	}
    167 
    168 	var matchesAfterExcludes []pair
    169 	seen := make(map[string]*zip.File)
    170 
    171 	for _, match := range matches {
    172 		// Filter out matches whose original file name matches an exclude filter
    173 		excluded := false
    174 		for _, exclude := range excludes {
    175 			if excludeMatch, err := pathtools.Match(exclude, match.File.Name); err != nil {
    176 				return err
    177 			} else if excludeMatch {
    178 				excluded = true
    179 				break
    180 			}
    181 		}
    182 
    183 		if excluded {
    184 			continue
    185 		}
    186 
    187 		// Check for duplicate output names, ignoring ones that come from the same input zip entry.
    188 		if prev, exists := seen[match.newName]; exists {
    189 			if prev != match.File {
    190 				return fmt.Errorf("multiple entries for %q with different contents", match.newName)
    191 			}
    192 			continue
    193 		}
    194 		seen[match.newName] = match.File
    195 
    196 		matchesAfterExcludes = append(matchesAfterExcludes, match)
    197 	}
    198 
    199 	for _, match := range matchesAfterExcludes {
    200 		if setTime {
    201 			match.File.SetModTime(staticTime)
    202 		}
    203 		if err := writer.CopyFrom(match.File, match.newName); err != nil {
    204 			return err
    205 		}
    206 	}
    207 
    208 	return nil
    209 }
    210 
    211 func includeSplit(s string) (string, string) {
    212 	split := strings.SplitN(s, ":", 2)
    213 	if len(split) == 2 {
    214 		return split[0], split[1]
    215 	} else {
    216 		return split[0], ""
    217 	}
    218 }
    219 
    220 type excludeArgs []string
    221 
    222 func (e *excludeArgs) String() string {
    223 	return strings.Join(*e, " ")
    224 }
    225 
    226 func (e *excludeArgs) Set(s string) error {
    227 	*e = append(*e, s)
    228 	return nil
    229 }
    230