blob: 411a935adfea942d45fc122b0d51320504dd8d8d [file] [log] [blame]
/*
* Copyright (C) 2018 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.tools.deployer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.io.BaseEncoding;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ZipUtils {
private static final int CENTRAL_DIRECTORY_FILE_HEADER_MAGIC = 0x02014b50;
private static final int CENTRAL_DIRECTORY_FILE_HEADER_SIZE = 46;
private static final int LOCAL_DIRECTORY_FILE_HEADER_SIZE = 30;
private static final String DIGEST_ALGORITHM = "SHA-1";
public static class ZipEntry implements Serializable {
public final long crc;
public final String name;
public final long start; // Offset in the archive to the Local File Header location
// Offset in the archive to the last byte of the payload we know of.
// Since we don't have the lfh extra size, we can only generate an index value that is
// "equal or before" the end of a zip entry which is good enough to mark areas as clean
// for delta-push to work.
public final long approx_end;
// Array with all attributes of an entry in the Local File Header. Used for deltaPushing.
public final byte[] localFileHeader;
ZipEntry(long crc, String name, long start, long approx_end, byte[] localFileHeader) {
this.crc = crc;
this.name = name;
this.start = start;
this.approx_end = approx_end;
this.localFileHeader = localFileHeader;
}
}
@VisibleForTesting
public static Map<String, ZipEntry> readZipEntries(byte[] buf) {
ByteBuffer buffer = ByteBuffer.wrap(buf);
return readZipEntries(buffer)
.stream()
.collect(Collectors.toMap(e -> e.name, Functions.identity()));
}
public static List<ZipEntry> readZipEntries(ByteBuffer buf) {
buf.order(ByteOrder.LITTLE_ENDIAN);
List<ZipEntry> entries = new ArrayList<>();
while (buf.remaining() >= CENTRAL_DIRECTORY_FILE_HEADER_SIZE
&& buf.getInt() == CENTRAL_DIRECTORY_FILE_HEADER_MAGIC) {
// Read all the data
short version = buf.getShort();
short versionNeeded = buf.getShort();
short flags = buf.getShort();
short compression = buf.getShort();
short modTime = buf.getShort();
short modDate = buf.getShort();
long crc = uintToLong(buf.getInt());
long compressedSize = uintToLong(buf.getInt());
long decompressedSize = uintToLong(buf.getInt());
int pathLength = ushortToInt(buf.getShort());
int extraLength = ushortToInt(buf.getShort());
int commentLength = ushortToInt(buf.getShort());
// Skip 2 (disk number) + 2 (internal attributes)+ 4 (external attributes)
buf.position(buf.position() + 8);
long start = uintToLong(buf.getInt()); // offset to local file entry header
// Read the filename
byte[] pathBytes = new byte[pathLength];
buf.get(pathBytes);
String name = new String(pathBytes, Charset.forName("UTF-8"));
buf.position(buf.position() + extraLength + commentLength);
// Create a fake zip file entry header which will be used to build a dirtyMap while
// delta pushing.
byte[] localFileHeader = new byte[LOCAL_DIRECTORY_FILE_HEADER_SIZE + pathBytes.length];
ByteBuffer fakeEntry = ByteBuffer.wrap(localFileHeader).order(ByteOrder.LITTLE_ENDIAN);
fakeEntry.putLong(start);
fakeEntry.putShort(versionNeeded);
fakeEntry.putShort(modTime);
fakeEntry.putShort(modDate);
fakeEntry.putInt((int) crc);
fakeEntry.putInt(longToUint(compressedSize));
fakeEntry.putInt(longToUint(decompressedSize));
fakeEntry.putShort(intToUShort(pathLength));
fakeEntry.putShort(intToUShort(extraLength));
fakeEntry.put(pathBytes);
// Keep track of boundaries of the entry in the zip archive since those are used while
// deltaPushing. Since we don't have the lfh extra size, we can only approximate the
// end boundary. -1 is because approx_end must point to the last byte and not after
// the last byte.
long approx_end = start + LOCAL_DIRECTORY_FILE_HEADER_SIZE + pathLength - 1;
approx_end += compression == 0 ? decompressedSize : compressedSize;
ZipEntry entry = new ZipEntry(crc, name, start, approx_end, localFileHeader);
entries.add(entry);
}
return entries;
}
public static String digest(ByteBuffer buffer) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(
"MessageDigest:" + DIGEST_ALGORITHM + " unavailable.", e);
}
// TODO: Parse the block and hash the top level signature instead of hashing the entire block.
messageDigest.update(buffer);
byte[] digestBytes = messageDigest.digest();
return BaseEncoding.base16().lowerCase().encode(digestBytes);
}
public static short intToUShort(int integer) {
if ((integer & 0xFF_FF_00_00) != 0) {
throw new IllegalStateException("Cannot cast int to uint16 (does not fit)");
}
return (short) integer;
}
public static long uintToLong(int integer) {
return integer & 0xFF_FF_FF_FFL;
}
public static int longToUint(long integer) {
if ((integer & 0xFF_FF_FF_FF_00_00_00_00L) != 0) {
throw new IllegalStateException("Cannot cast long to uint32 (does not fit)");
}
return (int) integer;
}
public static int ushortToInt(short integer) {
return integer & 0xFF_FF;
}
}