简介#

文件: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 i

klfa 脚本就是参照这份脚本逆向实现了打包(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)#

  1. 递归扫描输入目录,收集文件列表(为空则报错)。
  2. 创建输出目录,以 "wb" 打开归档文件。
  3. 写入魔数 KLFA 和文件数量。
  4. 预计算 header_size,然后遍历文件列表依次写入索引项,同时累加 current_offset 得到每个文件在数据区的偏移。
  5. 二次遍历文件列表,逐个读取源文件内容写入数据区;写入前校验实际读到的字节数与索引记录的 size 一致,防止扫描后文件被修改导致的错位。

解包流程(unpack)#

  1. 打开归档,校验魔数 KLFA
  2. 读取文件数量,循环读取每条索引项(名字长度、文件名、偏移、大小,跳过 1 字节 dummy)。
  3. 遍历索引项:
    • 根据文件名推导目录部分,用 mkdirs 创建多级目录;
    • file:seek("set", entry.offset) 定位到数据区,按 size 读取并写入输出文件;
    • 校验读到的字节数与 size 一致;
    • 文件创建失败且原文件名含非 ASCII 字符(如中文)时,自动改用随机英文名重试:FIX_ 前缀 + 8 位随机字符,保留原扩展名,最多重试 3 次。

两个方向都有进度输出(每 10 个文件、第一个和最后一个文件时打印一行),成功/失败信息带 ANSI 颜色,出错时返回 nil, err 并以退出码 1 结束。

设计要点与局限#

项目说明
数据存储原样存储,不压缩(包体大小 = 所有文件之和 + 头部开销)
完整性无校验和/CRC,损坏只能靠长度校验发现
大小限制32 位无符号偏移,理论上限约 4GB
路径处理打包时转 / 存储,解包时转回系统分隔符;中文文件名写入失败时自动改名重试
隐藏文件打包时跳过 . 开头的隐藏文件/目录
幂等性解包会直接覆盖同名文件

小结#

KLFA 本质上是一个"索引 + 数据"两段式的扁平归档:头部存魔数和文件数,索引区存每个文件的相对路径、偏移和大小,数据区按顺序连续存放文件内容。算法上没有压缩和加密,胜在实现简单、依赖少,适合作为模组资源打包的入门范例——理解了它的字节布局,也就能看懂大多数游戏自研归档格式的基本套路。