package com.leon.channel.common;
import com.leon.channel.common.verify.ApkSignatureSchemeV2Verifier;
import com.leon.channel.common.verify.ZipUtils;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Created by leontli on 17/2/19.
*/
public class V1SchemeUtil {
/**
* write channel to apk
*
* @param file
* @param channel
* @throws Exception
*/
public static void writeChannel(File file, String channel) throws Exception {
if (file == null || !file.exists() || !file.isFile() || channel == null || channel.isEmpty()) {
throw new Exception("param error , file : " + file + " , channel : " + channel);
}
byte[] comment = channel.getBytes(ChannelConstants.CONTENT_CHARSET);
Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(file);
if (eocdAndOffsetInFile.getFirst().remaining() == ZipUtils.ZIP_EOCD_REC_MIN_SIZE) {
System.out.println("file : " + file.getAbsolutePath() + " , has no comment");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
//1.locate comment length field
raf.seek(file.length() - ChannelConstants.SHORT_LENGTH);
//2.write zip comment length (content field length + length field length + magic field length)
writeShort(comment.length + ChannelConstants.SHORT_LENGTH + ChannelConstants.V1_MAGIC.length, raf);
//3.write content
raf.write(comment);
//4.write content length
writeShort(comment.length, raf);
//5. write magic bytes
raf.write(ChannelConstants.V1_MAGIC);
raf.close();
} else {
System.out.println("file : " + file.getAbsolutePath() + " , has comment");
if (containV1Magic(file)) {
String existChannel = readChannel(file);
throw new ChannelExistException("file : " + file.getAbsolutePath() + " has a channel : " + existChannel + ", only ignore");
}
int existCommentLength = ZipUtils.getUnsignedInt16(eocdAndOffsetInFile.getFirst(), ZipUtils.ZIP_EOCD_REC_MIN_SIZE - ChannelConstants.SHORT_LENGTH);
int newCommentLength = existCommentLength + comment.length + ChannelConstants.SHORT_LENGTH + ChannelConstants.V1_MAGIC.length;
RandomAccessFile raf = new RandomAccessFile(file, "rw");
//1.locate comment length field
raf.seek(eocdAndOffsetInFile.getSecond() + ZipUtils.ZIP_EOCD_REC_MIN_SIZE - ChannelConstants.SHORT_LENGTH);
//2.write zip comment length (existCommentLength + content field length + length field length + magic field length)
writeShort(newCommentLength, raf);
//3.locate where channel should begin
raf.seek(eocdAndOffsetInFile.getSecond() + ZipUtils.ZIP_EOCD_REC_MIN_SIZE + existCommentLength);
//4.write content
raf.write(comment);
//5.write content length
writeShort(comment.length, raf);
//6.write magic bytes
raf.write(ChannelConstants.V1_MAGIC);
raf.close();
}
}
/**
* read channel from apk
*
* @param file
* @return
* @throws Exception
*/
public static String readChannel(File file) throws Exception {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "r");
long index = raf.length();
byte[] buffer = new byte[ChannelConstants.V1_MAGIC.length];
index -= ChannelConstants.V1_MAGIC.length;
raf.seek(index);
raf.readFully(buffer);
// whether magic bytes matched
if (isV1MagicMatch(buffer)) {
index -= ChannelConstants.SHORT_LENGTH;
raf.seek(index);
// read channel length field
int length = readShort(raf);
if (length > 0) {
index -= length;
raf.seek(index);
// read channel bytes
byte[] bytesComment = new byte[length];
raf.readFully(bytesComment);
return new String(bytesComment, ChannelConstants.CONTENT_CHARSET);
} else {
throw new Exception("zip channel info not found");
}
} else {
throw new Exception("zip v1 magic not found");
}
} finally {
if (raf != null) {
raf.close();
}
}
}
/**
* verify channel info
*
* @param file
* @param channel
* @return
* @throws Exception
*/
public static boolean verifyChannel(File file, String channel) throws Exception {
if (channel != null) {
return channel.equals(readChannel(file));
}
return false;
}
private static void writeShort(int i, DataOutput out) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(ChannelConstants.SHORT_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
bb.putShort((short) i);
out.write(bb.array());
}
private static short readShort(DataInput input) throws IOException {
byte[] buf = new byte[ChannelConstants.SHORT_LENGTH];
input.readFully(buf);
ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
return bb.getShort(0);
}
/**
* judge whether contain v1 magic int the end of file
*
* @param file
* @return
* @throws IOException
*/
public static boolean containV1Magic(File file) throws IOException {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "r");
long index = raf.length();
byte[] buffer = new byte[ChannelConstants.V1_MAGIC.length];
index -= ChannelConstants.V1_MAGIC.length;
raf.seek(index);
raf.readFully(buffer);
return isV1MagicMatch(buffer);
} finally {
if (raf != null) {
raf.close();
}
}
}
/**
* check v1 magic
*
* @param buffer
* @return
*/
private static boolean isV1MagicMatch(byte[] buffer) {
if (buffer.length != ChannelConstants.V1_MAGIC.length) {
return false;
}
for (int i = 0; i < ChannelConstants.V1_MAGIC.length; ++i) {
if (buffer[i] != ChannelConstants.V1_MAGIC[i]) {
return false;
}
}
return true;
}
/**
* get eocd and offset from apk
*
* @param apk
* @return
* @throws IOException
* @throws ApkSignatureSchemeV2Verifier.SignatureNotFoundException
*/
public static Pair<ByteBuffer, Long> getEocd(File apk) throws IOException, ApkSignatureSchemeV2Verifier.SignatureNotFoundException {
if (apk == null || !apk.exists() || !apk.isFile()) {
return null;
}
RandomAccessFile raf = new RandomAccessFile(apk, "r");
//find the EOCD
Pair<ByteBuffer, Long> eocdAndOffsetInFile = ApkSignatureSchemeV2Verifier.getEocd(raf);
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(raf, eocdAndOffsetInFile.getSecond())) {
throw new ApkSignatureSchemeV2Verifier.SignatureNotFoundException("ZIP64 APK not supported");
}
return eocdAndOffsetInFile;
}
/**
* copy file
*
* @param src
* @param dest
* @throws IOException
*/
public static void copyFile(File src, File dest) throws IOException {
if (!dest.exists()) {
dest.createNewFile();
}
FileChannel source = null;
FileChannel destination = null;
try {
source = new FileInputStream(src).getChannel();
destination = new FileOutputStream(dest).getChannel();
destination.transferFrom(source, 0, source.size());
} finally {
if (source != null) {
source.close();
}
if (destination != null) {
destination.close();
}
}
}
/**
* judge whether apk contain v1 signature
*
* @param file
* @return
*/
public static boolean containV1Signature(File file) {
JarFile jarFile;
try {
jarFile = new JarFile(file);
JarEntry manifestEntry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
JarEntry sfEntry = null;
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().matches("META-INF/\\w+\\.SF")) {
sfEntry = jarFile.getJarEntry(entry.getName());
break;
}
}
if (manifestEntry != null && sfEntry != null) {
return true;
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
public static class ChannelExistException extends Exception {
static final long serialVersionUID = -3387516993124229949L;
public ChannelExistException() {
super();
}
public ChannelExistException(final String message) {
super(message);
}
}
}