blob: d2964cf80e85392dbc945282c05ba43cd33972c6 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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.android.zipflinger;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Locale;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
public class LargeFileSource extends Source {
private final Path transferSrc;
private final int compressionLevel;
private static final String TMP_DIR = System.getProperty("java.io.tmpdir");
// Does not load the whole file in memory. If the entry is not compressed, only read it to
// compute the CRC32 and zero-copy src when needed. If compression is requested, uses a tmp
// storage to store the deflated payload then zero-copy it when needed.
public LargeFileSource(
@NonNull Path src,
@Nullable Path tmpStorage,
@NonNull String name,
int compressionLevel)
throws IOException {
super(name);
this.compressionLevel = compressionLevel;
if (tmpStorage == null && compressionLevel != Deflater.NO_COMPRESSION) {
String msg = "Compression without a provided tmp Path is not supported";
throw new IllegalStateException(msg);
}
try (CheckedInputStream in =
new CheckedInputStream(Files.newInputStream(src), new CRC32())) {
if (compressionLevel == Deflater.NO_COMPRESSION) {
buildStored(in);
transferSrc = src;
} else {
buildCompressed(in, compressionLevel, tmpStorage);
transferSrc = tmpStorage;
}
// At this point the input file has been completely read. We can request the crc32.
crc = Ints.longToUint(in.getChecksum().getValue());
}
}
public LargeFileSource(@NonNull Path src, @NonNull String name, int compressionLevel)
throws IOException {
this(src, getTmpStoragePath(src.getFileName().toString()), name, compressionLevel);
}
@NonNull
public static Path getTmpStoragePath(@NonNull String entryName) {
StringBuilder filename = new StringBuilder();
filename.append(Integer.toHexString(entryName.hashCode()));
filename.append("-");
filename.append(Thread.currentThread().getId());
filename.append("-");
filename.append(System.nanoTime());
filename.append(".tmp");
Path tmp = Paths.get(TMP_DIR, filename.toString());
if (Files.exists(tmp)) {
String msg = String.format(Locale.US, "Cannot use path '%s' (exists)", tmp);
throw new IllegalStateException(msg);
}
return tmp;
}
private void buildStored(@NonNull InputStream in) throws IOException {
byte[] buffer = new byte[4096];
long inputSize = 0;
int read;
while ((read = in.read(buffer)) != -1) {
inputSize += read;
}
compressedSize = inputSize;
uncompressedSize = compressedSize;
compressionFlag = LocalFileHeader.COMPRESSION_NONE;
}
private void buildCompressed(@NonNull InputStream in, int compressionLevel, @NonNull Path tmp)
throws IOException {
// Make sure we are not going to overwrite another tmp file.
if (Files.exists(tmp)) {
String msg = String.format("Tmp storage '%s' already exists", tmp.toAbsolutePath());
throw new IllegalStateException(msg);
}
// Pipe the src into the tmp compressed file.
Deflater deflater = Compressor.getDeflater(compressionLevel);
try (DeflaterOutputStream out =
new DeflaterOutputStream(
Files.newOutputStream(tmp, StandardOpenOption.CREATE_NEW), deflater)) {
// Just in case we crash before writeTo is called, attempt to clean up on VM exit.
tmp.toFile().deleteOnExit();
int read;
byte[] buffer = new byte[4096];
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
compressedSize = deflater.getBytesWritten();
uncompressedSize = deflater.getBytesRead();
compressionFlag = LocalFileHeader.COMPRESSION_DEFLATE;
}
@Override
public void prepare() throws IOException {}
@Override
public long writeTo(@NonNull ZipWriter writer) throws IOException {
try (FileChannel src = FileChannel.open(transferSrc, StandardOpenOption.READ)) {
writer.transferFrom(src, 0, this.compressedSize);
return this.compressedSize;
} finally {
if (compressionLevel != Deflater.NO_COMPRESSION) {
Files.delete(transferSrc);
}
}
}
}