@oh-my-pi/pi-utils 17.4.0 → 17.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +22 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/ar/arj.d.ts +5 -0
- package/dist/types/ar/asar.d.ts +7 -0
- package/dist/types/ar/bytes.d.ts +20 -0
- package/dist/types/ar/cab.d.ts +5 -0
- package/dist/types/ar/checksums.d.ts +10 -0
- package/dist/types/ar/codecs/bzip2.d.ts +4 -0
- package/dist/types/ar/codecs/gzip.d.ts +6 -0
- package/dist/types/ar/codecs/lzma.d.ts +6 -0
- package/dist/types/ar/codecs/lzw.d.ts +4 -0
- package/dist/types/ar/codecs/lzx.d.ts +7 -0
- package/dist/types/ar/codecs/xz.d.ts +4 -0
- package/dist/types/ar/codecs/zstd.d.ts +6 -0
- package/dist/types/ar/cpio.d.ts +7 -0
- package/dist/types/ar/deb.d.ts +5 -0
- package/dist/types/ar/entries.d.ts +22 -0
- package/dist/types/ar/error.d.ts +8 -0
- package/dist/types/ar/index.d.ts +11 -0
- package/dist/types/ar/iso.d.ts +5 -0
- package/dist/types/ar/limits.d.ts +33 -0
- package/dist/types/ar/lzh.d.ts +7 -0
- package/dist/types/ar/open.d.ts +45 -0
- package/dist/types/ar/paths.d.ts +18 -0
- package/dist/types/ar/rar/rar4-decoder.d.ts +6 -0
- package/dist/types/ar/rar/rar5-decoder.d.ts +6 -0
- package/dist/types/ar/rar.d.ts +5 -0
- package/dist/types/ar/reader.d.ts +28 -0
- package/dist/types/ar/registry.d.ts +17 -0
- package/dist/types/ar/rpm.d.ts +5 -0
- package/dist/types/ar/sevenzip/decode.d.ts +33 -0
- package/dist/types/ar/sevenzip.d.ts +5 -0
- package/dist/types/ar/source.d.ts +54 -0
- package/dist/types/ar/tar.d.ts +9 -0
- package/dist/types/ar/types.d.ts +99 -0
- package/dist/types/ar/unix-ar.d.ts +7 -0
- package/dist/types/ar/write.d.ts +10 -0
- package/dist/types/ar/zip.d.ts +10 -0
- package/dist/types/postmortem.d.ts +33 -0
- package/package.json +9 -3
- package/src/ar/arj.ts +314 -0
- package/src/ar/asar.ts +480 -0
- package/src/ar/bytes.ts +78 -0
- package/src/ar/cab.ts +377 -0
- package/src/ar/checksums.ts +62 -0
- package/src/ar/codecs/bzip2.ts +489 -0
- package/src/ar/codecs/gzip.ts +25 -0
- package/src/ar/codecs/lzma.ts +437 -0
- package/src/ar/codecs/lzw.ts +191 -0
- package/src/ar/codecs/lzx.ts +366 -0
- package/src/ar/codecs/xz.ts +522 -0
- package/src/ar/codecs/zstd.ts +25 -0
- package/src/ar/cpio.ts +389 -0
- package/src/ar/deb.ts +160 -0
- package/src/ar/entries.ts +97 -0
- package/src/ar/error.ts +11 -0
- package/src/ar/index.ts +16 -0
- package/src/ar/iso.ts +712 -0
- package/src/ar/limits.ts +80 -0
- package/src/ar/lzh.ts +659 -0
- package/src/ar/open.ts +225 -0
- package/src/ar/paths.ts +70 -0
- package/src/ar/rar/rar4-decoder.ts +459 -0
- package/src/ar/rar/rar5-decoder.ts +400 -0
- package/src/ar/rar.ts +735 -0
- package/src/ar/reader.ts +162 -0
- package/src/ar/registry.ts +208 -0
- package/src/ar/rpm.ts +320 -0
- package/src/ar/sevenzip/decode.ts +239 -0
- package/src/ar/sevenzip.ts +634 -0
- package/src/ar/source.ts +190 -0
- package/src/ar/tar.ts +771 -0
- package/src/ar/types.ts +131 -0
- package/src/ar/unix-ar.ts +312 -0
- package/src/ar/write.ts +55 -0
- package/src/ar/zip.ts +719 -0
- package/src/browsers.ts +2 -149
- package/src/docx/converter.ts +9 -9
- package/src/postmortem.ts +74 -0
- package/dist/types/docx/zip.d.ts +0 -6
- package/src/docx/zip.ts +0 -87
package/src/ar/lzh.ts
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { crc16Arc } from "./checksums";
|
|
2
|
+
import { ArchiveError } from "./error";
|
|
3
|
+
import { assertArchiveMemberSize, assertEntryCount, assertIndexSize, assertInMemorySize } from "./limits";
|
|
4
|
+
import { assertArchivePathBytes, assertArchivePathString, normalizeArchiveEntryPath } from "./paths";
|
|
5
|
+
import { type ByteSource, readAllBytes } from "./source";
|
|
6
|
+
import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
|
|
7
|
+
|
|
8
|
+
const LEGACY_DECODER = new TextDecoder("windows-1252");
|
|
9
|
+
// WHATWG maps the "utf-16" label to the UTF-16LE decoder used by LZH name extensions.
|
|
10
|
+
const UTF16LE_DECODER = new TextDecoder("utf-16");
|
|
11
|
+
const LHA_METHOD_PATTERN = /^-(?:lh[0-7d]|lz[45s])-$/;
|
|
12
|
+
|
|
13
|
+
class MsbBitReader {
|
|
14
|
+
readonly #bytes: Uint8Array;
|
|
15
|
+
readonly #label: string;
|
|
16
|
+
#position = 0;
|
|
17
|
+
|
|
18
|
+
constructor(bytes: Uint8Array, label: string) {
|
|
19
|
+
this.#bytes = bytes;
|
|
20
|
+
this.#label = label;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
read(count: number): number {
|
|
24
|
+
if (!Number.isInteger(count) || count < 0 || count > 24 || this.#position + count > this.#bytes.byteLength * 8) {
|
|
25
|
+
throw new ArchiveError(`Invalid ${this.#label} compressed data: truncated bitstream`);
|
|
26
|
+
}
|
|
27
|
+
let value = 0;
|
|
28
|
+
for (let index = 0; index < count; index++) {
|
|
29
|
+
const position = this.#position++;
|
|
30
|
+
value = value * 2 + ((this.#bytes[position >>> 3]! >>> (7 - (position & 7))) & 1);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
assertZeroPadding(): void {
|
|
36
|
+
while (this.#position < this.#bytes.byteLength * 8) {
|
|
37
|
+
if (this.read(1) !== 0) {
|
|
38
|
+
throw new ArchiveError(`Invalid ${this.#label} compressed data: non-zero trailing bits`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class CanonicalHuffman {
|
|
45
|
+
readonly #zero: number[] = [-1];
|
|
46
|
+
readonly #one: number[] = [-1];
|
|
47
|
+
readonly #symbol: number[] = [-1];
|
|
48
|
+
readonly #label: string;
|
|
49
|
+
|
|
50
|
+
private constructor(label: string) {
|
|
51
|
+
this.#label = label;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static single(symbol: number, symbolCount: number, label: string): CanonicalHuffman {
|
|
55
|
+
if (!Number.isInteger(symbol) || symbol < 0 || symbol >= symbolCount) {
|
|
56
|
+
throw new ArchiveError(`Invalid ${label} Huffman table: symbol is out of range`);
|
|
57
|
+
}
|
|
58
|
+
const tree = new CanonicalHuffman(label);
|
|
59
|
+
tree.#symbol[0] = symbol;
|
|
60
|
+
return tree;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static build(lengths: Uint8Array, symbolCount: number, label: string): CanonicalHuffman {
|
|
64
|
+
const counts = new Uint32Array(17);
|
|
65
|
+
let maximumLength = 0;
|
|
66
|
+
for (let symbol = 0; symbol < symbolCount; symbol++) {
|
|
67
|
+
const length = lengths[symbol]!;
|
|
68
|
+
if (length > 16) throw new ArchiveError(`Invalid ${label} Huffman table: code is too long`);
|
|
69
|
+
if (length !== 0) {
|
|
70
|
+
counts[length]++;
|
|
71
|
+
maximumLength = Math.max(maximumLength, length);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (maximumLength === 0) throw new ArchiveError(`Invalid ${label} Huffman table: no symbols`);
|
|
75
|
+
|
|
76
|
+
const nextCodes = new Uint32Array(17);
|
|
77
|
+
let code = 0;
|
|
78
|
+
for (let length = 1; length <= 16; length++) {
|
|
79
|
+
code = (code + counts[length - 1]!) * 2;
|
|
80
|
+
if (code + counts[length]! > 2 ** length) {
|
|
81
|
+
throw new ArchiveError(`Invalid ${label} Huffman table: oversubscribed codes`);
|
|
82
|
+
}
|
|
83
|
+
nextCodes[length] = code;
|
|
84
|
+
}
|
|
85
|
+
if (nextCodes[maximumLength]! + counts[maximumLength]! !== 2 ** maximumLength) {
|
|
86
|
+
throw new ArchiveError(`Invalid ${label} Huffman table: incomplete codes`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tree = new CanonicalHuffman(label);
|
|
90
|
+
for (let symbol = 0; symbol < symbolCount; symbol++) {
|
|
91
|
+
const length = lengths[symbol]!;
|
|
92
|
+
if (length === 0) continue;
|
|
93
|
+
const symbolCode = nextCodes[length]!;
|
|
94
|
+
nextCodes[length] = symbolCode + 1;
|
|
95
|
+
let node = 0;
|
|
96
|
+
for (let bitIndex = length - 1; bitIndex >= 0; bitIndex--) {
|
|
97
|
+
if (tree.#symbol[node]! >= 0) {
|
|
98
|
+
throw new ArchiveError(`Invalid ${label} Huffman table: prefix collision`);
|
|
99
|
+
}
|
|
100
|
+
const bit = (symbolCode >>> bitIndex) & 1;
|
|
101
|
+
let child = bit === 0 ? tree.#zero[node]! : tree.#one[node]!;
|
|
102
|
+
if (child < 0) {
|
|
103
|
+
child = tree.#symbol.length;
|
|
104
|
+
tree.#zero.push(-1);
|
|
105
|
+
tree.#one.push(-1);
|
|
106
|
+
tree.#symbol.push(-1);
|
|
107
|
+
if (bit === 0) tree.#zero[node] = child;
|
|
108
|
+
else tree.#one[node] = child;
|
|
109
|
+
}
|
|
110
|
+
node = child;
|
|
111
|
+
}
|
|
112
|
+
if (tree.#symbol[node]! >= 0 || tree.#zero[node]! >= 0 || tree.#one[node]! >= 0) {
|
|
113
|
+
throw new ArchiveError(`Invalid ${label} Huffman table: duplicate code`);
|
|
114
|
+
}
|
|
115
|
+
tree.#symbol[node] = symbol;
|
|
116
|
+
}
|
|
117
|
+
return tree;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
decode(reader: MsbBitReader): number {
|
|
121
|
+
let node = 0;
|
|
122
|
+
for (let depth = 0; depth <= 16; depth++) {
|
|
123
|
+
const symbol = this.#symbol[node]!;
|
|
124
|
+
if (symbol >= 0) return symbol;
|
|
125
|
+
node = reader.read(1) === 0 ? this.#zero[node]! : this.#one[node]!;
|
|
126
|
+
if (node < 0) throw new ArchiveError(`Invalid ${this.#label} Huffman code`);
|
|
127
|
+
}
|
|
128
|
+
throw new ArchiveError(`Invalid ${this.#label} Huffman code: excessive depth`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readCodeLength(reader: MsbBitReader, label: string): number {
|
|
133
|
+
let length = reader.read(3);
|
|
134
|
+
if (length === 7) {
|
|
135
|
+
while (reader.read(1) !== 0) {
|
|
136
|
+
length++;
|
|
137
|
+
if (length > 16) throw new ArchiveError(`Invalid ${label} Huffman table: code is too long`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readTemporaryTree(reader: MsbBitReader, label: string): CanonicalHuffman {
|
|
144
|
+
const symbolCount = 19;
|
|
145
|
+
const encodedCount = reader.read(5);
|
|
146
|
+
if (encodedCount === 0) return CanonicalHuffman.single(reader.read(5), symbolCount, label);
|
|
147
|
+
if (encodedCount > symbolCount) throw new ArchiveError(`Invalid ${label} temporary Huffman table size`);
|
|
148
|
+
const lengths = new Uint8Array(symbolCount);
|
|
149
|
+
let index = 0;
|
|
150
|
+
while (index < encodedCount) {
|
|
151
|
+
lengths[index++] = readCodeLength(reader, label);
|
|
152
|
+
if (index === 3) {
|
|
153
|
+
const skipped = reader.read(2);
|
|
154
|
+
if (index + skipped > encodedCount) throw new ArchiveError(`Invalid ${label} temporary Huffman table`);
|
|
155
|
+
index += skipped;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return CanonicalHuffman.build(lengths, symbolCount, label);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readCommandTree(reader: MsbBitReader, temporary: CanonicalHuffman, label: string): CanonicalHuffman {
|
|
162
|
+
const symbolCount = 510;
|
|
163
|
+
const encodedCount = reader.read(9);
|
|
164
|
+
if (encodedCount === 0) return CanonicalHuffman.single(reader.read(9), symbolCount, label);
|
|
165
|
+
if (encodedCount > symbolCount) throw new ArchiveError(`Invalid ${label} command Huffman table size`);
|
|
166
|
+
const lengths = new Uint8Array(symbolCount);
|
|
167
|
+
let index = 0;
|
|
168
|
+
while (index < encodedCount) {
|
|
169
|
+
const code = temporary.decode(reader);
|
|
170
|
+
if (code <= 2) {
|
|
171
|
+
const skipped = code === 0 ? 1 : code === 1 ? reader.read(4) + 3 : reader.read(9) + 20;
|
|
172
|
+
if (index + skipped > encodedCount) throw new ArchiveError(`Invalid ${label} command Huffman table`);
|
|
173
|
+
index += skipped;
|
|
174
|
+
} else {
|
|
175
|
+
lengths[index++] = code - 2;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return CanonicalHuffman.build(lengths, symbolCount, label);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readPositionTree(
|
|
182
|
+
reader: MsbBitReader,
|
|
183
|
+
positionBits: number,
|
|
184
|
+
symbolCount: number,
|
|
185
|
+
label: string,
|
|
186
|
+
): CanonicalHuffman {
|
|
187
|
+
const encodedCount = reader.read(positionBits);
|
|
188
|
+
if (encodedCount === 0) return CanonicalHuffman.single(reader.read(positionBits), symbolCount, label);
|
|
189
|
+
if (encodedCount > symbolCount) throw new ArchiveError(`Invalid ${label} position Huffman table size`);
|
|
190
|
+
const lengths = new Uint8Array(symbolCount);
|
|
191
|
+
for (let index = 0; index < encodedCount; index++) lengths[index] = readCodeLength(reader, label);
|
|
192
|
+
return CanonicalHuffman.build(lengths, symbolCount, label);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @internal Decode the static-Huffman LZSS stream shared by LZH and ARJ methods 1-3. */
|
|
196
|
+
export function decompressLhStatic(
|
|
197
|
+
packed: Uint8Array,
|
|
198
|
+
outSize: number,
|
|
199
|
+
dictionarySize: number,
|
|
200
|
+
positionBits: number,
|
|
201
|
+
positionSymbols: number,
|
|
202
|
+
label: string,
|
|
203
|
+
): Uint8Array {
|
|
204
|
+
const reader = new MsbBitReader(packed, label);
|
|
205
|
+
const output = new Uint8Array(outSize);
|
|
206
|
+
let outputPosition = 0;
|
|
207
|
+
let blockRemaining = 0;
|
|
208
|
+
let commands: CanonicalHuffman | undefined;
|
|
209
|
+
let positions: CanonicalHuffman | undefined;
|
|
210
|
+
while (outputPosition < outSize) {
|
|
211
|
+
if (blockRemaining === 0) {
|
|
212
|
+
blockRemaining = reader.read(16);
|
|
213
|
+
if (blockRemaining === 0) throw new ArchiveError(`Invalid ${label} compressed data: empty block`);
|
|
214
|
+
const temporary = readTemporaryTree(reader, label);
|
|
215
|
+
commands = readCommandTree(reader, temporary, label);
|
|
216
|
+
positions = readPositionTree(reader, positionBits, positionSymbols, label);
|
|
217
|
+
}
|
|
218
|
+
blockRemaining--;
|
|
219
|
+
const symbol = commands!.decode(reader);
|
|
220
|
+
if (symbol < 256) {
|
|
221
|
+
output[outputPosition++] = symbol;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const length = symbol - 256 + 3;
|
|
225
|
+
if (length > outSize - outputPosition) {
|
|
226
|
+
throw new ArchiveError(`Invalid ${label} compressed data: match exceeds declared size`);
|
|
227
|
+
}
|
|
228
|
+
const positionCode = positions!.decode(reader);
|
|
229
|
+
let distance = positionCode;
|
|
230
|
+
if (positionCode > 1) {
|
|
231
|
+
const lowBitCount = positionCode - 1;
|
|
232
|
+
distance = 2 ** lowBitCount + reader.read(lowBitCount);
|
|
233
|
+
}
|
|
234
|
+
if (distance >= dictionarySize || distance >= outputPosition) {
|
|
235
|
+
throw new ArchiveError(`Invalid ${label} compressed data: history distance is out of range`);
|
|
236
|
+
}
|
|
237
|
+
let sourcePosition = outputPosition - distance - 1;
|
|
238
|
+
for (let index = 0; index < length; index++) output[outputPosition++] = output[sourcePosition++]!;
|
|
239
|
+
}
|
|
240
|
+
if (blockRemaining !== 0) throw new ArchiveError(`Invalid ${label} compressed data: block exceeds declared size`);
|
|
241
|
+
reader.assertZeroPadding();
|
|
242
|
+
return output;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function decompressLzs(packed: Uint8Array, outSize: number): Uint8Array {
|
|
246
|
+
const reader = new MsbBitReader(packed, "LZH -lzs-");
|
|
247
|
+
const output = new Uint8Array(outSize);
|
|
248
|
+
const history = new Uint8Array(2048);
|
|
249
|
+
history.fill(0x20);
|
|
250
|
+
let historyPosition = 2048 - 17;
|
|
251
|
+
let outputPosition = 0;
|
|
252
|
+
const emit = (value: number): void => {
|
|
253
|
+
if (outputPosition >= outSize) throw new ArchiveError("Invalid LZH -lzs- data: output exceeds declared size");
|
|
254
|
+
output[outputPosition++] = value;
|
|
255
|
+
history[historyPosition] = value;
|
|
256
|
+
historyPosition = (historyPosition + 1) & 2047;
|
|
257
|
+
};
|
|
258
|
+
while (outputPosition < outSize) {
|
|
259
|
+
if (reader.read(1) !== 0) {
|
|
260
|
+
emit(reader.read(8));
|
|
261
|
+
} else {
|
|
262
|
+
const position = reader.read(11);
|
|
263
|
+
const length = reader.read(4) + 2;
|
|
264
|
+
if (length > outSize - outputPosition)
|
|
265
|
+
throw new ArchiveError("Invalid LZH -lzs- data: match exceeds declared size");
|
|
266
|
+
for (let index = 0; index < length; index++) emit(history[(position + index) & 2047]!);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
reader.assertZeroPadding();
|
|
270
|
+
return output;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const ZERO_CRC16_BYTES = new Uint8Array(2);
|
|
274
|
+
|
|
275
|
+
function crc16WithZeroRange(bytes: Uint8Array, zeroStart: number, zeroEnd: number): number {
|
|
276
|
+
if (zeroStart < 0) return crc16Arc(bytes);
|
|
277
|
+
let value = crc16Arc(bytes.subarray(0, zeroStart));
|
|
278
|
+
value = crc16Arc(ZERO_CRC16_BYTES.subarray(0, zeroEnd - zeroStart), value);
|
|
279
|
+
return crc16Arc(bytes.subarray(zeroEnd), value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function u16(bytes: Uint8Array, offset: number): number {
|
|
283
|
+
return bytes[offset]! | (bytes[offset + 1]! << 8);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function u32(bytes: Uint8Array, offset: number): number {
|
|
287
|
+
return (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function u64(bytes: Uint8Array, offset: number): number {
|
|
291
|
+
const value = u32(bytes, offset) + u32(bytes, offset + 4) * 0x100000000;
|
|
292
|
+
if (!Number.isSafeInteger(value)) throw new ArchiveError("LZH uses sizes too large to read safely");
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function assertRange(bytes: Uint8Array, start: number, end: number, what: string): void {
|
|
297
|
+
if (
|
|
298
|
+
!Number.isSafeInteger(start) ||
|
|
299
|
+
!Number.isSafeInteger(end) ||
|
|
300
|
+
start < 0 ||
|
|
301
|
+
end < start ||
|
|
302
|
+
end > bytes.byteLength
|
|
303
|
+
) {
|
|
304
|
+
throw new ArchiveError(`Invalid LZH archive: truncated ${what}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function decodeLegacy(bytes: Uint8Array): string {
|
|
309
|
+
let end = bytes.indexOf(0);
|
|
310
|
+
if (end < 0) end = bytes.byteLength;
|
|
311
|
+
return LEGACY_DECODER.decode(bytes.subarray(0, end));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function decodeUtf16(bytes: Uint8Array): string {
|
|
315
|
+
if ((bytes.byteLength & 1) !== 0) throw new ArchiveError("Invalid LZH Unicode path header: odd UTF-16 length");
|
|
316
|
+
let end = bytes.byteLength;
|
|
317
|
+
while (end >= 2 && bytes[end - 1] === 0 && bytes[end - 2] === 0) end -= 2;
|
|
318
|
+
return UTF16LE_DECODER.decode(bytes.subarray(0, end));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function dosTimeToMs(value: number): number | undefined {
|
|
322
|
+
if (value === 0) return undefined;
|
|
323
|
+
const year = 1980 + ((value >>> 25) & 0x7f);
|
|
324
|
+
const month = (value >>> 21) & 0x0f;
|
|
325
|
+
const day = (value >>> 16) & 0x1f;
|
|
326
|
+
const hour = (value >>> 11) & 0x1f;
|
|
327
|
+
const minute = (value >>> 5) & 0x3f;
|
|
328
|
+
const second = (value & 0x1f) * 2;
|
|
329
|
+
if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return undefined;
|
|
330
|
+
return Date.UTC(year, month - 1, day, hour, minute, second);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface LzhExtendedFields {
|
|
334
|
+
filename?: string;
|
|
335
|
+
directory?: string;
|
|
336
|
+
unicodeFilename?: string;
|
|
337
|
+
unicodeDirectory?: string;
|
|
338
|
+
mtimeMs?: number;
|
|
339
|
+
mode?: number;
|
|
340
|
+
packedSize?: number;
|
|
341
|
+
size?: number;
|
|
342
|
+
commonCrc?: number;
|
|
343
|
+
commonCrcOffset?: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function processExtendedHeader(
|
|
347
|
+
type: number,
|
|
348
|
+
data: Uint8Array,
|
|
349
|
+
absoluteDataOffset: number,
|
|
350
|
+
fields: LzhExtendedFields,
|
|
351
|
+
): void {
|
|
352
|
+
switch (type) {
|
|
353
|
+
case 0x00:
|
|
354
|
+
if (data.byteLength < 2) throw new ArchiveError("Invalid LZH common extended header");
|
|
355
|
+
fields.commonCrc = u16(data, 0);
|
|
356
|
+
fields.commonCrcOffset = absoluteDataOffset;
|
|
357
|
+
break;
|
|
358
|
+
case 0x01:
|
|
359
|
+
fields.filename = decodeLegacy(data);
|
|
360
|
+
break;
|
|
361
|
+
case 0x02:
|
|
362
|
+
fields.directory = decodeLegacy(data).replaceAll("ÿ", "/");
|
|
363
|
+
break;
|
|
364
|
+
case 0x39:
|
|
365
|
+
throw new ArchiveError("Multi-volume LZH archives are unsupported");
|
|
366
|
+
case 0x41:
|
|
367
|
+
if (data.byteLength >= 16) {
|
|
368
|
+
const low = u32(data, 8);
|
|
369
|
+
const high = u32(data, 12);
|
|
370
|
+
const filetime = low + high * 0x100000000;
|
|
371
|
+
if (Number.isSafeInteger(filetime)) fields.mtimeMs = filetime / 10_000 - 11_644_473_600_000;
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
case 0x42:
|
|
375
|
+
if (data.byteLength < 16) throw new ArchiveError("Invalid LZH 64-bit size extended header");
|
|
376
|
+
fields.packedSize = u64(data, 0);
|
|
377
|
+
fields.size = u64(data, 8);
|
|
378
|
+
break;
|
|
379
|
+
case 0x44:
|
|
380
|
+
fields.unicodeFilename = decodeUtf16(data);
|
|
381
|
+
break;
|
|
382
|
+
case 0x45:
|
|
383
|
+
fields.unicodeDirectory = decodeUtf16(data).replaceAll("ÿ", "/");
|
|
384
|
+
break;
|
|
385
|
+
case 0x50:
|
|
386
|
+
if (data.byteLength < 2) throw new ArchiveError("Invalid LZH Unix permissions extended header");
|
|
387
|
+
fields.mode = u16(data, 0);
|
|
388
|
+
break;
|
|
389
|
+
case 0x54:
|
|
390
|
+
if (data.byteLength < 4) throw new ArchiveError("Invalid LZH Unix timestamp extended header");
|
|
391
|
+
fields.mtimeMs = u32(data, 0) * 1000;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
class LzhMemberSource implements MemberSource {
|
|
397
|
+
readonly #archive: Uint8Array;
|
|
398
|
+
readonly #start: number;
|
|
399
|
+
readonly #packedSize: number;
|
|
400
|
+
readonly #method: string;
|
|
401
|
+
readonly #crc: number;
|
|
402
|
+
|
|
403
|
+
constructor(archive: Uint8Array, start: number, packedSize: number, method: string, crc: number) {
|
|
404
|
+
this.#archive = archive;
|
|
405
|
+
this.#start = start;
|
|
406
|
+
this.#packedSize = packedSize;
|
|
407
|
+
this.#method = method;
|
|
408
|
+
this.#crc = crc;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async read(size: number, memberPath: string): Promise<Uint8Array> {
|
|
412
|
+
const packed = this.#archive.subarray(this.#start, this.#start + this.#packedSize);
|
|
413
|
+
let output: Uint8Array;
|
|
414
|
+
switch (this.#method) {
|
|
415
|
+
case "-lh0-":
|
|
416
|
+
case "-lz4-":
|
|
417
|
+
if (packed.byteLength !== size)
|
|
418
|
+
throw new ArchiveError(`LZH member '${memberPath}' has inconsistent stored size`);
|
|
419
|
+
output = packed.slice();
|
|
420
|
+
break;
|
|
421
|
+
case "-lh4-":
|
|
422
|
+
output = decompressLhStatic(packed, size, 1 << 12, 4, 13, "LZH -lh4-");
|
|
423
|
+
break;
|
|
424
|
+
case "-lh5-":
|
|
425
|
+
output = decompressLhStatic(packed, size, 1 << 13, 4, 14, "LZH -lh5-");
|
|
426
|
+
break;
|
|
427
|
+
case "-lh6-":
|
|
428
|
+
output = decompressLhStatic(packed, size, 1 << 15, 5, 16, "LZH -lh6-");
|
|
429
|
+
break;
|
|
430
|
+
case "-lh7-":
|
|
431
|
+
output = decompressLhStatic(packed, size, 1 << 16, 5, 17, "LZH -lh7-");
|
|
432
|
+
break;
|
|
433
|
+
case "-lzs-":
|
|
434
|
+
output = decompressLzs(packed, size);
|
|
435
|
+
break;
|
|
436
|
+
case "-lh1-":
|
|
437
|
+
throw new ArchiveError(`LZH member '${memberPath}' uses unsupported dynamic-Huffman method -lh1-`);
|
|
438
|
+
default:
|
|
439
|
+
throw new ArchiveError(`LZH member '${memberPath}' uses unsupported compression method ${this.#method}`);
|
|
440
|
+
}
|
|
441
|
+
if (output.byteLength !== size)
|
|
442
|
+
throw new ArchiveError(`LZH member '${memberPath}' extracted to an unexpected size`);
|
|
443
|
+
if (crc16Arc(output) !== this.#crc)
|
|
444
|
+
throw new ArchiveError(`LZH member '${memberPath}' failed CRC-16 verification`);
|
|
445
|
+
return output;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
interface ParsedLzhHeader {
|
|
450
|
+
method: string;
|
|
451
|
+
packedSize: number;
|
|
452
|
+
size: number;
|
|
453
|
+
dataStart: number;
|
|
454
|
+
nextOffset: number;
|
|
455
|
+
crc: number;
|
|
456
|
+
path?: string;
|
|
457
|
+
mtimeMs?: number;
|
|
458
|
+
mode?: number;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function parseLzhHeader(bytes: Uint8Array, offset: number, options: FormatReadOptions): ParsedLzhHeader {
|
|
462
|
+
assertRange(bytes, offset, offset + 22, "header");
|
|
463
|
+
const level = bytes[offset + 20]!;
|
|
464
|
+
if (level > 2) throw new ArchiveError(`Unsupported LZH header level ${level}`);
|
|
465
|
+
const method = String.fromCharCode(...bytes.subarray(offset + 2, offset + 7));
|
|
466
|
+
if (!LHA_METHOD_PATTERN.test(method)) throw new ArchiveError(`Invalid LZH compression method '${method}'`);
|
|
467
|
+
let packedSize = u32(bytes, offset + 7);
|
|
468
|
+
let size = u32(bytes, offset + 11);
|
|
469
|
+
let crc = 0;
|
|
470
|
+
let legacyFilename: string | undefined;
|
|
471
|
+
let mtimeMs: number | undefined;
|
|
472
|
+
let osId = 0;
|
|
473
|
+
let dataStart: number;
|
|
474
|
+
const fields: LzhExtendedFields = {};
|
|
475
|
+
|
|
476
|
+
if (level < 2) {
|
|
477
|
+
const headerLength = bytes[offset]!;
|
|
478
|
+
const minimum = level === 0 ? 22 : 25;
|
|
479
|
+
if (headerLength < minimum) throw new ArchiveError(`Invalid LZH level-${level} header size`);
|
|
480
|
+
const baseEnd = offset + headerLength + 2;
|
|
481
|
+
assertRange(bytes, offset, baseEnd, "header");
|
|
482
|
+
let sum = 0;
|
|
483
|
+
for (let index = offset + 2; index < baseEnd; index++) sum = (sum + bytes[index]!) & 0xff;
|
|
484
|
+
if (sum !== bytes[offset + 1]) throw new ArchiveError("Invalid LZH header checksum");
|
|
485
|
+
const nameLength = bytes[offset + 21]!;
|
|
486
|
+
if (22 + nameLength + 2 > headerLength + 2) throw new ArchiveError("Invalid LZH filename length");
|
|
487
|
+
assertArchivePathBytes(nameLength, "member path", options.limits.maxPathBytes);
|
|
488
|
+
legacyFilename = decodeLegacy(bytes.subarray(offset + 22, offset + 22 + nameLength));
|
|
489
|
+
crc = u16(bytes, offset + 22 + nameLength);
|
|
490
|
+
mtimeMs = dosTimeToMs(u32(bytes, offset + 15));
|
|
491
|
+
if (level === 0) {
|
|
492
|
+
const extendedStart = offset + 24 + nameLength;
|
|
493
|
+
if (baseEnd - extendedStart >= 12) {
|
|
494
|
+
const extended = bytes.subarray(extendedStart, baseEnd);
|
|
495
|
+
if ((extended[0] === 0x55 || extended[0] === 0x4b) && extended[1] === 0) {
|
|
496
|
+
osId = extended[0]!;
|
|
497
|
+
fields.mtimeMs = u32(extended, 2) * 1000;
|
|
498
|
+
fields.mode = u16(extended, extended.byteLength - 6);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
dataStart = baseEnd;
|
|
502
|
+
} else {
|
|
503
|
+
osId = bytes[offset + 24 + nameLength]!;
|
|
504
|
+
let extensionSize = u16(bytes, baseEnd - 2);
|
|
505
|
+
let cursor = baseEnd;
|
|
506
|
+
let totalExtensionSize = 0;
|
|
507
|
+
let extensionCount = 0;
|
|
508
|
+
while (extensionSize !== 0) {
|
|
509
|
+
if (extensionSize < 3) throw new ArchiveError("Invalid LZH extended header size");
|
|
510
|
+
assertRange(bytes, cursor, cursor + extensionSize, "extended header");
|
|
511
|
+
totalExtensionSize += extensionSize;
|
|
512
|
+
assertIndexSize(headerLength + 2 + totalExtensionSize, options.limits, "header metadata");
|
|
513
|
+
if (++extensionCount > 65_535) throw new ArchiveError("Invalid LZH archive: too many extended headers");
|
|
514
|
+
const type = bytes[cursor]!;
|
|
515
|
+
const dataEnd = cursor + extensionSize - 2;
|
|
516
|
+
const data = bytes.subarray(cursor + 1, dataEnd);
|
|
517
|
+
if (type === 0x01 || type === 0x02 || type === 0x44 || type === 0x45) {
|
|
518
|
+
assertArchivePathBytes(data.byteLength, "member path", options.limits.maxPathBytes);
|
|
519
|
+
}
|
|
520
|
+
processExtendedHeader(type, data, cursor + 1, fields);
|
|
521
|
+
const currentSize = extensionSize;
|
|
522
|
+
extensionSize = u16(bytes, dataEnd);
|
|
523
|
+
cursor += currentSize;
|
|
524
|
+
}
|
|
525
|
+
dataStart = baseEnd + totalExtensionSize;
|
|
526
|
+
packedSize = fields.packedSize ?? packedSize - totalExtensionSize;
|
|
527
|
+
if (packedSize < 0) throw new ArchiveError("Invalid LZH level-1 packed size");
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
const headerLength = u16(bytes, offset);
|
|
531
|
+
if (headerLength < 26) throw new ArchiveError("Invalid LZH level-2 header size");
|
|
532
|
+
const headerEnd = offset + headerLength;
|
|
533
|
+
assertRange(bytes, offset, headerEnd, "header");
|
|
534
|
+
crc = u16(bytes, offset + 21);
|
|
535
|
+
osId = bytes[offset + 23]!;
|
|
536
|
+
mtimeMs = u32(bytes, offset + 15) * 1000;
|
|
537
|
+
let cursor = offset + 24;
|
|
538
|
+
let extensionCount = 0;
|
|
539
|
+
while (cursor + 2 <= headerEnd) {
|
|
540
|
+
const extensionSize = u16(bytes, cursor);
|
|
541
|
+
if (extensionSize === 0) {
|
|
542
|
+
cursor += 2;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
if (extensionSize < 3 || cursor + extensionSize > headerEnd)
|
|
546
|
+
throw new ArchiveError("Invalid LZH extended header size");
|
|
547
|
+
if (++extensionCount > 65_535) throw new ArchiveError("Invalid LZH archive: too many extended headers");
|
|
548
|
+
const type = bytes[cursor + 2]!;
|
|
549
|
+
const data = bytes.subarray(cursor + 3, cursor + extensionSize);
|
|
550
|
+
if (type === 0x01 || type === 0x02 || type === 0x44 || type === 0x45) {
|
|
551
|
+
assertArchivePathBytes(data.byteLength, "member path", options.limits.maxPathBytes);
|
|
552
|
+
}
|
|
553
|
+
processExtendedHeader(type, data, cursor + 3, fields);
|
|
554
|
+
cursor += extensionSize;
|
|
555
|
+
}
|
|
556
|
+
if (cursor !== headerEnd) throw new ArchiveError("Invalid LZH level-2 extended header chain");
|
|
557
|
+
dataStart = headerEnd;
|
|
558
|
+
packedSize = fields.packedSize ?? packedSize;
|
|
559
|
+
}
|
|
560
|
+
if (level === 1 && osId === 0x20 && method === "-lh7-") {
|
|
561
|
+
throw new ArchiveError("LZH uses the incompatible LHARK -lh7- variant");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
size = fields.size ?? size;
|
|
565
|
+
mtimeMs = fields.mtimeMs ?? mtimeMs;
|
|
566
|
+
const mode = fields.mode;
|
|
567
|
+
if (fields.commonCrc !== undefined) {
|
|
568
|
+
const relative = fields.commonCrcOffset! - offset;
|
|
569
|
+
if (crc16WithZeroRange(bytes.subarray(offset, dataStart), relative, relative + 2) !== fields.commonCrc) {
|
|
570
|
+
throw new ArchiveError("Invalid LZH common header CRC-16");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const filename = fields.unicodeFilename ?? fields.filename ?? legacyFilename ?? "";
|
|
574
|
+
const directory = fields.unicodeDirectory ?? fields.directory ?? "";
|
|
575
|
+
const rawPath = `${directory}${directory && !/[\\/]$/.test(directory) ? "/" : ""}${filename}`;
|
|
576
|
+
assertArchivePathString(rawPath, "member path", options.limits.maxPathBytes);
|
|
577
|
+
assertArchiveMemberSize(size, rawPath || "<unnamed>", options.limits);
|
|
578
|
+
assertRange(bytes, dataStart, dataStart + packedSize, "member data");
|
|
579
|
+
return {
|
|
580
|
+
method,
|
|
581
|
+
packedSize,
|
|
582
|
+
size,
|
|
583
|
+
dataStart,
|
|
584
|
+
nextOffset: dataStart + packedSize,
|
|
585
|
+
crc,
|
|
586
|
+
path: normalizeArchiveEntryPath(rawPath),
|
|
587
|
+
mtimeMs,
|
|
588
|
+
mode,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Probe whether bytes begin with an LZH/LHA member header. */
|
|
593
|
+
export function sniffLzh(bytes: Uint8Array): boolean {
|
|
594
|
+
if (bytes.byteLength < 22 || bytes[2] !== 0x2d || bytes[6] !== 0x2d || bytes[3] !== 0x6c) return false;
|
|
595
|
+
const method = String.fromCharCode(...bytes.subarray(2, 7));
|
|
596
|
+
return LHA_METHOD_PATTERN.test(method) && bytes[20]! <= 2;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Index an LZH/LHA archive and lazily decode its members from the bounded archive buffer. */
|
|
600
|
+
export const readLzh: FormatReader = async (
|
|
601
|
+
source: ByteSource,
|
|
602
|
+
options: FormatReadOptions,
|
|
603
|
+
): Promise<ArchiveIndexEntry[]> => {
|
|
604
|
+
assertInMemorySize(source.size, options.limits);
|
|
605
|
+
let bytes: Uint8Array;
|
|
606
|
+
try {
|
|
607
|
+
bytes = await readAllBytes(source);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (error instanceof ArchiveError) throw error;
|
|
610
|
+
throw new ArchiveError(`Unable to read LZH archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
611
|
+
}
|
|
612
|
+
if (bytes.byteLength !== source.size) throw new ArchiveError("Invalid LZH archive: truncated data");
|
|
613
|
+
if (!sniffLzh(bytes)) throw new ArchiveError("Invalid LZH archive header");
|
|
614
|
+
const entries: ArchiveIndexEntry[] = [];
|
|
615
|
+
let offset = 0;
|
|
616
|
+
let parsedCount = 0;
|
|
617
|
+
let metadataSize = 0;
|
|
618
|
+
while (offset < bytes.byteLength && bytes[offset] !== 0) {
|
|
619
|
+
const header = parseLzhHeader(bytes, offset, options);
|
|
620
|
+
metadataSize += header.dataStart - offset;
|
|
621
|
+
assertIndexSize(metadataSize, options.limits, "index");
|
|
622
|
+
assertEntryCount(++parsedCount, options.limits);
|
|
623
|
+
if (header.nextOffset <= offset) throw new ArchiveError("Invalid LZH archive: header did not advance");
|
|
624
|
+
offset = header.nextOffset;
|
|
625
|
+
if (!header.path) continue;
|
|
626
|
+
const isDirectory = header.method === "-lhd-";
|
|
627
|
+
if (isDirectory && header.mode !== undefined && (header.mode & 0xf000) === 0xa000) {
|
|
628
|
+
const separator = header.path.indexOf("|");
|
|
629
|
+
if (separator < 1) throw new ArchiveError(`Invalid LZH symbolic link '${header.path}'`);
|
|
630
|
+
const path = normalizeArchiveEntryPath(header.path.slice(0, separator));
|
|
631
|
+
const targetPath = normalizeArchiveEntryPath(header.path.slice(separator + 1));
|
|
632
|
+
if (!path || !targetPath) continue;
|
|
633
|
+
entries.push({
|
|
634
|
+
path,
|
|
635
|
+
isDirectory: false,
|
|
636
|
+
size: 0,
|
|
637
|
+
mtimeMs: header.mtimeMs,
|
|
638
|
+
mode: header.mode,
|
|
639
|
+
storage: { type: "link", targetPath, resolveTarget: false },
|
|
640
|
+
});
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
entries.push({
|
|
644
|
+
path: header.path,
|
|
645
|
+
isDirectory,
|
|
646
|
+
size: isDirectory ? 0 : header.size,
|
|
647
|
+
mtimeMs: header.mtimeMs,
|
|
648
|
+
mode: header.mode,
|
|
649
|
+
storage: isDirectory
|
|
650
|
+
? undefined
|
|
651
|
+
: {
|
|
652
|
+
type: "member",
|
|
653
|
+
source: new LzhMemberSource(bytes, header.dataStart, header.packedSize, header.method, header.crc),
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (entries.length === 0 && parsedCount === 0) throw new ArchiveError("Invalid LZH archive: no members");
|
|
658
|
+
return entries;
|
|
659
|
+
};
|