blob: 9a521fb497f1733aa582300e74f92db510bfbd7d [file] [log] [blame]
// Copyright (c) 2018, 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 com.android.tools.r8.ByteDataView;
import com.android.tools.r8.DataEntryResource;
import com.android.tools.r8.DataResource;
import com.android.tools.r8.DiagnosticsHandler;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import com.google.common.io.ByteStreams;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;
public class ArchiveBuilder implements OutputBuilder {
private final Path archive;
private final Origin origin;
private ZipOutputStream stream = null;
private boolean closed = false;
private int openCount = 0;
private int classesFileIndex = 0;
private Map<Integer, DelayedData> delayedClassesDexFiles = new HashMap<>();
private SortedSet<DelayedData> delayedWrites = new TreeSet<>();
public ArchiveBuilder(Path archive) {
this.archive = archive;
origin = new PathOrigin(archive);
}
@Override
public synchronized void open() {
assert !closed;
openCount++;
}
@Override
public synchronized void close(DiagnosticsHandler handler) {
assert !closed;
openCount--;
if (openCount == 0) {
writeDelayed(handler);
closed = true;
try {
getStreamRaw().close();
stream = null;
} catch (IOException e) {
handler.error(new ExceptionDiagnostic(e, origin));
}
}
}
private void writeDelayed(DiagnosticsHandler handler) {
// We should never have any indexed files at this point
assert delayedClassesDexFiles.isEmpty();
for (DelayedData data : delayedWrites) {
if (data.isDirectory) {
assert data.content == null;
writeDirectoryNow(data.name, handler);
} else {
assert data.content != null;
writeFileNow(data.name, data.content, handler);
}
}
}
private ZipOutputStream getStreamRaw() throws IOException {
if (stream != null) {
return stream;
}
stream =
new ZipOutputStream(
new BufferedOutputStream(
Files.newOutputStream(
archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)));
return stream;
}
/** Get or open the zip output stream. */
private synchronized ZipOutputStream getStream() throws IOException {
assert !closed;
return getStreamRaw();
}
private void handleIOException(IOException e, DiagnosticsHandler handler) {
ExceptionDiagnostic diagnostic = new ExceptionDiagnostic(e, origin);
if (e instanceof ZipException && e.getMessage().startsWith("duplicate entry")) {
// For now we stick to the Proguard behaviour, see section "Warning: can't write resource ...
// Duplicate zip entry" on https://www.guardsquare.com/en/proguard/manual/troubleshooting.
handler.warning(diagnostic);
} else {
handler.error(diagnostic);
}
}
@Override
public synchronized void addDirectory(String name, DiagnosticsHandler handler) {
delayedWrites.add(DelayedData.createDirectory(name));
}
private void writeDirectoryNow(String name, DiagnosticsHandler handler) {
if (name.charAt(name.length() - 1) != DataResource.SEPARATOR) {
name += DataResource.SEPARATOR;
}
ZipEntry entry = new ZipEntry(name);
entry.setTime(0);
synchronized (this) {
try {
ZipOutputStream zip = getStream();
zip.putNextEntry(entry);
zip.closeEntry();
} catch (IOException e) {
handleIOException(e, handler);
}
}
}
@Override
public void addFile(String name, DataEntryResource content, DiagnosticsHandler handler) {
try (InputStream in = content.getByteStream()) {
ByteDataView view = ByteDataView.of(ByteStreams.toByteArray(in));
synchronized (this) {
delayedWrites.add(DelayedData.createFile(name, view));
}
} catch (IOException e) {
handleIOException(e, handler);
} catch (ResourceException e) {
handler.error(new StringDiagnostic("Failed to open input: " + e.getMessage(),
content.getOrigin()));
}
}
@Override
public synchronized void addFile(String name, ByteDataView content, DiagnosticsHandler handler) {
delayedWrites.add(DelayedData.createFile(name, ByteDataView.of(content.copyByteData())));
}
private void writeFileNow(String name, ByteDataView content, DiagnosticsHandler handler) {
try {
ZipUtils.writeToZipStream(getStream(), name, content, ZipEntry.DEFLATED);
} catch (IOException e) {
handleIOException(e, handler);
}
}
private void writeNextIfAvailable(DiagnosticsHandler handler) {
DelayedData data = delayedClassesDexFiles.remove(classesFileIndex);
while (data != null) {
writeFileNow(data.name, data.content, handler);
classesFileIndex++;
data = delayedClassesDexFiles.remove(classesFileIndex);
}
}
@Override
public synchronized void addIndexedClassFile(
int index, String name, ByteDataView content, DiagnosticsHandler handler) {
if (index == classesFileIndex) {
// Fast case, we got the file in order (or we only had one).
writeFileNow(name, content, handler);
classesFileIndex++;
writeNextIfAvailable(handler);
} else {
// Data is released in the application writer, take a copy.
delayedClassesDexFiles.put(index,
new DelayedData(name, ByteDataView.of(content.copyByteData()), false));
}
}
@Override
public Origin getOrigin() {
return origin;
}
@Override
public Path getPath() {
return archive;
}
private static class DelayedData implements Comparable<DelayedData> {
public final String name;
public final ByteDataView content;
public final boolean isDirectory;
public static DelayedData createFile(String name, ByteDataView content) {
return new DelayedData(name, content, false);
}
public static DelayedData createDirectory(String name) {
return new DelayedData(name, null, true);
}
private DelayedData(String name, ByteDataView content, boolean isDirectory) {
this.name = name;
this.content = content;
this.isDirectory = isDirectory;
}
@Override
public int compareTo(DelayedData other) {
if (other == null) {
return name.compareTo(null);
}
return name.compareTo(other.name);
}
}
}