简介#
文件:klfa | 适用:饥荒单机版 iOS 端的数据包
klfa 是一个用 Lua 编写的命令行工具,用于把游戏资源目录打包成自定义的 .archive 归档文件,或者把归档文件解包回目录。整个脚本只依赖 Lua 标准库和 lfs(LuaFileSystem),无需任何第三方压缩库——它做的是纯打包,不压缩数据。
格式来源#
KLFA 格式是饥荒单机版 iOS 端的数据包打包格式,其结构由 QuickBMS 提取脚本 dont_starve.bms 定义:
# Don't Starve
# script for QuickBMS http://quickbms.aluigi.org
idstring "KLFA" # 校验魔数
get FILES long # 文件数量
for i = 0 < FILES
get NAMESZ long # 文件名长度
getdstring NAME NAMESZ # 文件名
get OFFSET long # 数据偏移
get SIZE long # 文件大小
get DUMMY byte # 占位字节
log NAME OFFSET SIZE # 提取文件
next iklfa 脚本就是参照这份脚本逆向实现了打包(pack)与解包(unpack)功能——QuickBMS 只能"读",这个脚本补全了"写"的能力,让模组作者可以把自己的资源重新打成游戏能识别的 .archive 包。
工具用法(自动检测模式,无需指定子命令):
# 解包:输入为 .archive,输出为目录
lua klfa data.archive output_dir
# 打包:输入为目录,输出为 .archive
lua klfa input_dir data.archive模式判断逻辑:输入以 .archive 结尾且输出不是 → 解包;输入是目录且输出以 .archive 结尾 → 打包;无法判断时报错退出。
归档文件格式#
KLFA 格式为小端序(Little-Endian),整体结构如下:
+------------------+-----------------------------+
| 区域 | 内容 |
+------------------+-----------------------------+
| 文件头 | 魔数 "KLFA" (4 字节) |
| | 文件数量 file_count (4 字节)|
+------------------+-----------------------------+
| 索引区(每项) | 文件名长度 (4 字节) |
| | 文件名 (name_size 字节) |
| | 数据偏移 offset (4 字节) |
| | 文件大小 size (4 字节) |
| | dummy 占位字节 (1 字节) |
+------------------+-----------------------------+
| 数据区 | 各文件内容依次连续存放 |
+------------------+-----------------------------+关键点:
- 魔数:文件前 4 字节必须是
KLFA,否则视为非法格式。 - 数据区起始偏移:
header_size = 8 + Σ(4 + name_size + 4 + 4 + 1),即文件头 8 字节加上所有索引项的长度。 - 偏移的巧合:打包时索引按扫描顺序依次写入,
current_offset累加各文件大小,因此索引项顺序与数据区顺序天然一致(索引区不含 padding,数据紧接索引区结束处开始)。 - 相对路径:打包时保存的是相对路径,内部统一使用
/作为分隔符,跨平台通用;解包时再转换回当前系统的分隔符。
核心算法#
字节序处理#
Lua 没有 32 位整数类型,脚本用字节拼接手动实现小端序读写:
-- 小端序读取 32 位整数
local function read_uint32(file)
local data = file:read(4)
if not data or #data ~= 4 then return nil end
local b1, b2, b3, b4 = data:byte(1,4)
return b1 + b2*256 + b3*65536 + b4*16777216
end写入则是反向拆分:反复对 256 取模取低位、整除 256 移位,最后用 string.char 拼成 4 字节。
递归建目录与递归扫描#
mkdirs(path):按分隔符逐级拆分路径,逐级检查lfs.attributes,不存在则创建,存在但不是目录则报错。相当于mkdir -p。scan_dir(dir):递归遍历输入目录,跳过.、..和.开头的隐藏文件/目录,把每个文件记录为{full_path, rel_path, size}。其中rel_path通过path:sub(#input_dir + 1)去掉输入目录前缀(input_dir已保证以分隔符结尾)得到,并统一转换为/分隔符。
打包流程(pack)#
- 递归扫描输入目录,收集文件列表(为空则报错)。
- 创建输出目录,以
"wb"打开归档文件。 - 写入魔数
KLFA和文件数量。 - 预计算
header_size,然后遍历文件列表依次写入索引项,同时累加current_offset得到每个文件在数据区的偏移。 - 二次遍历文件列表,逐个读取源文件内容写入数据区;写入前校验实际读到的字节数与索引记录的
size一致,防止扫描后文件被修改导致的错位。
解包流程(unpack)#
- 打开归档,校验魔数
KLFA。 - 读取文件数量,循环读取每条索引项(名字长度、文件名、偏移、大小,跳过 1 字节 dummy)。
- 遍历索引项:
- 根据文件名推导目录部分,用
mkdirs创建多级目录; file:seek("set", entry.offset)定位到数据区,按size读取并写入输出文件;- 校验读到的字节数与
size一致; - 文件创建失败且原文件名含非 ASCII 字符(如中文)时,自动改用随机英文名重试:
FIX_前缀 + 8 位随机字符,保留原扩展名,最多重试 3 次。
- 根据文件名推导目录部分,用
两个方向都有进度输出(每 10 个文件、第一个和最后一个文件时打印一行),成功/失败信息带 ANSI 颜色,出错时返回 nil, err 并以退出码 1 结束。
设计要点与局限#
| 项目 | 说明 |
|---|---|
| 数据存储 | 原样存储,不压缩(包体大小 = 所有文件之和 + 头部开销) |
| 完整性 | 无校验和/CRC,损坏只能靠长度校验发现 |
| 大小限制 | 32 位无符号偏移,理论上限约 4GB |
| 路径处理 | 打包时转 / 存储,解包时转回系统分隔符;中文文件名写入失败时自动改名重试 |
| 隐藏文件 | 打包时跳过 . 开头的隐藏文件/目录 |
| 幂等性 | 解包会直接覆盖同名文件 |
小结#
KLFA 本质上是一个"索引 + 数据"两段式的扁平归档:头部存魔数和文件数,索引区存每个文件的相对路径、偏移和大小,数据区按顺序连续存放文件内容。算法上没有压缩和加密,胜在实现简单、依赖少,适合作为模组资源打包的入门范例——理解了它的字节布局,也就能看懂大多数游戏自研归档格式的基本套路。