likes
comments
collection
share

二进制文件解析系列-如何解析Android中的FLAT文件?

作者站长头像
站长
· 阅读数 37

解析文件头信息

要读取一个二进制文件,首先要搞清楚的就是他的 文件格式,按照 byte 一个个去读取 所需要的信息即可

二进制文件解析系列-如何解析Android中的FLAT文件?

二进制文件解析系列-如何解析Android中的FLAT文件?

看下代码:

val file = File("/Users/njvivo/Desktop/company_code/forkBooster/FLATReader2/assets/test2.png.flat")
val chan = FileChannel.open(file.toPath(), StandardOpenOption.READ)
// 小端读
val buffer = chan.map(FileChannel.MapMode.READ_ONLY, 0, file.length()).order(ByteOrder.LITTLE_ENDIAN)
// 文件头
val magic = buffer.int
// 这里magic取出来是一个10进制的值   这里转成16进制就是flat的固定文件头了
println("magic:$magic  ${magic.toString(16)}")
// aapt2 版本
val version = buffer.int
println("version:$version")
// 多少个资源项
val entryCount = buffer.int
println("entryCount:$entryCount")
// 文件类型
val entryType = buffer.int
println("entryType:$entryType")

// 内容长度 long一次性读8个byte
val entryLength = buffer.long
println("entryLength:$entryLength")



我们也可以看下 dexdump出来的文件

二进制文件解析系列-如何解析Android中的FLAT文件?

这里注意涉及到一个大小端解读的问题, 我们其实不用那么绕,只要谨记下面几个规则:

  1. 数据自身没有任何意义,意义是由观测者对数据的解读而产生的。对一份原始数据,正确的解读方式(正确使用大小端来解读)可以得到正确的信息,错误的解读方式则毁天灭地了

  2. 大小端只在多字节数据中 单元内部生效,单元和单元之间 是没有所有大小端一说的

  3. 对于解析二进制文件而言, 我们只要找到二进制文件格式的 格式说明,看下这个文件头的值是多少,即可猜出来 到底是应该用大端还是小端来解读

譬如对于flat文件来说,谷歌说了 文件头是 54504141, 那么hexdump打开看一下,就知道应该用小端来读了

解析 protobuf 模块

二进制文件解析系列-如何解析Android中的FLAT文件?

可以在这个路径下面 找到我们的proto文件 剩下的就是解析了

android.googlesource.com/platform/fr…

比如我们要提取出关键的 文件路径信息 则是要关注 这个ResourcesInternal 文件

二进制文件解析系列-如何解析Android中的FLAT文件?

注意要拷贝完全不要遗漏

二进制文件解析系列-如何解析Android中的FLAT文件?

另外注意proto文件 指定一下我们自己想要的package

二进制文件解析系列-如何解析Android中的FLAT文件?

有了protobuf的源文件,我们就是要生成出对应的java类,

注意 这里protbuf 源文件 生成java类的过程 ,有两种方式,一种是命令行生成,

这种方式 比较简单,大家自行百度即可,用命令行生成java类以后 就可以把java类 拷贝到我们的工程下面就能直接用了

使用的时候 不要忘记添加依赖:

implementation 'com.google.protobuf:protobuf-java:3.10.0'

如果想自动生成的 则需要 另外配置一些 依赖

首先当然是要apply plugin

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.6.21'
    id "com.google.protobuf" version "0.8.19"

}

然后指定你的proto文件位置

sourceSets{
    main {
        proto{
            srcDir 'src/main/proto'
        }
    }
}

最后配置一下 你的 proto task参数即可

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.10.0'
    }
    // 配置你生成java类的路径
    generatedFilesBaseDir = "$projectDir/src/generated"
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option "lite"
                }
            }
        }
    }
}

tasks.withType(Copy).all { duplicatesStrategy 'exclude' }

然后build 一下,你就发现 自动生成了你的java类了

二进制文件解析系列-如何解析Android中的FLAT文件?

有了这些东西 我们就可以 解析我们flat文件中的 pb模块了,我们以 获取文件路径信息 为例子

我们首先观察一下 文件信息 是放在哪个类中的

二进制文件解析系列-如何解析Android中的FLAT文件?

然后看一下 flat 文件中 pb 部分的格式

二进制文件解析系列-如何解析Android中的FLAT文件?

那我们从 header size 开始解析即可

val headerSize = buffer.int
println("headerSize:$headerSize")

val dataSize = buffer.long
println("dataSize:$dataSize")

// 先申请一块 bytearry
val byteArray = ByteArray(headerSize)
// 然后从buffer 中取出来
buffer.get(byteArray)
val fileInfo = ResourcesInternal.CompiledFile.parseFrom(byteArray)

println("resourceName:${fileInfo.resourceName}")
println("resourcePath:${fileInfo.sourcePath}")

还能从flat文件中 直接取出原始文件?

这个骚操作当然是可以的, 直接取出原始文件 以后 我们可以对原始文件进行 压缩等骚处理, 当然坏处就是涉及到一次文件拷贝,效率会很低

那么来看下 如何 从flat 文件中 还原出 原始的文件

我们可以看下 第一小节的内容 其中文件头信息是 4+4+4+4+8 =24 一共是固定24个byte

然后再看下我们的资源数据

其中head+datasize的信息 一共是12个byte 换句话说 一共是36个byte 这36个byte是固定的不变的

36+head 这个就可以算出来 一直到head部分的byte大小了,这个时候要主意了,head大小后面 还要跟一个header padding的 4字节对齐的大小, 所以在data 部分之前的byte大小一共就是

36+head + (36+head)%4

有了这个数据 剩下的就是取出 data部分的 bytearray 然后写文件就可以了

全部代码


fun main() {
    val file = File("你要读取的flat文件地址")
    val chan = FileChannel.open(file.toPath(), StandardOpenOption.READ)
    val buffer = chan.map(FileChannel.MapMode.READ_ONLY, 0, file.length()).order(ByteOrder.LITTLE_ENDIAN)
    // 文件头
    val magic = buffer.int
    println("magic:$magic  ${magic.toString(16)}")
    // aapt2 版本
    val version = buffer.int
    println("version:$version")
    // 多少个资源项
    val entryCount = buffer.int
    println("entryCount:$entryCount")
    // 文件类型
    val entryType = buffer.int
    println("entryType:$entryType")

    // 内容长度
    val entryLength = buffer.long
    println("entryLength:$entryLength")

    val headerSize = buffer.int
    println("headerSize:$headerSize")

    val dataSize = buffer.long
    println("dataSize:$dataSize")

    // 先申请一块 bytearry
    val byteArray = ByteArray(headerSize)
    // 然后从buffer 中取出来
    buffer.get(byteArray)
    val fileInfo = ResourcesInternal.CompiledFile.parseFrom(byteArray)

    println("resourceName:${fileInfo.resourceName}")
    println("resourcePath:${fileInfo.sourcePath}")

    val before = 24 + 12 + headerSize
    // 4字节对齐的偏移量
    val pos = before % 4
    // 定位到特定位置
    buffer.position(before + pos)

    // 取出原始文件的byte 信息
    val sourceByteArray = ByteArray(dataSize.toInt())
    buffer.get(sourceByteArray)

    // 写入文件
    val file2 = File("你要写入的文件")
    file2.writeBytes(sourceByteArray)


}
转载自:https://juejin.cn/post/7141185048807473166
评论
请登录