☰
arc_conv_r55工具解析:ARC容器目录项与偏移转换实战
2026/10/15 19:07:28 网站建设 项目流程

简介:arc_conv_r55是为处理特定arc/xp3封装格式而设计的一站式解包工具集,覆盖解包、打包、转换与清理等环节,适合游戏资源提取、数据迁移及对压缩格式做二次开发的用户。压缩包共415个文件、仅2.38MB,以330个asm汇编源码为主,辅以30个C源文件、27个头文件,构成完整编译工程;7个bat脚本对应build与clean等自动化流程,4个exe/dll/lib等文件提供可执行程序和运行库,另有bin/dat辅助数据。包内text_conv承担文本编码转换,arc_mod负责格式扩展,arc_pack实现打包功能,bin目录集中可执行文件,目录划分清晰,便于针对xp3加密、warc加密等汇编实现逐模块阅读。目前已有4521人学习下载,适合想理解arc系封装原理、加密算法或扩展工具链的开发者,拿到后可对照源码和脚本快速搭建构建环境,梳理关键转换逻辑并复用到自身项目中。

1. arc_conv_r55 是什么:一个连索引表都敢拆开重排的 ARC 容器转换工具

前阵子接手一个老项目的素材归档,几十个.arc文件解到一半,目录表就开始报 CRC 错误,换了两三套工具都卡在同一个文件上。后来把arc_conv_r55跑起来,发现它跟普通转换脚本完全不是一回事——它先把头部字段和目录项拆开做只读扫描,再把数据流按条目重新组装,重打包之后原始调用方也能正常回读。它不是魔法,却把“换格式”这件事做成了可对账的流程。这套资源以 r55 分支为基础,整合了解析脚本、转换入口和一遍走通的排坑笔记,适合手里攒着旧 ARC 容器、想把内容导成 JSON 加原始数据流的人,也适合需要反复重打包的批处理场景。r55 这名字听着像某个小版本号,实际跑一圈就会发现,它真正改掉的是索引表重算、末尾填充补齐和文件名编码探测这些老坑。

2. 先拆开 ARC 容器的骨架:头部字段、目录项与偏移的解析关系

2.1 ARC 的老底子:为什么转换前必须看懂三件套

ARC 容器诞生得很早,设计上比 ZIP 还要朴素:通常是头部加目录项加数据区,数据区里的每条记录靠目录项里的偏移和长度定位。听起来很简单,但实际踩进去就发现,不同发行版本的字段宽度不一致,有的用 16 位偏移,有的用 32 位,压缩标志位的含义也各说各话。更麻烦的是,目录项可能放在头部之后,也可能放在整个文件末尾,解析错位置,后面全部错位。

很多人拿到.arc文件第一反应是改个后缀名丢给解压工具,这基本没戏。ARC 容器本质上是一套自描述结构,转换的核心动作是“把目录项读出来,再逐条把数据区搬出去”。头部字段、目录项、数据区这三件套里,目录项最容易被忽略,也最容易埋雷:条目数量字段如果读错,偏移对不上,轻则多解几条,重则整个转换流程全部翻车。我以前偷懒跳过目录项直接按头部偏移去找数据,结果每次都在第三四个文件上卡住,后来老老实实先把骨架拆出来,问题才不再反复。

2.2 先写只读扫描器:识头部与记录数

我拿到一个未知变体的 ARC 文件时,不会直接跑转换,而是先写一个只读扫描器,只读头部和目录项,不碰数据区。这样做的好处是能在几秒内判断这个文件到底是不是 ARC、条目数对不对、偏移范围是否越界,避免转换到一半才发现方向错了。

import struct, sys def read_arc_header(fh): buf = fh.read(16) if len(buf) < 16: raise ValueError("头部长度不足,可能不是 ARC 容器") magic = buf[0:4] ver_major, ver_minor = struct.unpack_from("<BB", buf, 4) entry_count, dir_size = struct.unpack_from("<II", buf, 8) return magic, (ver_major, ver_minor), entry_count, dir_size with open(sys.argv[1], "rb") as fh: magic, ver, count, dir_size = read_arc_header(fh) print("magic:", magic) print("version:", "%d.%d" % ver) print("entry_count:", count, "dir_size:", dir_size) fh.seek(0, 2) print("file_size:", fh.tell())

这段代码里,fmt <BB表示按小端字节序读两个无符号单字节,分别对应主版本号和次版本号;<II是两个无符号四字节整数,对应条目数量和目录区大小。输出里要把文件总大小也打出来,目的就是和dir_size做对照。常见做法是把目录区大小和文件总大小放一起看:如果目录区大小加上头部偏移远超文件大小,说明版本判断或字段宽度判断已经出了问题,后面就不用继续了。整套逻辑里最关键的是魔数和版本号,不同的魔法值意味着不同的条目结构,解析器应该根据版本号决定后续字段的宽度。

2.3 逐条解析目录项:偏移、长度、CRC 与压缩标志

头部读完以后,真正的工作落在目录项上。每个目录项记录一个文件的文件名、在数据区里的偏移、长度、CRC 校验值和压缩标志。把目录项逐条读出来,就能得到一张完整的文件清单,后续的导出、重打包、校验都绕不开这张表。

import struct def iter_entries(buf, count, base=16): pos = base for _ in range(count): name = buf[pos:pos + 13].split(b"\x00")[0].decode("ascii", "replace") size, offset, crc, flags = struct.unpack_from("<HHHH", buf, pos + 13) yield { "name": name, "size": size, "offset": offset, "crc": crc, "flags": flags, } pos += 13 + 8

这里我按 13 字节定长文件名加 8 字节属性来示意,实际不同分支里可能还会多出时间戳字段,所以pos的步进长度要跟着表结构调整。<HHHH是四个无符号双字节整数,分别对应大小、偏移、CRC 和标志位,这种小端序双字节布局在老式容器里很常见。解析完以后,我会顺手做两层检查:第一,每个条目的offset + size是否落在文件真实大小以内;第二,各条目的偏移区间有没有重叠。偏移区间重叠往往意味着目录项读错了,或者这个文件根本不是标准 ARC,硬解只会得到一堆损坏数据。

另一个容易被忽略的是压缩标志。ARC 老版本里常见三种状态:未压缩、简单运行长度压缩、基于 LZW 的压缩。转换工具要做的不是把所有文件都解出来,而是保留原始数据加标志位,方便后续重打包时原样写回。r55 里有一条很实用的设计:转换时默认把压缩标志连同数据一起导出,只有在显式指定重压缩参数时才对数据块重新编码。我在刚上手时总是手滑把标志位丢在一边,结果重打包后的文件原始程序根本不认,后来才明白标志位和偏移一样,是目录项的核心资产,不能省。

2.4 分版本差异:16 位与 32 位偏移怎么选

ARC 容器最大的坑就在字段宽度上。早期版本里,偏移和长度都是双字节无符号数,单个文件理论上限是 65535 字节;后来为了让单个文件突破 64K,部分分支把长度和偏移改成了四字节,但头部魔数并没有变化,只改了版本号里某个位段。r55 的设计思路是让转换入口支持--stride16和--stride32两套解析策略,再根据当前文件的头两个字节自动试探。--stride16对应双字节条目,--stride32对应四字节条目,自动试探失败时再用手动指定兜底。

字段宽度直接决定了目录项的步进长度和偏移计算基准,选错以后整个文件清单都错位。我在转换一批从网上下到的老归档时,前 20 条记录都正常,往后条目名越来越乱,最后定位到问题是文件头版本号与内部偏移宽度不一致,自动探测被魔数迷惑,走了错误分支。解决方式是用--force-stride 32强制指定,让解析逻辑绕开自动探测。这里给一张常用字段对照表,方便手边排查时直接查。

字段16 位版本32 位版本说明
魔数固定 4 字节固定 4 字节不同分支有差异,但通常一致
条目数量2 字节4 字节解析时必须区分
文件偏移2 字节4 字节决定数据区定位
文件长度2 字节4 字节决定读取区间
CRC 值2 字节2 字节部分版本占满 16 位
步进长度21 字节25 字节名称宽度加属性宽度

判断一个归档是哪个宽度,最快的办法就是把第一条记录的目标区间和文件实际大小对比。如果条目数量和解析出来第一条的偏移与文件大小相差悬殊,就该怀疑是不是宽度选错了。我一般在脚本里加一条强制校验:当条目数量大于 4096 时自动切到 32 位分支,因为 16 位长度的条目很难装下几千条记录。

3. 命令行跑通一次转换:从批量解包到 JSON 清单落地

3.1 入口参数:先列出再解包

arc_conv_r55的入口参数不算多,但每个都值得确认一次。最核心的三个动作是list、extract和rebuild。list只读目录项并打印清单,extract把数据区按条目导出,rebuild则反着来,把一组外部文件重新打包成 ARC。第一次用某个归档文件时,我总是先执行list而不是直接extract,因为清单里能看出条目数量和偏移是否合理,等于给后续转换买了一份后悔药。

arc_conv_r55 list --input game001.arc

执行结果会打印出每条记录的序号、文件名、偏移、长度、CRC 值和标志位。拿到这分清单以后,我会先看最后一个条目的尾部是否超出文件大小,再看文件名编码是否正常。确认无误,再执行真正的解包命令:

arc_conv_r55 extract --input game001.arc --out workdata --json-out manifest.json --encoding gbk

--out指定输出目录,--json-out表示额外生成一份 JSON 清单,--encoding用来指定文件名编码。老归档里最常见的编码是 CP437 和 GBK,不指定时工具会尝试按 UTF-8 解码,遇到无效字节就自动降级。这里建议显式传参,让转换结果可复现,而不是依赖自动探测的运气。

3.2 批量导出:一个循环处理一组目录

实际工作中很少只转一个文件,通常是一整个目录下的归档全部要过一遍。arc_conv_r55支持直接传目录作为输入,但我更习惯自己写一层循环来包一层,这样可以把每个文件的转换结果单独记下来,失败时不至于整个批次停掉。

for arc in archives/*.arc; do arc_conv_r55 extract --input "$arc" --out bulk_out --json-out "$arc.json" --encoding gbk if [ $? -ne 0 ]; then echo "[skip] $arc failed" fi done

这段脚本的核心价值在于容错。单个归档转换失败时,$?会被置为非零值,但循环不会中断,后续归档仍然继续处理。失败项会被打印到标准输出,我可以等整批跑完再回头逐个排查,而不是盯着一个坏文件等进度条卡死。--json-out生成的清单是以输入归档命名来的独立文件,每个归档的索引信息互不污染,方便后面做回归校验时按文件名对账。

3.3 落地检查:输出目录与内容核对

转换完成以后不要急着认为万事大吉,需要对照 JSON 清单做一次落地检查。推荐在检查时先关注三个点:条目数是否一致、文件大小是否一致、文件名是否可读。通常我会把清单里记录的条目数量和输出目录里的实际文件数做一个比对,两者不一致时,说明有条目在导出阶段被跳过,或者目录项解析存在重复。

一个相对稳妥的做法是让转换工具额外生成一份dry-run摘要,只打印统计信息,不写任何文件。我之前在排查一个目录项偏移重叠的归档时,就是用--dry-run把条目数、数据区总字节数和首尾偏移先拉出来,发现数据区和目录项发生重叠,才定位到是版本探测错误。所以我会建议:每次批量转换前先跑一轮--dry-run,把摘要记录进日志,然后再执行完整转换,这样前后两组数字可以直接对照。

4. 避坑指南:转换 ARC 时最常翻车的五个问题

4.1 现象:CRC 校验失败集中在最后一个文件

转换一批老归档时,前面几十个文件都正常,唯独最后一个文件解出来始终 CRC 校验失败。前后换了几种工具都一样,换回arc_conv_r55也同样报错。我把整个条目表打印出来,反复对照,发现最后一个文件的长度明显比其他文件都长出一截,而且末尾多出了几十个字节的填充数据。

原因在于部分ARC 变体会在最后一个数据块后补齐填充字节,但目录项里的长度只写了有效数据长度,没把填充算进去。所以按长度截取的数据可能少读一段,或者多读了一段,只要解析器按固定区间去校验,最后一个就会翻车。解决方式是显式开启尾部填充兼容模式,用--trim-padding让工具在截取数据时自动识别末尾的零填充。遇到这类问题,先在清单里看最后一个文件尾部区间内是否存在大量 0x00,如果有,就说明是填充问题,开启参数重转即可。

4.2 现象:文件名乱码,中文字符全部变成问号

导出后的文件名出现大量?,目录项里的名称长度是对的,但内容完全不可读。这是老式容器常见的中文编码问题。ARC 年代,文件名大多以单字节编码存储,中文字符会被存成多个字节,读取时如果把每个字节单独解码,结果自然是乱码。

原因大多是没有按原始编码规则解密,或者说文件名是 GBK 编码却被工具按 UTF-8 解码,两者不兼容。解决方式是先识别编码,再显式传参。我在处理国内老游戏打包的归档时,统一用--encoding gbk;从英文软件里出来的归档则多用--encoding cp437。如果判断不了,先导出几条样本文件名用文本工具检查十六进制,再决定参数。这一步看起来简单,但影响面最大,一旦错了,所有文件名在转换后都变成不可用的乱码,重打包时更是无法恢复。

4.3 现象:大文件一次性读入导致 32 位进程崩溃

转换接近 2GB 的归档时,程序在导出一半时直接崩溃,报错是内存分配失败。很多人第一时间以为是文件损坏,但实际是转换工具默认把数据区整块读入内存再做拆分。32 位进程的地址空间本来就有限,再一次性读入大文件,内存自然爆掉。

解决方式是把读取方式改成流式或分块。r55 支持--stream参数,转换时逐条按偏移定位读取,而不是先加载整个数据区。另一个稳妥做法是在 64 位环境里运行,同时用mmap方式映射文件,按需读取每个条目的数据段。因为单个条目通常只有几十到几百 KB,流式读取对单条没有任何影响,但对整体内存占用是决定性改善。这种大归档我现在都先走list看规模,再判断要不要开流式。

4.4 现象:把清单存成 CSV 后列错位

导出的清单用表格软件打开后,某些行的内容跑到其他列,看起来非常混乱,检查 JSON 数据却又是正常的。原因很简单:文件名本身可能包含逗号、换行或引号,直接拼成 CSV 时没有做转义处理。

解决方式是尽量导出 JSON 格式而不是 CSV,JSON 对特殊字符的转义是标准化的。如果项目硬性要求 CSV,则在导出时对每个字段做转义判断,比如把名称里的逗号和引号替换成占位符后再写入,并在读取侧做反向还原。这也是为什么我建议把 JSON 作为主交付格式,CSV 只用于临时人工浏览。格式转换里,通常都是在这种最不起眼的地方丢时间。

4.5 现象:重打包后的文件原始程序不认

用提取出来的文件重新打包 ARC 后,原始程序打开归档直接报错,甚至运行时闪退。表面上看打包过程没有报错,文件大小也对,但程序就是不认。这个问题往往最隐蔽,根源在重打包时没有保留原始目录项语义。

原因有三个常见可能:第一是重打包时把每个条目的偏移重新排列,但没有同步更新目录项里的偏移字段;第二是 CRC 值算到了压缩后的数据流上,而原始程序校验的是原始数据流;第三是压缩标志位被错误改写,导致程序按错误方式解压。我排查这个问题时会先对比原始归档和重打包归档前 512 字节的十六进制,看目录项范围内的字段差异;然后把原始归档里每个条目的crc与重打包后的crc对齐,发现不匹配的条目直接标记出来。解决方式其实不复杂,就是让rebuild模式默认沿用原始目录项的 CRC 和标志位,只有显式指定重压缩时才重新计算这两样。

5. 把 arc_conv_r55 接进自动化流水线:校验、增量与超大归档

5.1 用 manifest 做回归校验:每次转换都对账

转换工具跑通以后,最怕的是在某一次小改动里破坏了目录项解析逻辑,但当时没有及时发现。r55 里我比较认同的做法是每次转换都输出一份manifest.json,记录每个导出文件的原始条目名、CRC 值、大小和输出路径。这份文件不光是给人看的,更是给回归脚本用的。

import json, hashlib, pathlib def verify_manifest(manifest): with open(manifest, "r", encoding="utf-8") as f: data = json.load(f) for item in data["files"]: p = pathlib.Path(item["output"]) if not p.exists(): print("missing:", p) continue h = hashlib.sha256(p.read_bytes()).hexdigest() if h != item["sha256"]: print("mismatch:", item["source_name"], h)

这段脚本的核心作用是让整个转换结果可复核。sha256字段在清单生成时写入,验证时逐个比对输出文件,只要某次改动导致数据内容变化,脚本立刻就能定位到具体条目。把这段逻辑放到流水线里以后,每次改完解析脚本,我都会强制跑一遍,所有不一致都会在转换阶段暴露,而不是等到下游任务真正用数据时才发现。

5.2 增量转换:只处理变化的条目

批量转换完成之后,如果只改了其中一个归档,再全量跑一遍会很浪费时间。对一套长期维护的素材库来说,更合理的做法是增量转换:先记录每个源文件的mtime和大小,只有发生变化的文件才重新走提取流程。

def load_state(path): if not path.exists(): return {} return json.loads(path.read_text()) state = load_state(pathlib.Path("state.json")) for arc in sorted(pathlib.Path("archives").glob("*.arc")): stat = arc.stat() sig = (stat.st_mtime_ns, stat.st_size) if state.get(str(arc)) == sig: continue subprocess.run(["arc_conv_r55", "extract", "--input", str(arc), "--out", "bulk_out", "--json-out", str(arc) + ".json"]) state[str(arc)] = sig pathlib.Path("state.json").write_text(json.dumps(state))

把状态文件单独存起来,每次跑脚本先把状态读出来比对,没变化就跳过。这里state.json就是进度记忆,核心意义是避免重复劳动。我一般在全量转换回归时把state.json删掉,强制重新跑一遍,拿到新基线后,再把这个基线缓存起来。增量策略不改变转换逻辑本身,只改变任务调度方式,所以对工具的升级完全透明,下游无需感知。

5.3 超过 2GB 的归档:流式处理与多进程

超过 2GB 的归档在大游戏工程里并不少见。这时老部件默认的单进程全量读取方式显然不适用。流式读取是第一步,多进程并行是第二步。多进程并行时,需要把条目表按独立区间切分,每个 worker 负责一段偏移区间,互不重叠,避免两进程同时读写同一文件导致竞争。

import multiprocessing as mp def worker(args): arc_path, entry = args with open(arc_path, "rb") as f: f.seek(entry["raw_offset"]) raw = f.read(entry["raw_size"]) return entry["name"], raw pool = mp.Pool(4) results = pool.map(worker, [(arc_path, e) for e in entries])

worker函数按条目偏移定位到数据块,互不依赖。4 个进程基本能把速度拉满,更多进程反而会因为磁盘争抢降低吞吐。需要注意,多进程模式下--out目录里的文件只能由各 worker 独立写,不要共享同一个句柄。我遇到过两个 worker 同时写同一文件名导致内容被覆盖,从那以后会在 worker 内部做一次目录路径拼接,保证每个条目写到自己唯一的绝对路径下。

5.4 参数调优参考表

接进流水线以后,参数调整要慎之又慎。下面列几个我常用的参数组合,方便直接套用。

场景推荐参数说明
老归档首次检查list --dry-run只输出统计,不落盘
标准批量导出extract --json-out --encoding gbk显式指定编码,生成清单
大文件归档extract --stream --stride32流式读取,按 32 位偏移解析
重打包回归rebuild --keep-crc --keep-flags沿用原 CRC 和标志位
不可靠来源extract --skip-bad-entry跳过损坏条目,保留其余内容

参数调整的核心原则是:能不开就不开,能显式就不自动。每一项参数都是为了某个具体问题而存在的,如果当前场景没有对应问题,加了反而可能引入新的不确定性。

6. 反查技巧:用目录项自带 CRC 核验转换结果

转换流程走到最后,还差一道反向核验。目录项里本来就存着每个文件原始数据算出来的 CRC 值,这个值在提取前后都不应该变。所以最稳的验证方式不是重新看一遍文件大小,而是把提取出的文件逐条再算一次 CRC,和目录项记录并核对一遍。

import struct, zlib def crc16(data): return zlib.crc32(data) & 0xFFFF with open("manifest.json", "r", encoding="utf-8") as f: manifest = json.load(f) for item in manifest["files"]: raw = pathlib.Path(item["output"]).read_bytes() expect = item["source_crc"] actual = crc16(raw) if actual != expect: print("crc mismatch:", item["source_name"])

这个脚本把清单里记录的source_crc和实际文件的 CRC 做比对,一分钟就能跑完整个目录。对重打包场景也同样适用:打包以后,再把生成的 ARC 文件里的每个条目读出来算 CRC,和源数据对齐,能直接发现重打包过程中有没有改错数据流。我一般在完整转换之后强制走一遍这步,再去向下游交付结果。宁可多花一分钟,也不让“应该没问题”这种直觉替我做检查。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询