Ohhnews

分类导航

$ cd ..
foojay原文

Arcana:一个支持几乎所有归档格式的纯Java库

#arcana#java#压缩解压#开源库#文件格式

Arcana 是一个开源 Java 库兼命令行工具,可列出、提取并识别 40 多种归档、压缩和磁盘映像格式,不含任何原生代码,也不依赖任何第三方库。它运行于 Java 8 及以上版本,打包后仅 1.2 MB,以 Apache 2.0 许可发布在 Maven Central 上,坐标为 io.github.realburst:arcana。

为什么还要再写一个归档库?

如果你曾经在 Java 应用里打开“用户上传的随便什么文件”,那你一定熟悉这套流程。ZIP 在 JDK 里就有。Apache Commons Compress 覆盖了 TAR、7z、CPIO、AR 以及常见压缩器,但某些编解码器需要额外的库,Zstandard 还要走 JNI 绑定。RAR 需要另一个库,而纯 Java 的 RAR 5 支持很难找到。至于 CAB、MSI、ISO、WIM 或安装程序 .exe,通常的答案是借助原生绑定打包 7-Zip,并且每个操作系统和 CPU 各配一个二进制。

我想要的是一个 JAR,能在任何 JVM 上完成上述所有事情,无需安装任何东西,也无需加载任何原生库。这就是 Arcana:单一入口,按内容而非文件扩展名检测格式,并且无论文件是 .tar.zst、RAR 5 分卷集合、MSI 包还是 macOS .dmg,用的都是同一套 API。

Arcana 能做什么

Arcana 可以提取它支持的每一种格式,并能创建最常见的那些格式。

类别格式创建
归档ZIP(ZipCrypto、WinZip AES、Deflate64、Zip64)、7z、RAR 4 与 RAR 5(固实、分卷、加密头)、TAR、CPIO、AR / DEB、RPM、XAR、ARJ、LHAZIP、7z、TAR、LHA
压缩GZIP、BZIP2、XZ、LZMA、Zstandard、LZ4、Brotli、Snappy、lzip、lzop、Unix .Z、MS SZDDGZIP、BZIP2、XZ、TAR + gz / bz2 / xz / lz4
WindowsCAB(MSZIP、LZX)、MSI(真实的安装文件树)、OLE 复合文件、CHM、WIM / ESD(XPRESS、LZX、LZMS)、自解压 .exeCAB
磁盘映像ISO 9660(Joliet、Rock Ridge)、UDF、SquashFS、含 HFS+ 的 Apple DMG-

加密归档同样受支持:ZIP 和 7z 支持 AES-256(读写皆可),RAR 4 和 RAR 5 支持 AES-128 与 AES-256。任何压缩器内部的 TAR 或 CPIO 都能一步解开,而那些“伪装成普通文件”的归档(.docx、.odt、.epub、.apk、.jar)也能像普通归档一样打开。

除了解压,Arcana 还可以:

  • 识别任意文件:归档类型与版本、可执行文件(PE、ELF、Mach-O)、安装程序、带尺寸的图片、PDF、SQLite、字体;
  • 恢复损坏的归档:强制提取会保留所有可读内容,并输出一份关于每个文件状态的报告;
  • 拆分黏连在一起的文件,例如安装程序或固件镜像;
  • 加载插件以支持更多格式。官方插件可处理 Inno Setup、NSIS、InstallShield 和 WiX Burn 安装程序,以及 UPX 加壳的可执行文件。

快速上手

添加依赖:

<dependency>
  <groupId>io.github.realburst</groupId>
  <artifactId>arcana</artifactId>
  <version>1.0.8</version>
</dependency>

提取和列出永远不需要你说明文件是什么:格式是按其内容检测的。

import be.stef.arcana.Arcana;
import be.stef.arcana.ArcanaEntry;
import be.stef.arcana.analyze.Identification;

// 提取任何文件:.zip、.rar、.7z、.tar.zst、.msi、.iso、.dmg……
Arcana.extractTo(new File("upload.bin"), new File("out"));

// 只列出条目,不解压
for (ArcanaEntry e : Arcana.listEntries(new File("backup.rar"))) {
    System.out.println(e.getName() + "  " + e.getUncompressedSize());
}

// 加密归档
Arcana.withPassword("secret".getBytes("UTF-8")).extract(new File("secure.7z"), new File("out"));

// 这是什么文件?
Identification id = Arcana.identify(new File("mystery.bin"));
System.out.println(id.type + " - " + id.description);

创建归档同样简短:

import be.stef.arcana.ArcanaFormat;
import be.stef.arcana.compressor.ZipCompressor;
import be.stef.arcana.plugin.FormatRegistry;

byte[] password = "secret".getBytes("UTF-8");
new ZipCompressor(password).compress(new File("mydir"), new File("secure.zip"));   // WinZip AES-256
FormatRegistry.builtin(ArcanaFormat.SEVEN_Z).createCompressor(9, password).compress(new File("mydir"), new File("vault.7z"));

同一个 JAR 也是一个命令行工具:

java -jar Arcana-v1.0.8.jar x setup.exe -o out       # 提取
java -jar Arcana-v1.0.8.jar l disk.dmg               # 列出
java -jar Arcana-v1.0.8.jar i mystery.bin            # 识别
java -jar Arcana-v1.0.8.jar r damaged.zip -o rescue  # 恢复

在 Windows 上,还有一个可选脚本会向资源管理器右键菜单添加“Arcana Unpack / List / Info”三项。

默认安全

归档提取器按定义就要读取不可信输入,因此 Arcana 把每个文件都当作敌意对象来处理。

  • 没有路径穿越。 在写入任何内容之前,条目名称都会先被清洗:不允许绝对路径,不允许 ..,也不允许 Windows 禁止的字符。一个“zip slip”条目无法逃出目标文件夹。
  • 没有解压炸弹。 每次提取都在可于运行时修改的限制下进行(ExtractionLimits):写入 1 GiB 后输出/输入比上限为 1000,可选的总体积上限,以及至少 256 MiB 的可用磁盘空间。
  • 内存有界。 解码器会限制头部所能索取的上限:Zstandard 窗口被限制在 128 MiB,与 zstd 工具一致;7z 密钥派生被限制在 2^24 轮。精心构造的头部只会得到错误,而不是 OutOfMemoryError。
  • 校验和会被验证。 CRC32、Adler-32、BLAKE2sp、XXH64、WinZip AES 的 HMAC:只要格式中存有校验和,Arcana 就会校验并报告不匹配。
  • 插件必须公开其代码。 除非插件 JAR 包含其源代码、声明其许可证与源码 URL,并使用自己的包名,否则会被拒绝。这并不能让插件变安全,但能让它可被审查。

内部实现

每个解码器都用 Java 编写:Deflate64、LZMA 与 LZMA2、PPMd、Zstandard、Brotli、LZ4、LZX、LZMS、XPRESS、LZFSE 与 LZVN、ADC、LZO1X、RAR 4 与 RAR 5 算法、AES 以及密钥派生。核心部分约 80,000 行 Java 8 代码,除 JDK 外没有任何依赖。它是依据公开规范编写的:PKWARE APPNOTE、7-Zip 格式说明、微软公开规范(MS-CAB、LZX、MS-XCA)、Zstandard 的 RFC 8878、Brotli 的 RFC 7932、HFS+ 的 Apple TN1150。

有几项设计选择塑造了这个库:

  • 按内容检测。 先看魔数,再看尾部特征(DMG 通过其最后 512 字节中的“koly”块识别),最后才看文件扩展名。被改过名的文件照样能打开。
  • 格式允许时采用流式处理。 单流格式从 InputStream 解码,并使用有界缓冲。Zstandard 解码器只保留当前帧的窗口,因此一个数 GB 的 .tar.zst 只需几十 MB 堆内存即可解码。
  • 格式需要时采用随机访问。 磁盘映像是惰性读取的:DMG 分区借助小型缓存逐块解码,绝不会整个载入内存。
  • 用真实名称,而非内部键。 MSI 会按其安装的文件树提取,使用其 File 和 Directory 表中的名称,而不是一个充满晦涩键名的 cab 包。

每个版本都会经过回归测试套件。约 180 个小型样本随代码一同提交:每种格式和方法各一个,外加必须干净失败、绝不挂起的损坏文件。另有一个真实文件语料库(安装程序、来自 GitHub Releases 的 DMG 镜像、MSI 包)会对照提交的 SHA-256 参考值进行检查。仓库中每种格式还有一页技术文档,涵盖检测、布局、支持的方法、限制以及所用来源。

当前局限

Arcana 是一个年轻的项目,仓库为每种格式维护着一份公开的已知问题清单。在你依赖它之前,主要的局限有:

  • 提取优先。 写入仅限 ZIP、7z、TAR(+ gz、bz2、xz、lz4)、GZIP、BZIP2、XZ、LHA 和 CAB。Arcana 无法创建 RAR、Zstandard 或 Brotli 文件,也无法就地更新已有归档。
  • 某些文件系统只能原样复制。 DMG 内的 APFS 卷会被作为原始映像提取,而不会展开为文件。加密和稀疏 DMG 映像、拆分 WIM 文件(.swm)以及某些少见的压缩方法(旧的 LHA 方法、CAB 中的 Quantum)不受支持。
  • 纯 Java 是有代价的。 解码器对大多数用途来说足够快,但比不上原生代码。在为该项目所做的基准测试中,Zstandard 解码器达到原生 zstd 库约 60% 的速度。
  • 边界情况仍然存在。 少数损坏或不寻常的文件仍会抛出原始的 Java 异常而不是清晰的消息,或比参考工具更缺乏容忍度。这些情况都会记录相关文件与代码行,让你在使用某种格式于生产环境前先行判断。

如果某个文件失败,提交一个包含命令行、并在可能时附带该文件或重建方法的 issue,是最快的修复途径。## 试一试

最快的测试方法,是把 Arcana 指向你“下载”文件夹里最奇怪的文件,对它运行 arcana i,然后运行 arcana x。如果你维护的 Java 应用会接收用户上传的归档文件,试着用一个依赖替换原生绑定或一连串库,然后告诉我哪里会出问题。

欢迎提交 issue、失败的样本文件和插件。如果 Arcana 帮你省掉了一个依赖或一个原生二进制文件,在 GitHub 上加星标能帮助其他 Java 开发者找到它。

发现错误或有补充?在 GitHub 上编辑此页面

作者

Stéphane Bury

Stéphane Bury 是常驻比利时的 Java 开发者和 IT 顾问。他维护纯 Java 归档库 Arcana,以及纯 Java RAR 解压器 unrar5j。

相关文章

参与讨论