跳到主要内容
AZ Tools

xz(.xz)流结构分析器

.xz 是有框架的格式,这正是它和 gzip 的根本区别。每条流都以 12 字节的尾部结束,尾部的后向大小指向一份索引,索引里每个块对应一条记录,记着这个块的未填充大小和解压后大小。于是真实的解压后大小、块的数量、所有可定位的位置,只要读文件末尾几百字节就都有了。这就是 xz --list 面对 4 GB 的归档也能立刻作答的原因,而 gzip -l 只能把 .gz 最后四个字节读出来,然后指望它代表整个文件。本页读取的正是同一套框架,和 xz 一样从文件末尾往回走,并把全部内容显示出来:fd 37 7a 58 5a 00 魔数,携带校验类型(None、CRC32、默认的 CRC64、SHA-256)的流标志,以及守护这两个字节的 CRC32;然后是每个块的头部长度、头部是否自行声明了压缩后和解压后大小、存放的校验值,以及被正确解释的过滤器链。LZMA2 的字典大小不是一个普通数字,而是一段六比特编码,取值范围从 4 KiB 一直排到 4 GiB;它前面还可以挂一个用于 x86、ARM、ARM64、PowerPC、IA-64、SPARC、ARM-Thumb 或 RISC-V 的 BCJ 过滤器,并带有自己的起始偏移。这就是用 --x86 生成的文件链上有两个过滤器的原因,而本页把这条链写成可以原样传回给 xz 的形式。块之后是索引记录、索引自身的 CRC32,以及尾部的后向大小、重复一次的流标志和 YZ 魔数。全过程不做任何解压。这里没有实现 LZMA2,也不需要实现,这正是重点;但同时也意味着每个块的校验值是照抄出来的,而不是核对过的,因为要证明一个 CRC64 正确,终究得把数据解码一遍。相反,守护框架本身的四类 CRC32 都会重新计算,发现不一致时会如实报告而不是拒绝打开文件,所以索引损坏的文件、或者尾部标志与头部不一致的文件,完好的部分你依然都看得到。块数比看上去更值得留意:只有一个块就无法用多核解压,也没有任何读取器能定位到它中间。xz -T0 改变的正是这一点,它按大约三倍字典大小切分输入,因此比这更小的输入无论你要多少线程都还是一个块。同一个文件里也可以首尾相接地放多条流,流之间的填充必须是四的倍数个零字节;本页会逐条列出,对不属于任何流的尾随字节也会单独指出来。

使用方法

  1. 把 .xz 或 .tar.xz 拖到方框里。解析全部在页面内完成,不上传,也不解压。
  2. 先看概要:流数、块数、从索引取得的真实解压后大小、压缩比,以及这个文件使用的完整性校验方式。
  3. 展开某条流可以看到它的头部、块表格和索引。过滤器那一列按可以直接传给 xz 的写法呈现,所以 --x86 --lzma2=dict=8MiB 就是生成该块的那条链。
  4. 在计划并行解压或部分读取之前先看块数:只有一个块就意味着一个核心、无法定位,读取器有多少线程都一样。
  5. 如果框架结构那格显示不一致,去找红色标记:流头部、每个块头部、索引和尾部各有自己的 CRC32,尾部还会把流标志再重复一遍。

常见问题

不解压怎么可能知道解压后的大小?
因为 .xz 格式把这个数字写进了文件。每条流的末尾都有一份索引,逐块列出未填充大小和解压后大小,而尾部的后向大小说明这份索引从多靠前的位置开始。读取方跳到最后 12 字节,再跳到索引,把记录加起来就完事了——xz --list 就是这么做的,本页也是。这同样是 .xz 可以定位读取的原因:索引能告诉你某个解压后偏移落在哪个块里。gzip 没有对应的东西,它的 ISIZE 在文件最后四个字节,只描述最后一个成员,而且是对 2^32 取模的,所以多成员的 .gz 或超过 4 GB 的 .gz 根本无法只凭框架说出自己的大小。
我明明用了 -T0,为什么文件只有一个块?
因为多线程压缩按大约三倍字典大小切分输入,而默认预设 6 的字典是 8 MiB,所以小于约 24 MiB 的输入永远不会被切成两块。-T0 只是要求使用与核心数相同的线程,它没法从一块装得下的输入里凭空造出第二个块。如果你要的就是块本身——为了并行解压,或者为了让读取器能定位——请用 --block-size 或 --block-list 明确指定。代价是压缩率:每个块都从空字典开始,匹配无法跨越块的边界。
该用哪种校验类型,代价是什么?
流标志会指明四者之一:None、CRC32、CRC64 或 SHA-256,分别在每个块后面存放 0、4、8 或 32 字节。默认的 CRC64 几乎总是正确答案:每块只花几个字节,就能发现存储和传输真正会造成的偶发损坏。SHA-256 每块要 32 字节,CPU 开销也大得多,而且有必要说清楚它买不到什么:摘要就存在文件里,能改动数据的人同样能改动摘要。它防的是事故,不是攻击者;那种场景需要的是对整个文件的签名。至于 --check=none,它省下四到八个字节,代价是删掉了唯一能告诉你数据已经损坏的东西——只有在内容本身自带校验和时才说得过去,其他情况几乎都不划算。
未填充大小、总大小和压缩后大小有什么区别?
它们是同一个块的三种视角。压缩后大小只算 LZMA2 吐出的字节。真正写进索引的未填充大小,是块头部加上这些压缩数据再加上校验值,不计填充。总大小则是把未填充大小向上取整到四的倍数,因为每个块都会用零字节补齐,好让下一个块从对齐的位置开始。两者相减就是填充,介于零到三字节之间。本页把压缩数据长度和填充分开显示,并且只要头部自己声明了大小,就拿它和块头部相互核对——大小字段那一列里的 c 和 u 指的就是这两个声明。
本页说不一致,xz -t 却只说数据损坏,到底谁对?
两者都对,只是深度不同。xz -t 会把整个数据解码并逐块验证校验值,所以任何位置的损坏都逃不掉,但它在第一个错误处就停下,而且不管坏在哪儿都用同一句话报告。本页从不解码数据,所以压缩数据内部的损坏它完全看不见;作为交换,它会分别重算框架里的每个 CRC32,因此能指出是哪个结构坏了:流头部、某个块的头部、索引,还是尾部。它还会把头部的流标志和尾部那份副本对比,这种不一致在任何基于大小的检查里都看不出来,却是货真价实的损坏信号。用本页定位损坏,用 xz -t 判断数据本身还能不能用。
能列出 .tar.xz 里面的文件吗?
不能,任何读取器不解压都做不到。带着文件名、大小和权限的那个 tar 归档就是数据本体,而这份数据被 LZMA2 压成了一段不透明的字节流。从框架里你能得到的是:解出来以后这个 tar 有多大(开始之前就知道放不放得下)、它有几个块(能不能用多核提取),以及容器是否完好。如果解压后大小正好是 512 的整数倍,那多半确实是个 tar,因为 tar 会把每个成员乃至整个归档都补齐到这个块长。
过滤器链里的字典大小到底说明了什么?
说明压缩器最远能往回看多少去找重复,因而也大致说明解压方需要多少内存:解码器会按字典大小分配缓冲区,所以用 -9(64 MiB)写出的 .xz 打开时大约要 65 MB,而有些机器拿不出来。同一个文件在笔记本上打开正常、在嵌入式设备或 CI 上解压失败,通常就是这个原因。LZMA2 把它存成六比特编码:第 0 位选择尾数 2 或 3,其余是指数,于是取值依次是 4 KiB、6 KiB、8 KiB、12 KiB,一直排到 3 GiB,只有 40 这个值被保留给 4 GiB 减一。要是当成普通整数读,默认的 8 MiB 字典会显示成 22。

相关工具