二进制文件解析系列-如何解析Android中的FLAT文件?
解析文件头信息
要读取一个二进制文件,首先要搞清楚的就是他的 文件格式,按照 byte 一个个去读取 所需要的信息即可
看下代码:
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出来的文件
这里注意涉及到一个大小端解读的问题, 我们其实不用那么绕,只要谨记下面几个规则:
-
数据自身没有任何意义,意义是由观测者对数据的解读而产生的。对一份原始数据,正确的解读方式(正确使用大小端来解读)可以得到正确的信息,错误的解读方式则毁天灭地了
-
大小端只在多字节数据中 单元内部生效,单元和单元之间 是没有所有大小端一说的
-
对于解析二进制文件而言, 我们只要找到二进制文件格式的 格式说明,看下这个文件头的值是多少,即可猜出来 到底是应该用大端还是小端来解读
譬如对于flat文件来说,谷歌说了 文件头是 54504141, 那么hexdump打开看一下,就知道应该用小端来读了
解析 protobuf 模块
可以在这个路径下面 找到我们的proto文件 剩下的就是解析了
android.googlesource.com/platform/fr…
比如我们要提取出关键的 文件路径信息 则是要关注 这个ResourcesInternal 文件
注意要拷贝完全不要遗漏
另外注意proto文件 指定一下我们自己想要的package
有了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类了
有了这些东西 我们就可以 解析我们flat文件中的 pb模块了,我们以 获取文件路径信息 为例子
我们首先观察一下 文件信息 是放在哪个类中的
然后看一下 flat 文件中 pb 部分的格式
那我们从 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