arc_conv_r55工具解析:ARC容器目录项与偏移转换实战

发布时间:2026/10/15 19:07:35
arc_conv_r55工具解析:ARC容器目录项与偏移转换实战
简介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, base16): 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 格式而不是 CSVJSON 对特殊字符的转义是标准化的。如果项目硬性要求 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, encodingutf-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, encodingutf-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和源数据对齐能直接发现重打包过程中有没有改错数据流。我一般在完整转换之后强制走一遍这步再去向下游交付结果。宁可多花一分钟也不让“应该没问题”这种直觉替我做检查。希望帮到你。本文还有配套的精品资源点击获取