blob: 2b11a9f2248bd2cbf586f5b0720a2c0696b79e57 [file] [log] [blame]
// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
import static com.android.tools.r8.utils.FileUtils.MODULE_INFO_CLASS;
import com.android.tools.r8.ByteDataView;
import com.android.tools.r8.DataDirectoryResource;
import com.android.tools.r8.DataEntryResource;
import com.android.tools.r8.ProgramResource;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.androidapi.AndroidApiDataAccess;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.references.ClassReference;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class ZipUtils {
// Beginning of extra field length: https://en.wikipedia.org/wiki/ZIP_(file_format)
private static final int EXTRA_FIELD_LENGTH_OFFSET = 30;
public static void writeResourcesToZip(
List<ProgramResource> resources,
Set<DataDirectoryResource> dataDirectoryResources,
Set<DataEntryResource> dataEntryResources,
Closer closer,
ZipOutputStream out)
throws IOException, ResourceException {
for (DataDirectoryResource dataDirectoryResource : dataDirectoryResources) {
writeToZipStream(out, dataDirectoryResource.getName(), new byte[0], ZipEntry.DEFLATED);
}
for (DataEntryResource dataEntryResource : dataEntryResources) {
String entryName = dataEntryResource.getName();
byte[] bytes = ByteStreams.toByteArray(closer.register(dataEntryResource.getByteStream()));
writeToZipStream(
out,
entryName,
bytes,
AndroidApiDataAccess.isApiDatabaseEntry(entryName) ? ZipEntry.STORED : ZipEntry.DEFLATED);
}
for (ProgramResource resource : resources) {
assert resource.getClassDescriptors().size() == 1;
Iterator<String> iterator = resource.getClassDescriptors().iterator();
String className = iterator.next();
String entryName = DescriptorUtils.getClassFileName(className);
byte[] bytes = ByteStreams.toByteArray(closer.register(resource.getByteStream()));
writeToZipStream(out, entryName, bytes, ZipEntry.DEFLATED);
}
}
public interface OnEntryHandler {
void onEntry(ZipEntry entry, InputStream input) throws IOException;
}
public static void iter(String zipFileStr, OnEntryHandler handler) throws IOException {
iter(Paths.get(zipFileStr), handler);
}
public static void iter(Path zipFilePath, OnEntryHandler handler) throws IOException {
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
try (InputStream entryStream = zipFile.getInputStream(entry)) {
handler.onEntry(entry, entryStream);
}
}
}
}
public static void iter(Path zipFilePath, Consumer<ZipEntry> entryConsumer) throws IOException {
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
entryConsumer.accept(entries.nextElement());
}
}
}
@SuppressWarnings("UnnecessaryParentheses")
public static boolean containsEntry(Path zipfile, String name) throws IOException {
BooleanBox result = new BooleanBox();
ZipUtils.iter(
zipfile,
(entry, stream) -> {
result.computeIfNotSet(() -> entry.getName().equals(name));
});
return result.get();
}
@SuppressWarnings("UnnecessaryParentheses")
public static Path map(
Path zipFilePath, Path mappedFilePath, BiFunction<ZipEntry, byte[], byte[]> map)
throws IOException {
ZipBuilder builder = ZipBuilder.builder(mappedFilePath);
ZipUtils.iter(
zipFilePath,
((entry, input) -> {
builder.addBytes(entry.getName(), map.apply(entry, ByteStreams.toByteArray(input)));
}));
return builder.build();
}
@SuppressWarnings("UnnecessaryParentheses")
public static Path filter(Path zipFilePath, Path filteredFilePath, Predicate<ZipEntry> predicate)
throws IOException {
ZipBuilder builder = ZipBuilder.builder(filteredFilePath);
ZipUtils.iter(
zipFilePath,
((entry, input) -> {
if (predicate.test(entry)) {
builder.addBytes(entry.getName(), ByteStreams.toByteArray(input));
}
}));
return builder.build();
}
public static byte[] readSingleEntry(Path zipFilePath, String name) throws IOException {
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
return ByteStreams.toByteArray(zipFile.getInputStream(zipFile.getEntry(name)));
}
}
@SuppressWarnings("StreamResourceLeak")
public static void zip(Path zipFile, Path inputDirectory) throws IOException {
List<Path> files =
Files.walk(inputDirectory)
.filter(path -> !Files.isDirectory(path))
.sorted()
.collect(Collectors.toList());
zip(zipFile, inputDirectory, files);
}
public static void zip(Path zipFile, Path basePath, Collection<Path> filesToZip)
throws IOException {
try (ZipOutputStream stream =
new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(zipFile)))) {
zip(stream, basePath, filesToZip);
}
}
public static void zip(ZipOutputStream stream, Path basePath, Collection<Path> filesToZip)
throws IOException {
for (Path path : filesToZip) {
ZipEntry zipEntry =
new ZipEntry(
StreamSupport.stream(
Spliterators.spliteratorUnknownSize(
basePath.relativize(path).iterator(), Spliterator.ORDERED),
false)
.map(Path::toString)
.collect(Collectors.joining("/")));
stream.putNextEntry(zipEntry);
Files.copy(path, stream);
stream.closeEntry();
}
}
public static void zip(Path zipFile, Path basePath, Path... filesToZip) throws IOException {
zip(zipFile, basePath, Arrays.asList(filesToZip));
}
public static List<Path> unzip(Path zipFile, Path outDirectory) throws IOException {
return unzip(zipFile, outDirectory, (entry) -> true, Function.identity());
}
public static List<File> unzip(String zipFile, File outDirectory) throws IOException {
return unzip(Paths.get(zipFile), outDirectory.toPath(), (entry) -> true, Path::toFile);
}
public static List<Path> unzip(Path zipFile, Path outDirectory, Predicate<ZipEntry> filter)
throws IOException {
return unzip(zipFile, outDirectory, filter, Function.identity());
}
public static <T> List<T> unzip(
Path zipFile, Path outDirectory, Predicate<ZipEntry> filter, Function<Path, T> map)
throws IOException {
final List<T> outFiles = new ArrayList<>();
iter(
zipFile,
(entry, input) -> {
String name = entry.getName();
if (!entry.isDirectory() && filter.test(entry)) {
if (name.contains("..")) {
// Protect against malicious archives.
throw new CompilationError("Invalid entry name \"" + name + "\"");
}
Path outPath = outDirectory.resolve(name);
outPath.toFile().getParentFile().mkdirs();
try (OutputStream output = new FileOutputStream(outPath.toFile())) {
ByteStreams.copy(input, output);
}
outFiles.add(map.apply(outPath));
}
});
return outFiles;
}
public static void writeToZipStream(
ZipOutputStream stream, String entry, byte[] content, int compressionMethod)
throws IOException {
writeToZipStream(stream, entry, ByteDataView.of(content), compressionMethod);
}
public static void writeToZipStream(
ZipOutputStream stream, String entry, ByteDataView content, int compressionMethod)
throws IOException {
byte[] buffer = content.getBuffer();
int offset = content.getOffset();
int length = content.getLength();
CRC32 crc = new CRC32();
crc.update(buffer, offset, length);
ZipEntry zipEntry = new ZipEntry(entry);
zipEntry.setMethod(compressionMethod);
zipEntry.setSize(length);
zipEntry.setCrc(crc.getValue());
zipEntry.setTime(0);
stream.putNextEntry(zipEntry);
stream.write(buffer, offset, length);
stream.closeEntry();
}
public static boolean isDexFile(String entry) {
String name = StringUtils.toLowerCase(entry);
return name.endsWith(DEX_EXTENSION);
}
public static boolean isClassFile(String entry) {
if (entry.endsWith(MODULE_INFO_CLASS)) {
return false;
}
// Only check for upper case META-INF. See JAR File Specification,
// https://docs.oracle.com/en/java/javase/17/docs/specs/jar/jar.html.
if (entry.startsWith("META-INF") || entry.startsWith("/META-INF")) {
return false;
}
return entry.endsWith(CLASS_EXTENSION);
}
public static class ZipBuilder {
private final Path zipFile;
private final ZipOutputStream stream;
private ZipBuilder(Path zipFile) throws IOException {
this.zipFile = zipFile;
stream = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(zipFile)));
}
public static ZipBuilder builder(Path zipFile) throws IOException {
return new ZipBuilder(zipFile);
}
public ZipOutputStream getOutputStream() {
return stream;
}
public ZipBuilder addFile(String name, Path file) throws IOException {
ZipEntry zipEntry = new ZipEntry(name);
stream.putNextEntry(zipEntry);
Files.copy(file, stream);
stream.closeEntry();
return this;
}
public ZipBuilder addFilesRelative(Path basePath, Collection<Path> filesToAdd)
throws IOException {
for (Path path : filesToAdd) {
ZipEntry zipEntry =
new ZipEntry(
StreamSupport.stream(
Spliterators.spliteratorUnknownSize(
basePath.relativize(path).iterator(), Spliterator.ORDERED),
false)
.map(Path::toString)
.collect(Collectors.joining("/")));
stream.putNextEntry(zipEntry);
Files.copy(path, stream);
stream.closeEntry();
}
return this;
}
public ZipBuilder addFilesRelative(Path basePath, Path... filesToAdd) throws IOException {
return addFilesRelative(basePath, Arrays.asList(filesToAdd));
}
public ZipBuilder addBytes(String path, byte[] bytes) throws IOException {
ZipEntry zipEntry = new ZipEntry(path);
stream.putNextEntry(zipEntry);
stream.write(bytes);
stream.closeEntry();
return this;
}
public ZipBuilder addText(String path, String text) throws IOException {
ZipEntry zipEntry = new ZipEntry(path);
stream.putNextEntry(zipEntry);
stream.write(text.getBytes(StandardCharsets.UTF_8));
stream.closeEntry();
return this;
}
public Path build() throws IOException {
stream.close();
return zipFile;
}
}
public static String zipEntryNameForClass(Class<?> clazz) {
return DescriptorUtils.getClassBinaryName(clazz) + CLASS_EXTENSION;
}
public static String zipEntryNameForClass(ClassReference clazz) {
return clazz.getBinaryName() + CLASS_EXTENSION;
}
public static long getOffsetOfResourceInZip(File file, String entry) throws IOException {
// Look into the jar file to see find the offset.
ZipFile zipFile = new ZipFile(file);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
long offset = 0;
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
byte[] extra = zipEntry.getExtra();
offset +=
EXTRA_FIELD_LENGTH_OFFSET
+ zipEntry.getName().length()
+ (extra == null ? 0 : extra.length);
if (zipEntry.getName().equals(entry)) {
return zipEntry.getSize() == zipEntry.getCompressedSize() ? offset : -1;
} else if (!zipEntry.isDirectory()) {
offset += zipEntry.getCompressedSize();
}
}
return -1;
}
}