lzma1
Lempel-Ziv-Markov链算法(LZMA)的TypeScript实现
为 .lzma 格式,从 XZ代表Java. 它也会解码
7z存档中的原始LZMA1和LZMA2数据 .xz 文件。它开始
作为一个 叉 的 Nathan拉格的 LZMA-JS包。
为什么
JavaScript中有许多LZMA实现,但大多数都过时了,
没有维护,缺乏类型支持,或者依赖于特定的运行时api(例如,
node . js Buffer)。
这个版本是XZ的Java移植版本,是LZMA的一个简洁实现
Collin和Igor Pavlov,通过LZMA SDK进行了一些优化。它是
兼容 xz --format=lzma, 7-Zip和LZMA SDK,并且只使用web
标准的api。
功能
- 编码和解码
.lzma数据,可与xz、7-Zip和LZMA SDK - 将原始LZMA1和LZMA2数据解码为7z和
.xz存储它,给定 容器中的属性 - 字符串(UTF-8)和二进制数据(
ArrayBuffer或任何类型数组) - 异步压缩和解压缩与等待进度回调; 用于生产、进度和取消
- 压缩级别1-9或细粒度选项(字典大小,
lc/lp/pb,……) - 网页流(
TransformStream)用于压缩和解压缩流 - 截断输入和大部分损坏输入的错误
- 不可信原始数据的内存限制,在KiB中,如Java的XZ
- 一个可选的输出大小限制,以防御“解压炸弹”攻击
- 没有依赖,运行在浏览器、worker、Node.js、Deno和Bun中
安装
[!请注意) 这个包是本地的 ESM 并且不再提供CommonJS 出口。如果你的项目使用CommonJS,你将不得不转换为ESM或使用 动态
import()函数。
npm
npm install lzma1
浏览器
<script type="module">
import {
compress,
compressString,
decompress,
decompressString,
} from "https://esm.sh/lzma1@latest";
</script>
快速启动
六个功能压缩和解压 .lzma 格式,三解码
原始数据:
// For binary data
compress(data: ArrayBuffer | ArrayBufferView, mode?: 1-9 | CompressionOptions): Uint8Array
compressAsync(data: ArrayBuffer | ArrayBufferView, mode?: 1-9 | CompressAsyncOptions): Promise<Uint8Array>
decompress(data: ArrayBuffer | ArrayBufferView, options?: DecompressionOptions): Uint8Array
decompressAsync(data: ArrayBuffer | ArrayBufferView, options?: DecompressAsyncOptions): Promise<Uint8Array>
// For string data
compressString(data: string, mode?: 1-9 | CompressionOptions): Uint8Array
decompressString(data: ArrayBuffer | ArrayBufferView, options?: DecompressionOptions): string
// For raw data without a .lzma header, e.g. from 7z or .xz
decompressRawLzma1(data: ArrayBuffer | ArrayBufferView, options: RawLzma1Options): Uint8Array
decompressRawLzma1Async(data: ArrayBuffer | ArrayBufferView, options: RawLzma1AsyncOptions): Promise<Uint8Array>
decompressRawLzma2(data: ArrayBuffer | ArrayBufferView, options: RawLzma2Options): Uint8Array
这个 Async 函数返回的字节数与不带
后缀,并可选地等待 onProgressAsync 在它们工作的时候回调
调用者可以屈服于事件循环,显示进度或取消。这个
Callback的文档注释中有一个例子。重要:呼叫者可以阅读 data
但不得修改它,直到返回的承诺已经解决。
字符串被编码为UTF-8。 decompressString 也解码数据压缩
它使用Java的“修改过的UTF-8”。这个 LZMA 类
再次从v0.2.0导出;它的 decompress 现在返回一个 Uint8Array.
压缩字符串
import {
compressString,
decompressString,
} from "lzma1";
const data = "Hello World!";
const compressed = compressString(data, 1); // level 1 (fastest)
const decompressed = decompressString(compressed);
// data === decompressed
处理二进制数据
import {
compress,
decompress,
} from "lzma1";
const binaryData = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
const compressed = compress(binaryData, 5); // default level
const decompressed = decompress(compressed);
压缩选项
级别(1-9,默认为5)交易速度大小:
| 水平 | 编码器 | 匹配仪 | 字典 |
|---|---|---|---|
| 1 | 快 | hc4 | 1 MiB |
| 2 | 快 | hc4 | 2 MiB |
| 3 | 快 | hc4 | 4 MiB |
| 4 | 正常的 | bt4 | 1 MiB |
| 5 | 正常的 | bt4 | 2 MiB |
| 6 | 正常的 | bt4 | 4 MiB |
| 7 | 正常的 | bt4 | 8 MiB |
| 8 | 正常的 | bt4 | 16 MiB |
| 9 | 正常的 | bt4 | 32 MiB |
传递选项而不是关卡。没有给出的选项来自关卡。
const compressed = compress(data, {
level: 9, // preset 1-9
dictSize: 1 << 20, // dictionary size in bytes, 4 KiB - 768 MiB
lc: 3, // literal context bits, 0-4
lp: 0, // literal position bits, 0-4, lc + lp <= 4
pb: 2, // position bits, 0-4
mode: "normal", // "fast" or "normal" (optimal parsing)
niceLen: 64, // match length that is good enough, 8-273
matchFinder: "bt4", // "hc4" (hash chain) or "bt4" (binary tree)
depth: 0, // match finder search depth, 0 = automatic
endMarker: true, // false omits the end marker, see below
});
一个大于输入的字典被缩减到适合它,这节省了内存
压缩和解压缩时。数据与 lc + lp > 4 从其他
编码器(例如7-Zip)仍然可以解压缩。
报头保存未压缩的大小,因此数据后面的结束标记是
对于读取它的解码器来说是冗余的。 endMarker: false 省略它,比如XZ
Java和7-Zip没有 -eos,并节省5-6字节,这对于小的很重要
有效载荷,比如QR码。解码器不知道大小需要标记
查找结尾:原始LZMA解码器(xz --format=raw, Python的 FORMAT_RAW)
报告一个错误或解码到没有它的结束。只有当数据
用它的头(这个库, xz --format=lzma(7-Zip)或
大小在其他地方是已知的。知道大小的严格解码器可能会拒绝a
标记,例如XZ表示Java的raw LZMAInputStream 构造函数没有
enableRelaxedEndCondition().
Raw LZMA1和LZMA2
7z档案和 .xz 文件存储LZMA数据,没有 .lzma Header和keep
它的属性在它们自己的头文件中。将这些属性传递给
decompressRawLzma1 或者 decompressRawLzma2. 这个库不解析7z
或者 .xz 容器。
import {
decompressRawLzma1,
decompressRawLzma2,
} from "lzma1";
// A 7z LZMA entry. Its 5-byte coder properties are the byte
// (pb * 5 + lp) * 9 + lc and the dictionary size, 32-bit little-endian.
const file = decompressRawLzma1(packed, {
lc: 3, // literal context bits, 0-8
lp: 0, // literal position bits, 0-4
pb: 2, // position bits, 0-4
dictSize: 1 << 24, // dictionary size in bytes
uncompressedSize: 123_456, // optional, the size from the 7z header
memoryLimitKb: 65_536, // optional
});
// LZMA2 data from a 7z entry or an .xz block
const block = decompressRawLzma2(packed, {
dictSize: 1 << 20, // from the LZMA2 properties byte, see "LZMA2" below
memoryLimitKb: 65_536, // optional
});
LZMA2标记自己的末端,不需要大小。Raw LZMA1需要未压缩的
大小或结束标记。7-Zip写无止境
默认情况下,标记,所以从7z头传递大小。
xz --format=raw --lzma1 总是写一个。对于已知的大小,数据必须
在那里结束,有或没有结束标记,如with decompress. 结束标记
在此之前,大小是一个错误,如 strict.
字典大小来自容器,以及损坏或恶意的头文件
最高可申领4gib。 memoryLimitKb 就像Java的XZ一样 memoryLimit.
解码器根据字典大小估算其在KiB中的内存 lc 和
lp 对于LZMA1,并抛出 MemoryLimitError 在它分配任何东西之前
估价超出了限额。错误的 memoryNeededKb 和 memoryLimitKb 字段持有
两个数字。估计值按给定的大小计算字典。这个
字典缓冲区最多从64kib开始,只有在输出到达时才翻倍,
因此,在输出增加之前,一个巨大的声称字典不需要任何成本。已知的LZMA1
大小改变了这一点: decompressRawLzma1 然后一次分配最多64个MiB
对于字典或输出,它为 decompress 尺寸在吗
这个 .lzma 头。估计不包括输出。
流
Compress 和 Decompress 是 TransformStreamS,像内置的一样使用
CompressionStream:
import {
Compress,
Decompress,
} from "lzma1";
// Compress a file
const compressed = file.stream().pipeThrough(new Compress(5));
// Decompress a download
const response = await fetch("/data.lzma");
const text = await new Response(response.body.pipeThrough(new Decompress())).text();
流的大小是事先不知道的,所以标头将其标记为未知和
数据总是以结束标记结束: new Compress({ endMarker: false })
抛出一个 RangeError.
DecompressRawLzma1 和 DecompressRawLzma2 解码流中的原始数据和
采用与函数相同的选项。无效选项和字典结束
从构造函数抛出内存限制。
const output = response.body.pipeThrough(new DecompressRawLzma2({ dictSize: 1 << 20 }));
在TypeScript中,它们的类型需要 TransformStream 从 DOM 自由或
@types/node (或 skipLibCheck)。
错误
解压缩被截断或损坏的数据会抛出 Error,如。
"Truncated input" 或者 "Corrupted input: match distance is too large".
这个 .lzma 格式没有校验和,所以一些损坏的数据解码没有
输出错误。原始LZMA1和LZMA2数据也没有;7 z
和 .xz 将校验和保存在容器中。
无效选项引发 RangeError,还有一本字典 memoryLimitKb
抛出一个 MemoryLimitError.
数据必须以标题中的大小结束,带或不带结束标记
在它之后。在该大小之前的结束标记也结束了数据,因为有些
制作人写的大小太大了。 strict 拒绝它,如XZ表示Java
xz做:
decompress(data, { strict: true });
new Decompress({ strict: true });
在解压缩来自不可信源的数据时, maxOutputLength 可以保护
对抗“解压炸弹”攻击,在这种攻击中,几个字节的精心制作的数据会扩展
g。对于除原始LZMA2之外的每个解码器,它以字节为单位限制输出。
与 strict对于原始LZMA1,较大的声明大小会抛出错误
OutputLimitError 在分配任何东西之前。否则,与未知一样
大小,解码抛出一个一旦输出超过限制,因为一个结束标记
仍然可以结束其中的数据:
decompress(data, { maxOutputLength: 1_000_000 });
HTML例子
<!DOCTYPE html>
<html>
<body>
<textarea id="input">Hello World!</textarea><br />
<button id="run">Compress & Decompress</button>
<div id="result"></div>
<script type="module">
import { compressString, decompressString } from "https://esm.sh/lzma1@latest";
document.getElementById("run").onclick = () => {
const text = document.getElementById("input").value;
const compressed = compressString(text);
const decompressed = decompressString(compressed);
document.getElementById("result").innerHTML =
`Original: ${text.length} bytes<br>` +
`Compressed: ${compressed.length} bytes<br>` +
`Result: ${decompressed}`;
};
</script>
</body>
</html>
它是如何工作的
LZMA (Lempel-Ziv-Markov chain Algorithm)是一种压缩算法 将字典(lz77风格)压缩器与范围编码器相结合。它是已知的 因为它的高压缩比和在7z和xz格式中使用。
LZMA头
这个 .lzma 数据以一个13字节的头开始,后面跟着压缩数据:
packet-beta
0-7: "Properties (lc, lp, pb)"
8-39: "Dictionary Size (32-bit, LE)"
40-103: "Uncompressed Size (64-bit, LE)"
第一个字节是 (pb * 5 + lp) * 9 + lc. 所有的未压缩大小 0xFF
字节表示未知。更多的 信息 关于LZMA报头
结构。
LZMA2
LZMA2将数据分成未压缩的2 MiB和64 KiB的块 压缩。每个块都以一个控制字节开始:
| 控制 | 块 |
|---|---|
| 0 x00 | 数据结束 |
| 0 x01 | 未压缩,重置字典 |
| 0 x02 | 未压缩的 |
| 0 x80-0x9f | LZMA |
| 0 xa0-0xbf | LZMA,重置状态 |
| 0 xc0-0xdf | LZMA,重置状态,new lc/lp/pb |
| 0 xe0-0xff | LZMA,重置字典和状态,new lc/lp/pb |
在接下来的2个字节中,未压缩的块的大小为- 1。一个LZMA Chunk有这样的头,大小为- 1,大端序:
packet-beta
0-7: "Control (low 5 bits: size bits 16-20)"
8-23: "Uncompressed size, bits 0-15"
24-39: "Compressed size"
40-47: "Properties (0xC0 and up)"
第一个块必须重置字典,并且 lc + lp 不能超过4。
7 z和 .xz 将字典大小存储为一个字节 n. 尺寸是
0xFFFFFFFF 用于 n = 40 和 (2 | (n & 1)) * 2 ** ((n >> 1) + 11) 下面
从4千磅起。
基准
看到 文档/ benchmarks.md, 板凳上 来运行它们。
验证
编码器的输出字节与 XZ代表Java 在提交
6925f81
输入和编码器设置的语料库。解码器处理一组
除了两种已记录的腐化类型之外,腐化流的方式相同
它接受的输入,而Java的XZ拒绝它们。原始的LZMA1和LZMA2
解码器处理它们自己的一组流,就像Java的XZ一样。CI检查
所有三个。
设备/ README.md 描述语料库以及如何
重新生成它。
对于这个代码来说,通过测试是不够的。不同于的编码器决策 Java的XZ通常仍然产生正确解压缩的有效流, 因此,往返测试忽略了它,代码虽然很小,但具有巨大的输入 空间中,许多边缘情况和模算法可以将这些差异隐藏在其中。 所以对编码员的每一次更改都有一个书面的参数 文档/ verification.md要么这种行为没有改变 对于所有输入,或者对Java代码的文档化偏离。身份测试 在语料库中检查这些参数。
自己检查: bun test src/xz-java-identity_test.ts,或者
node fixtures/verify.ts (节点>= 22.18)在V8上运行它们。
相关
- XZ代表Java -移植这个库的实现
- LZMA SDK - Igor Pavlov的参考实现
- XZ跑龙套 -
xz和liblzma - LZMA-JS -原始的JavaScript实现
- lzma-purejs
- lzmajs
cjser
这个包是由cjser为仍然需要的项目生成的commonjs兼容的构建 require() 支持。源版本与原始的npm包版本匹配,对这个生成的构建带有cjser预发布后缀。
原始存储库: https://github.com/xseman/lzma1