blob: 0a0744746d3373cc7231645af29960bcd7898e37 [file] [log] [blame]
// Copyright 2017 The Bazel Authors. 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 com.google.devtools.build.android;
import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.IValueValidator;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimap;
import com.google.devtools.build.singlejar.ZipCombiner;
import com.google.devtools.build.singlejar.ZipCombiner.OutputMode;
import com.google.devtools.build.zip.ZipFileEntry;
import com.google.devtools.build.zip.ZipReader;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.regex.Pattern;
/**
* Action to filter entries out of a Zip file.
*
* <p>The entries to remove are determined from the filterZips and filterTypes. All entries from the
* filter Zip files that have an extension listed in filterTypes will be removed. If no filterZips
* are specified, no entries will be removed. Specifying no filterTypes is treated as if an
* extension of '.*' was specified.
*
* <p>Assuming each Zip as a set of entries, the result is:
*
* <pre> outputZip = inputZip - union[x intersect filterTypes for x in filterZips]</pre>
*
* <p>
*
* <pre>
* Example Usage:
* java/com/google/build/android/ZipFilterAction\
* --inputZip path/to/inputZip
* --outputZip path/to/outputZip
* --filterZips [path/to/filterZip[,path/to/filterZip]...]
* --filterTypes [fileExtension[,fileExtension]...]
* --explicitFilters [fileRegex[,fileRegex]...]
* --outputMode [DONT_CARE|FORCE_DEFLATE|FORCE_STORED]
* --checkHashMismatch [IGNORE|WARN|ERROR]
* </pre>
*/
public class ZipFilterAction {
private static final Logger logger = Logger.getLogger(ZipFilterAction.class.getName());
/** Modes of performing content hash checking during zip filtering. */
public enum HashMismatchCheckMode {
/** Filter file from input zip iff a file is found with the same filename in filter zips. */
IGNORE,
/**
* Filter file from input zip iff a file is found with the same filename and content hash in
* filter zips. Print warning if the filename is identical but content hash is not.
*/
WARN,
/**
* Same behavior as WARN, but throw an error if a file is found with the same filename with
* different content hash.
*/
ERROR
}
@Parameters(optionPrefixes = "--")
static class Options {
@Parameter(
names = "--inputZip",
description = "Path of input zip.",
converter = PathFlagConverter.class,
validateValueWith = PathExistsValidator.class
)
Path inputZip;
@Parameter(
names = "--outputZip",
description = "Path to write output zip.",
converter = PathFlagConverter.class
)
Path outputZip;
@Parameter(
names = "--filterZips",
description = "Filter zips.",
converter = PathFlagConverter.class,
validateValueWith = AllPathsExistValidator.class
)
List<Path> filterZips = ImmutableList.of();
@Parameter(names = "--filterTypes", description = "Filter file types.")
List<String> filterTypes = ImmutableList.of();
@Parameter(names = "--explicitFilters", description = "Explicitly specified filters.")
List<String> explicitFilters = ImmutableList.of();
@Parameter(names = "--outputMode", description = "Output zip compression mode.")
OutputMode outputMode = OutputMode.DONT_CARE;
@Parameter(
names = "--checkHashMismatch",
description =
"Ignore, warn or throw an error if the content hashes of two files with the "
+ "same name are different."
)
HashMismatchCheckMode hashMismatchCheckMode = HashMismatchCheckMode.WARN;
/**
* @deprecated please use --checkHashMismatch ERROR instead. Other options are IGNORE and WARN.
*/
@Deprecated
@Parameter(
names = "--errorOnHashMismatch",
description = "Error on entry filter with hash mismatch."
)
boolean errorOnHashMismatch = false;
/**
* @deprecated please use --checkHashMismatch WARN instead. Other options are IGNORE and WARN.
* <p>This is a hack to support existing users of --noerrorOnHashMismatch. JCommander does
* not support setting boolean flags with "--no", so instead we set the default to false and
* just ignore anyone who passes --noerrorOnHashMismatch.
*/
@Deprecated
@Parameter(names = "--noerrorOnHashMismatch")
boolean ignored = false;
}
/** Converts string flags to paths. Public because JCommander invokes this by reflection. */
public static class PathFlagConverter implements IStringConverter<Path> {
@Override
public Path convert(String text) {
return FileSystems.getDefault().getPath(text);
}
}
/** Validates that a path exists. Public because JCommander invokes this by reflection. */
public static class PathExistsValidator implements IValueValidator<Path> {
@Override
public void validate(String s, Path path) {
if (!Files.exists(path)) {
throw new ParameterException(String.format("%s is not a valid path.", path.toString()));
}
}
}
/** Validates that a set of paths exist. Public because JCommander invokes this by reflection. */
public static class AllPathsExistValidator implements IValueValidator<List<Path>> {
@Override
public void validate(String s, List<Path> paths) {
for (Path path : paths) {
if (!Files.exists(path)) {
throw new ParameterException(String.format("%s is not a valid path.", path.toString()));
}
}
}
}
@VisibleForTesting
static Multimap<String, Long> getEntriesToOmit(
Collection<Path> filterZips, Collection<String> filterTypes) throws IOException {
// Escape filter types to prevent regex abuse
Set<String> escapedFilterTypes = new HashSet<>();
for (String filterType : filterTypes) {
escapedFilterTypes.add(Pattern.quote(filterType));
}
// Match any string that ends with any of the filter file types
String filterRegex = String.format(".*(%s)$", Joiner.on("|").join(escapedFilterTypes));
ImmutableSetMultimap.Builder<String, Long> entriesToOmit = ImmutableSetMultimap.builder();
for (Path filterZip : filterZips) {
try (ZipReader zip = new ZipReader(filterZip.toFile())) {
for (ZipFileEntry entry : zip.entries()) {
if (filterTypes.isEmpty() || entry.getName().matches(filterRegex)) {
entriesToOmit.put(entry.getName(), entry.getCrc());
}
}
}
}
return entriesToOmit.build();
}
public static void main(String[] args) throws IOException {
System.exit(run(args));
}
static int run(String[] args) throws IOException {
Options options = new Options();
new JCommander(options).parse(args);
logger.fine(
String.format(
"Creating filter from entries of type %s, in zip files %s.",
options.filterTypes, options.filterZips));
final Stopwatch timer = Stopwatch.createStarted();
Multimap<String, Long> entriesToOmit =
getEntriesToOmit(options.filterZips, options.filterTypes);
final String explicitFilter =
options.explicitFilters.isEmpty()
? ""
: String.format(".*(%s).*", Joiner.on("|").join(options.explicitFilters));
logger.fine(String.format("Filter created in %dms", timer.elapsed(TimeUnit.MILLISECONDS)));
ImmutableMap.Builder<String, Long> inputEntries = ImmutableMap.builder();
try (ZipReader input = new ZipReader(options.inputZip.toFile())) {
for (ZipFileEntry entry : input.entries()) {
inputEntries.put(entry.getName(), entry.getCrc());
}
}
// TODO(jingwen): Remove --errorOnHashMismatch when Blaze release with --checkHashMismatch
// is checked in.
if (options.errorOnHashMismatch) {
options.hashMismatchCheckMode = HashMismatchCheckMode.ERROR;
}
ZipFilterEntryFilter entryFilter =
new ZipFilterEntryFilter(
explicitFilter, entriesToOmit, inputEntries.build(), options.hashMismatchCheckMode);
try (OutputStream out = Files.newOutputStream(options.outputZip);
ZipCombiner combiner = new ZipCombiner(options.outputMode, entryFilter, out)) {
combiner.addZip(options.inputZip.toFile());
}
logger.fine(String.format("Filtering completed in %dms", timer.elapsed(TimeUnit.MILLISECONDS)));
return entryFilter.sawErrors() ? 1 : 0;
}
}