朋友谈天讨论到一个问题,怎样检测zip的完整性。zip是咱们常用的紧缩格局,不管是Win/Mac/Linux下都很常用,咱们做文件的下载也会经常用到,网络充满不确定性,关于多个小文件(比方配置文件)的下载,咱们希望只发起一次衔接,因为建立衔接是很耗费资源的,即便现在http2.0能够对一条TCP衔接进行复用,咱们仍是希望网络恳求的次数越少越好,不管是关于稳定性仍是成功失利的逻辑判别,都会有好处。
这个时分咱们常用的其实便是把他们紧缩成一个zip文件,下载下来之后解压就好了。
但很多时分zip会解压失利,如果咱们的zip现已下载下来了,其实不存在没有访问权限的问题了,那原因除了空间不行之外,便是zip格局有问题了,zip文件为空或许只下载了一半。
这个时分就需求检测一下咱们下载下来的zip是不是合法有效的zip了。
有这么几种思路:
- 直接解压,抛反常标明zip有问题
- 下载前得到zip文件的length,下载后检测文件巨细
- 运用md5或sha1等摘要算法,下载下来后做md5,然后比对合法性
- 检测zip文件完毕的特殊编码格局,检测是否zip合法
这几种做法有利有弊,这儿咱们只看第4种。
咱们讨论之前,能够大致了解一下zip的格局ZIP文件格局剖析,咱们重视的是End of central directory record
,中心目录完毕符号,每个zip只会呈现一次。
Offset | Bytes | Description | 译 |
---|---|---|---|
0 | 4 | End of central directory signature = 0x06054b50 | 中心目录完毕符号(0x06054b50) |
4 | 2 | Number of this disk | 当时磁盘编号 |
6 | 2 | number of the disk with the start of the central directory | 中心目录开端方位的磁盘编号 |
8 | 2 | total number of entries in the central directory on this disk | 该磁盘上所记录的中心目录数量 |
10 | 2 | total number of entries in the central directory | 中心目录结构总数 |
12 | 4 | Size of central directory (bytes) | 中心目录的巨细 |
16 | 4 | offset of start of central directory with respect to the starting disk number | 中心目录开端方位相关于archive开端的位移 |
20 | 2 | .ZIP file comment length(n) | 注释长度 |
22 | n | .ZIP Comment | 注释内容 |
咱们能够看到,0x06054b50
地点的方位其实是在zip.length减去22个字节,所以咱们只需求seek到需求的方位,然后读4个字节看是否是0x06054b50
,就能够确定zip是否完整。
下面是一个判别的代码
//没有zip文件注释时分的目录完毕符的偏移量
private static final int RawEndOffset = 22;
//0x06054b50占4个字节
private static final int endOfDirLength = 4;
//目录完毕标识0x06054b50 的小端读取方法。
private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};
private boolean isZipFile(File file) throws IOException {
if (file.exists() && file.isFile()) {
if (file.length() <= RawEndOffset + endOfDirLength) {
return false;
}
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//seek到完毕符号地点的方位
randomAccessFile.seek(fileLength - RawEndOffset);
byte[] end = new byte[endOfDirLength];
//读取4个字节
randomAccessFile.read(end);
//关掉文件
randomAccessFile.close();
return isEndOfDir(end);
} else {
return false;
}
}
/**
* 是否契合文件夹完毕符号
*/
private boolean isEndOfDir(byte[] src) {
if (src.length != endOfDirLength) {
return false;
}
for (int i = 0; i < src.length; i++) {
if (src[i] != endOfDir[i]) {
return false;
}
}
return true;
}
有人可能留意到了,你上面写的完毕标识分明是0x06054b50
,为什么检测的时分是反着写的。这儿就涉及到一个大端小端的问题,录音的时分也能会遇到巨细端顺序的问题,反过来读就好了。
涉及到二进制的检查和修正,咱们能够运用010editor这个软件来检查文件的十六进制或许二进制,并且能够手动修正某个方位的二进制。
他的界面大致长这样子,小端显示的,咱们能够看到咱们要得到的06 05 4b 50
,
咱们看上面的表格里面最终一个表格里的 .ZIP file comment length(n)
和 .ZIP Comment
,意思是描绘长度是两个字节,描绘长度是n,表示这个长度是可变的。这个有啥效果呢?
其实便是给了一个能够写额定的描绘数据的当地(.ZIP Comment),他的长度由前面的.ZIP file comment length(n)来操控。也便是zip答应你在它的文件完毕后面额定的追加内容,而不会影响前面的数据。描绘文件的长度是两个字节,也便是一个short的长度,所以理论上能够寻址2^16^个方位。
举个比如:
修正之前:
修正之后
看上面两个文件,修正之前长度为0,咱们把它改成2(留意巨细端),咱们改成2,然后随意在后面追加两个byte,保存,翻开修正之后的zip,发现是能够正常运转的,甚至咱们能够在长度是2的基础上追加多个byte,其实仍是能够翻开的。
所以回到标题内容,其实apk便是zip,咱们同样能够在apk的Comment后面追加内容,比方能够作为途径来源,或许完成这样的需求:h5网页A上下载的需求翻开某个ActivityA,h5网页B上下载的需求翻开某个ActivityB。
原理仍是上面的原理,写入途径或许配置,读取apk途径或许配置,做相应统计或许操作。
//magic -> yocn
private static final byte[] MAGIC = new byte[]{0x79, 0x6F, 0x63, 0x6E};
//没有zip文件注释时分的目录完毕符的偏移量
private static final int RawEndOffset = 22;
//0x06054b50占4个字节
private static final int endOfDirLength = 4;
//目录完毕标识0x06054b50 的小端读取方法。
private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};
//注释长度占两个字节,所以理论上能够支持 2^16 个字节。
private static final int commentLengthBytes = 2;
//注释长度
private static final int commentLength = 8;
private boolean isZipFile(File file) throws IOException {
if (file.exists() && file.isFile()) {
if (file.length() <= RawEndOffset + endOfDirLength) {
return false;
}
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
//seek到完毕符号地点的方位
randomAccessFile.seek(fileLength - RawEndOffset);
byte[] end = new byte[endOfDirLength];
//读取4个字节
randomAccessFile.read(end);
//关掉文件
randomAccessFile.close();
return isEndOfDir(end);
} else {
return false;
}
}
/**
* 是否契合文件夹完毕符号
*/
private boolean isEndOfDir(byte[] src) {
if (src.length != endOfDirLength) {
return false;
}
for (int i = 0; i < src.length; i++) {
if (src[i] != endOfDir[i]) {
return false;
}
}
return true;
}
/**
* zip(apk)尾追加途径信息
*/
private void write2Zip(File file, String channelInfo) throws IOException {
if (isZipFile(file)) {
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//seek到完毕符号地点的方位
randomAccessFile.seek(fileLength - commentLengthBytes);
byte[] lengthBytes = new byte[2];
lengthBytes[0] = commentLength;
lengthBytes[1] = 0;
randomAccessFile.write(lengthBytes);
randomAccessFile.write(getChannel(channelInfo));
randomAccessFile.close();
}
}
/**
* 获取zip(apk)文件完毕
*
* @param file 目标哦文件
*/
private String getZipTail(File file) throws IOException {
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
//seek到magic的方位
randomAccessFile.seek(fileLength - MAGIC.length);
byte[] magicBytes = new byte[MAGIC.length];
//读取magic
randomAccessFile.read(magicBytes);
//如果不是magic完毕,返回空
if (!isMagicEnd(magicBytes)) return "";
//seek到读到信息的offest
randomAccessFile.seek(fileLength - commentLength);
byte[] lengthBytes = new byte[commentLength];
//读取途径
randomAccessFile.read(lengthBytes);
randomAccessFile.close();
char[] lengthChars = new char[commentLength];
for (int i = 0; i < commentLength; i++) {
lengthChars[i] = (char) lengthBytes[i];
}
return String.valueOf(lengthChars);
}
/**
* 是否以魔数完毕
*
* @param end 检测的byte数组
* @return 是否完毕
*/
private boolean isMagicEnd(byte[] end) {
for (int i = 0; i < end.length; i++) {
if (MAGIC[i] != end[i]) {
return false;
}
}
return true;
}
/**
* 生成途径byte数组
*/
private byte[] getChannel(String s) {
byte[] src = s.getBytes();
byte[] channelBytes = new byte[commentLength];
System.arraycopy(src, 0, channelBytes, 0, commentLength);
return channelBytes;
}
//读取源apk的路径
public static String getSourceApkPath(Context context, String packageName) {
if (TextUtils.isEmpty(packageName))
return null;
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
return appInfo.sourceDir;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
这儿运用了一个魔数的概念,标明是否是写入了咱们特定的途径,只要写了咱们特定途径的基础上才会去读取,避免读到了没有写过的文件。
读取途径的时分首要获取装置包的绝对路径。Android体系在用户装置app时,会把用户装置的apk拷贝一份到/data/apk/路径下,通过getSourceApkPath 能够获取该apk的绝对路径。如果运用rw
可能会有权限问题,所以读取的时分只运用r
就能够了。
参阅: ZIP文件格局剖析 全民K歌增量升级计划