美文网首页
检测zip文件完整(进阶:APK文件渠道号)

检测zip文件完整(进阶:APK文件渠道号)

作者: YocnZhao | 来源:发表于2019-07-31 17:02 被阅读0次

    朋友聊天讨论到一个问题,怎么检测zip的完整性。zip是我们常用的压缩格式,不管是Win/Mac/Linux下都很常用,我们做文件的下载也会经常用到,网络充满不确定性,对于多个小文件(比如配置文件)的下载,我们希望只发起一次连接,因为建立连接是很耗费资源的,即使现在http2.0可以对一条TCP连接进行复用,我们还是希望网络请求的次数越少越好,不管是对于稳定性还是成功失败的逻辑判断,都会有益处。

    这个时候我们常用的其实就是把他们压缩成一个zip文件,下载下来之后解压就好了。

    但很多时候zip会解压失败,如果我们的zip已经下载下来了,其实不存在没有访问权限的问题了,那原因除了空间不够之外,就是zip格式有问题了,zip文件为空或者只下载了一半。
    这个时候就需要检测一下我们下载下来的zip是不是合法有效的zip了。
    有这么几种思路:

    1. 直接解压,抛异常表明zip有问题
    2. 下载前得到zip文件的length,下载后检测文件大小
    3. 使用md5或sha1等摘要算法,下载下来后做md5,然后比对合法性
    4. 检测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的长度,所以理论上可以寻址216个位置。
    举个例子:

    修改之前
    修改之后
    看上面两个文件,修改之前长度为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歌增量升级方案

    相关文章

      网友评论

          本文标题:检测zip文件完整(进阶:APK文件渠道号)

          本文链接:https://www.haomeiwen.com/subject/jlftdctx.html