@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +22 -0
  3. package/THIRD-PARTY-NOTICES.txt +22909 -0
  4. package/dist/types/ar/arj.d.ts +5 -0
  5. package/dist/types/ar/asar.d.ts +7 -0
  6. package/dist/types/ar/bytes.d.ts +20 -0
  7. package/dist/types/ar/cab.d.ts +5 -0
  8. package/dist/types/ar/checksums.d.ts +10 -0
  9. package/dist/types/ar/codecs/bzip2.d.ts +4 -0
  10. package/dist/types/ar/codecs/gzip.d.ts +6 -0
  11. package/dist/types/ar/codecs/lzma.d.ts +6 -0
  12. package/dist/types/ar/codecs/lzw.d.ts +4 -0
  13. package/dist/types/ar/codecs/lzx.d.ts +7 -0
  14. package/dist/types/ar/codecs/xz.d.ts +4 -0
  15. package/dist/types/ar/codecs/zstd.d.ts +6 -0
  16. package/dist/types/ar/cpio.d.ts +7 -0
  17. package/dist/types/ar/deb.d.ts +5 -0
  18. package/dist/types/ar/entries.d.ts +22 -0
  19. package/dist/types/ar/error.d.ts +8 -0
  20. package/dist/types/ar/index.d.ts +11 -0
  21. package/dist/types/ar/iso.d.ts +5 -0
  22. package/dist/types/ar/limits.d.ts +33 -0
  23. package/dist/types/ar/lzh.d.ts +7 -0
  24. package/dist/types/ar/open.d.ts +45 -0
  25. package/dist/types/ar/paths.d.ts +18 -0
  26. package/dist/types/ar/rar/rar4-decoder.d.ts +6 -0
  27. package/dist/types/ar/rar/rar5-decoder.d.ts +6 -0
  28. package/dist/types/ar/rar.d.ts +5 -0
  29. package/dist/types/ar/reader.d.ts +28 -0
  30. package/dist/types/ar/registry.d.ts +17 -0
  31. package/dist/types/ar/rpm.d.ts +5 -0
  32. package/dist/types/ar/sevenzip/decode.d.ts +33 -0
  33. package/dist/types/ar/sevenzip.d.ts +5 -0
  34. package/dist/types/ar/source.d.ts +54 -0
  35. package/dist/types/ar/tar.d.ts +9 -0
  36. package/dist/types/ar/types.d.ts +99 -0
  37. package/dist/types/ar/unix-ar.d.ts +7 -0
  38. package/dist/types/ar/write.d.ts +10 -0
  39. package/dist/types/ar/zip.d.ts +10 -0
  40. package/dist/types/postmortem.d.ts +33 -0
  41. package/package.json +9 -3
  42. package/src/ar/arj.ts +314 -0
  43. package/src/ar/asar.ts +480 -0
  44. package/src/ar/bytes.ts +78 -0
  45. package/src/ar/cab.ts +377 -0
  46. package/src/ar/checksums.ts +62 -0
  47. package/src/ar/codecs/bzip2.ts +489 -0
  48. package/src/ar/codecs/gzip.ts +25 -0
  49. package/src/ar/codecs/lzma.ts +437 -0
  50. package/src/ar/codecs/lzw.ts +191 -0
  51. package/src/ar/codecs/lzx.ts +366 -0
  52. package/src/ar/codecs/xz.ts +522 -0
  53. package/src/ar/codecs/zstd.ts +25 -0
  54. package/src/ar/cpio.ts +389 -0
  55. package/src/ar/deb.ts +160 -0
  56. package/src/ar/entries.ts +97 -0
  57. package/src/ar/error.ts +11 -0
  58. package/src/ar/index.ts +16 -0
  59. package/src/ar/iso.ts +712 -0
  60. package/src/ar/limits.ts +80 -0
  61. package/src/ar/lzh.ts +659 -0
  62. package/src/ar/open.ts +225 -0
  63. package/src/ar/paths.ts +70 -0
  64. package/src/ar/rar/rar4-decoder.ts +459 -0
  65. package/src/ar/rar/rar5-decoder.ts +400 -0
  66. package/src/ar/rar.ts +735 -0
  67. package/src/ar/reader.ts +162 -0
  68. package/src/ar/registry.ts +208 -0
  69. package/src/ar/rpm.ts +320 -0
  70. package/src/ar/sevenzip/decode.ts +239 -0
  71. package/src/ar/sevenzip.ts +634 -0
  72. package/src/ar/source.ts +190 -0
  73. package/src/ar/tar.ts +771 -0
  74. package/src/ar/types.ts +131 -0
  75. package/src/ar/unix-ar.ts +312 -0
  76. package/src/ar/write.ts +55 -0
  77. package/src/ar/zip.ts +719 -0
  78. package/src/browsers.ts +2 -149
  79. package/src/docx/converter.ts +9 -9
  80. package/src/postmortem.ts +74 -0
  81. package/dist/types/docx/zip.d.ts +0 -6
  82. package/src/docx/zip.ts +0 -87
@@ -0,0 +1,437 @@
1
+ import { ArchiveError } from "../error";
2
+
3
+ const TOP_VALUE = 1 << 24;
4
+ const BIT_MODEL_TOTAL = 1 << 11;
5
+ const MOVE_BITS = 5;
6
+ const LITERAL_SIZE = 0x300;
7
+ const NUM_STATES = 12;
8
+ const NUM_POS_STATES_MAX = 16;
9
+ const MATCH_MIN_LEN = 2;
10
+
11
+ class RangeDecoder {
12
+ readonly bytes: Uint8Array;
13
+ pos = 0;
14
+ range = 0xffffffff;
15
+ code = 0;
16
+
17
+ constructor(bytes: Uint8Array) {
18
+ this.bytes = bytes;
19
+ if (bytes.byteLength < 5 || bytes[0] !== 0) throw new ArchiveError("Invalid LZMA range-coded stream");
20
+ for (let index = 0; index < 5; index++) this.code = ((this.code << 8) | bytes[this.pos++]!) >>> 0;
21
+ }
22
+
23
+ #normalize(): void {
24
+ if (this.range < TOP_VALUE) {
25
+ if (this.pos >= this.bytes.byteLength) throw new ArchiveError("Invalid LZMA stream: truncated range data");
26
+ this.range = (this.range << 8) >>> 0;
27
+ this.code = ((this.code << 8) | this.bytes[this.pos++]!) >>> 0;
28
+ }
29
+ }
30
+
31
+ decodeBit(probs: Uint16Array, index: number): number {
32
+ this.#normalize();
33
+ const probability = probs[index]!;
34
+ const bound = Math.imul(this.range >>> 11, probability) >>> 0;
35
+ if (this.code < bound) {
36
+ this.range = bound;
37
+ probs[index] = probability + ((BIT_MODEL_TOTAL - probability) >>> MOVE_BITS);
38
+ return 0;
39
+ }
40
+ this.range = (this.range - bound) >>> 0;
41
+ this.code = (this.code - bound) >>> 0;
42
+ probs[index] = probability - (probability >>> MOVE_BITS);
43
+ return 1;
44
+ }
45
+
46
+ decodeDirectBits(count: number): number {
47
+ let result = 0;
48
+ for (let index = 0; index < count; index++) {
49
+ this.#normalize();
50
+ this.range >>>= 1;
51
+ let bit = 0;
52
+ if (this.code >= this.range) {
53
+ this.code = (this.code - this.range) >>> 0;
54
+ bit = 1;
55
+ }
56
+ result = ((result << 1) | bit) >>> 0;
57
+ }
58
+ return result;
59
+ }
60
+ }
61
+
62
+ function initializedProbabilities(size: number): Uint16Array {
63
+ const probabilities = new Uint16Array(size);
64
+ probabilities.fill(BIT_MODEL_TOTAL >>> 1);
65
+ return probabilities;
66
+ }
67
+
68
+ class LengthDecoder {
69
+ readonly choice = initializedProbabilities(2);
70
+ readonly low = initializedProbabilities(NUM_POS_STATES_MAX << 3);
71
+ readonly mid = initializedProbabilities(NUM_POS_STATES_MAX << 3);
72
+ readonly high = initializedProbabilities(1 << 8);
73
+
74
+ reset(): void {
75
+ this.choice.fill(BIT_MODEL_TOTAL >>> 1);
76
+ this.low.fill(BIT_MODEL_TOTAL >>> 1);
77
+ this.mid.fill(BIT_MODEL_TOTAL >>> 1);
78
+ this.high.fill(BIT_MODEL_TOTAL >>> 1);
79
+ }
80
+
81
+ decode(range: RangeDecoder, posState: number): number {
82
+ if (range.decodeBit(this.choice, 0) === 0) return decodeTree(range, this.low, posState << 3, 3);
83
+ if (range.decodeBit(this.choice, 1) === 0) return 8 + decodeTree(range, this.mid, posState << 3, 3);
84
+ return 16 + decodeTree(range, this.high, 0, 8);
85
+ }
86
+ }
87
+
88
+ function decodeTree(range: RangeDecoder, probs: Uint16Array, offset: number, bits: number): number {
89
+ let symbol = 1;
90
+ for (let index = 0; index < bits; index++) symbol = (symbol << 1) | range.decodeBit(probs, offset + symbol);
91
+ return symbol - (1 << bits);
92
+ }
93
+
94
+ function decodeReverseTree(range: RangeDecoder, probs: Uint16Array, offset: number, bits: number): number {
95
+ let symbol = 1;
96
+ let result = 0;
97
+ for (let index = 0; index < bits; index++) {
98
+ const bit = range.decodeBit(probs, offset + symbol);
99
+ symbol = (symbol << 1) | bit;
100
+ result |= bit << index;
101
+ }
102
+ return result >>> 0;
103
+ }
104
+
105
+ interface LzmaProperties {
106
+ lc: number;
107
+ lp: number;
108
+ pb: number;
109
+ dictionarySize: number;
110
+ }
111
+
112
+ function parseLzmaProperties(props: Uint8Array): LzmaProperties {
113
+ if (props.byteLength !== 5) throw new ArchiveError("Invalid LZMA properties: expected 5 bytes");
114
+ let packed = props[0]!;
115
+ if (packed >= 9 * 5 * 5) throw new ArchiveError("Invalid LZMA properties");
116
+ const lc = packed % 9;
117
+ packed = Math.floor(packed / 9);
118
+ const lp = packed % 5;
119
+ const pb = Math.floor(packed / 5);
120
+ const dictionarySize = (props[1]! | (props[2]! << 8) | (props[3]! << 16) | (props[4]! << 24)) >>> 0;
121
+ return { lc, lp, pb, dictionarySize: Math.max(dictionarySize, 4096) };
122
+ }
123
+
124
+ class LzmaDecoder {
125
+ readonly #output: Uint8Array;
126
+ readonly #dictionarySize: number;
127
+ #outputPos = 0;
128
+ #dictionaryStart = 0;
129
+ #processed = 0;
130
+ #lc = 0;
131
+ #lp = 0;
132
+ #pb = 0;
133
+ #state = 0;
134
+ #rep0 = 1;
135
+ #rep1 = 1;
136
+ #rep2 = 1;
137
+ #rep3 = 1;
138
+ #literal = initializedProbabilities(LITERAL_SIZE);
139
+ readonly #isMatch = initializedProbabilities(NUM_STATES * NUM_POS_STATES_MAX);
140
+ readonly #isRep = initializedProbabilities(NUM_STATES);
141
+ readonly #isRepG0 = initializedProbabilities(NUM_STATES);
142
+ readonly #isRepG1 = initializedProbabilities(NUM_STATES);
143
+ readonly #isRepG2 = initializedProbabilities(NUM_STATES);
144
+ readonly #isRep0Long = initializedProbabilities(NUM_STATES * NUM_POS_STATES_MAX);
145
+ readonly #posSlot = initializedProbabilities(4 << 6);
146
+ readonly #posDecoders = initializedProbabilities(115);
147
+ readonly #align = initializedProbabilities(16);
148
+ readonly #len = new LengthDecoder();
149
+ readonly #repLen = new LengthDecoder();
150
+
151
+ constructor(output: Uint8Array, props: LzmaProperties) {
152
+ this.#output = output;
153
+ this.#dictionarySize = props.dictionarySize;
154
+ this.setProperties(props.lc, props.lp, props.pb);
155
+ this.resetDictionary();
156
+ }
157
+
158
+ get outputPos(): number {
159
+ return this.#outputPos;
160
+ }
161
+
162
+ setProperties(lc: number, lp: number, pb: number): void {
163
+ if (lc > 8 || lp > 4 || pb > 4 || lc + lp > 12) throw new ArchiveError("Invalid LZMA properties");
164
+ this.#lc = lc;
165
+ this.#lp = lp;
166
+ this.#pb = pb;
167
+ const literalCount = LITERAL_SIZE * 2 ** (lc + lp);
168
+ if (this.#literal.length !== literalCount) this.#literal = initializedProbabilities(literalCount);
169
+ }
170
+
171
+ resetDictionary(): void {
172
+ this.#dictionaryStart = this.#outputPos;
173
+ this.#processed = 0;
174
+ this.resetState();
175
+ }
176
+
177
+ resetState(): void {
178
+ this.#state = 0;
179
+ this.#rep0 = this.#rep1 = this.#rep2 = this.#rep3 = 1;
180
+ this.#literal.fill(BIT_MODEL_TOTAL >>> 1);
181
+ this.#isMatch.fill(BIT_MODEL_TOTAL >>> 1);
182
+ this.#isRep.fill(BIT_MODEL_TOTAL >>> 1);
183
+ this.#isRepG0.fill(BIT_MODEL_TOTAL >>> 1);
184
+ this.#isRepG1.fill(BIT_MODEL_TOTAL >>> 1);
185
+ this.#isRepG2.fill(BIT_MODEL_TOTAL >>> 1);
186
+ this.#isRep0Long.fill(BIT_MODEL_TOTAL >>> 1);
187
+ this.#posSlot.fill(BIT_MODEL_TOTAL >>> 1);
188
+ this.#posDecoders.fill(BIT_MODEL_TOTAL >>> 1);
189
+ this.#align.fill(BIT_MODEL_TOTAL >>> 1);
190
+ this.#len.reset();
191
+ this.#repLen.reset();
192
+ }
193
+
194
+ appendUncompressed(bytes: Uint8Array, resetDictionary: boolean): void {
195
+ if (resetDictionary) this.resetDictionary();
196
+ if (bytes.byteLength > this.#output.byteLength - this.#outputPos)
197
+ throw new ArchiveError("LZMA2 output exceeds its limit");
198
+ this.#output.set(bytes, this.#outputPos);
199
+ this.#outputPos += bytes.byteLength;
200
+ this.#processed += bytes.byteLength;
201
+ }
202
+
203
+ decodeChunk(bytes: Uint8Array, outputSize: number, allowEndMarker = false): boolean {
204
+ if (
205
+ !Number.isSafeInteger(outputSize) ||
206
+ outputSize < 0 ||
207
+ outputSize > this.#output.byteLength - this.#outputPos
208
+ ) {
209
+ throw new ArchiveError("LZMA output exceeds its declared size");
210
+ }
211
+ const limit = this.#outputPos + outputSize;
212
+ const range = new RangeDecoder(bytes);
213
+ const posMask = (1 << this.#pb) - 1;
214
+ const literalPosMask = (1 << this.#lp) - 1;
215
+
216
+ while (this.#outputPos < limit || allowEndMarker) {
217
+ const posState = this.#processed & posMask;
218
+ if (range.decodeBit(this.#isMatch, (this.#state << 4) + posState) === 0) {
219
+ const previous = this.#outputPos === this.#dictionaryStart ? 0 : this.#output[this.#outputPos - 1]!;
220
+ const context =
221
+ (((this.#processed & literalPosMask) << this.#lc) + (previous >>> (8 - this.#lc))) * LITERAL_SIZE;
222
+ let symbol = 1;
223
+ if (this.#state >= 7) {
224
+ this.#assertDistance(this.#rep0);
225
+ let matchByte = this.#output[this.#outputPos - this.#rep0]!;
226
+ while (symbol < 0x100) {
227
+ const matchBit = (matchByte >>> 7) & 1;
228
+ matchByte = (matchByte << 1) & 0xff;
229
+ const bit = range.decodeBit(this.#literal, context + ((1 + matchBit) << 8) + symbol);
230
+ symbol = (symbol << 1) | bit;
231
+ if (matchBit !== bit) break;
232
+ }
233
+ }
234
+ while (symbol < 0x100) symbol = (symbol << 1) | range.decodeBit(this.#literal, context + symbol);
235
+ if (this.#outputPos >= limit)
236
+ throw new ArchiveError("LZMA output exceeds its size limit before the end marker");
237
+ this.#output[this.#outputPos++] = symbol & 0xff;
238
+ this.#processed++;
239
+ this.#state = this.#state < 4 ? 0 : this.#state < 10 ? this.#state - 3 : this.#state - 6;
240
+ continue;
241
+ }
242
+
243
+ let length: number;
244
+ if (range.decodeBit(this.#isRep, this.#state) === 1) {
245
+ if (range.decodeBit(this.#isRepG0, this.#state) === 0) {
246
+ if (range.decodeBit(this.#isRep0Long, (this.#state << 4) + posState) === 0) {
247
+ this.#state = this.#state < 7 ? 9 : 11;
248
+ this.#copyMatch(this.#rep0, 1, limit);
249
+ continue;
250
+ }
251
+ } else {
252
+ let distance: number;
253
+ if (range.decodeBit(this.#isRepG1, this.#state) === 0) distance = this.#rep1;
254
+ else {
255
+ if (range.decodeBit(this.#isRepG2, this.#state) === 0) distance = this.#rep2;
256
+ else {
257
+ distance = this.#rep3;
258
+ this.#rep3 = this.#rep2;
259
+ }
260
+ this.#rep2 = this.#rep1;
261
+ }
262
+ this.#rep1 = this.#rep0;
263
+ this.#rep0 = distance;
264
+ }
265
+ length = this.#repLen.decode(range, posState) + MATCH_MIN_LEN;
266
+ this.#state = this.#state < 7 ? 8 : 11;
267
+ } else {
268
+ this.#rep3 = this.#rep2;
269
+ this.#rep2 = this.#rep1;
270
+ this.#rep1 = this.#rep0;
271
+ length = this.#len.decode(range, posState) + MATCH_MIN_LEN;
272
+ this.#state = this.#state < 7 ? 7 : 10;
273
+ const lenState = Math.min(length - MATCH_MIN_LEN, 3);
274
+ const slot = decodeTree(range, this.#posSlot, lenState << 6, 6);
275
+ let distance: number;
276
+ if (slot < 4) distance = slot;
277
+ else {
278
+ const directBits = (slot >>> 1) - 1;
279
+ distance = ((2 | (slot & 1)) << directBits) >>> 0;
280
+ if (slot < 14)
281
+ distance =
282
+ (distance + decodeReverseTree(range, this.#posDecoders, distance - slot, directBits)) >>> 0;
283
+ else {
284
+ distance = (distance + (range.decodeDirectBits(directBits - 4) << 4)) >>> 0;
285
+ distance = (distance + decodeReverseTree(range, this.#align, 0, 4)) >>> 0;
286
+ if (distance === 0xffffffff) {
287
+ if (allowEndMarker) return true;
288
+ throw new ArchiveError("LZMA stream ended before its declared output size");
289
+ }
290
+ }
291
+ }
292
+ this.#rep0 = (distance + 1) >>> 0;
293
+ }
294
+ this.#copyMatch(this.#rep0, length, limit);
295
+ }
296
+ return false;
297
+ }
298
+
299
+ #assertDistance(distance: number): void {
300
+ const available = this.#outputPos - this.#dictionaryStart;
301
+ if (distance < 1 || distance > available || distance > this.#dictionarySize) {
302
+ throw new ArchiveError(`Invalid LZMA match distance ${distance} with ${available} bytes available`);
303
+ }
304
+ }
305
+
306
+ #copyMatch(distance: number, length: number, limit: number): void {
307
+ this.#assertDistance(distance);
308
+ if (length > limit - this.#outputPos) throw new ArchiveError("LZMA match exceeds its declared output size");
309
+ for (let index = 0; index < length; index++) {
310
+ this.#output[this.#outputPos] = this.#output[this.#outputPos - distance]!;
311
+ this.#outputPos++;
312
+ }
313
+ this.#processed += length;
314
+ }
315
+ }
316
+
317
+ /** Decompress a raw LZMA1 stream using the standard five-byte properties. */
318
+ export async function lzmaDecompress(props: Uint8Array, bytes: Uint8Array, outSize: number): Promise<Uint8Array> {
319
+ if (!Number.isSafeInteger(outSize) || outSize < 0) throw new ArchiveError("Invalid LZMA output size");
320
+ try {
321
+ const output = new Uint8Array(outSize);
322
+ const decoder = new LzmaDecoder(output, parseLzmaProperties(props));
323
+ decoder.decodeChunk(bytes, outSize);
324
+ if (decoder.outputPos !== outSize) throw new ArchiveError("LZMA output size mismatch");
325
+ return output;
326
+ } catch (error) {
327
+ if (error instanceof ArchiveError) throw error;
328
+ throw new ArchiveError(`Invalid LZMA stream: ${error instanceof Error ? error.message : String(error)}`);
329
+ }
330
+ }
331
+
332
+ /** Decompress an LZMA-alone container, including unknown-size end-marked streams. */
333
+ export async function lzmaAloneDecompress(bytes: Uint8Array, maxOutput: number): Promise<Uint8Array> {
334
+ if (bytes.byteLength < 13) throw new ArchiveError("Invalid LZMA-alone stream: truncated header");
335
+ if (!Number.isSafeInteger(maxOutput) || maxOutput < 0) throw new ArchiveError("Invalid LZMA output limit");
336
+ const props = bytes.subarray(0, 5);
337
+ let unknownSize = true;
338
+ let declaredSize = 0;
339
+ for (let index = 0; index < 8; index++) {
340
+ const value = bytes[5 + index]!;
341
+ if (value !== 0xff) unknownSize = false;
342
+ declaredSize += value * 2 ** (index * 8);
343
+ }
344
+ if (!unknownSize) {
345
+ if (!Number.isSafeInteger(declaredSize) || declaredSize > maxOutput) {
346
+ throw new ArchiveError("LZMA-alone output exceeds its size limit");
347
+ }
348
+ return lzmaDecompress(props, bytes.subarray(13), declaredSize);
349
+ }
350
+ try {
351
+ const output = new Uint8Array(maxOutput);
352
+ const decoder = new LzmaDecoder(output, parseLzmaProperties(props));
353
+ if (!decoder.decodeChunk(bytes.subarray(13), maxOutput, true)) {
354
+ throw new ArchiveError("LZMA-alone output exceeds its size limit before the end marker");
355
+ }
356
+ return output.subarray(0, decoder.outputPos);
357
+ } catch (error) {
358
+ if (error instanceof ArchiveError) throw error;
359
+ throw new ArchiveError(`Invalid LZMA-alone stream: ${error instanceof Error ? error.message : String(error)}`);
360
+ }
361
+ }
362
+
363
+ /** Decompress an LZMA2 stream, enforcing its dictionary and output limits. */
364
+ export async function lzma2Decompress(dictSizeProp: number, bytes: Uint8Array, maxOutput: number): Promise<Uint8Array> {
365
+ if (!Number.isInteger(dictSizeProp) || dictSizeProp < 0 || dictSizeProp > 40)
366
+ throw new ArchiveError("Unsupported LZMA2 dictionary property");
367
+ if (!Number.isSafeInteger(maxOutput) || maxOutput < 0) throw new ArchiveError("Invalid LZMA2 output limit");
368
+ const dictionarySize =
369
+ dictSizeProp === 40 ? 0xffffffff : (2 | (dictSizeProp & 1)) * 2 ** (Math.floor(dictSizeProp / 2) + 11);
370
+ const output = new Uint8Array(maxOutput);
371
+ const decoder = new LzmaDecoder(output, { lc: 3, lp: 0, pb: 2, dictionarySize });
372
+ let pos = 0;
373
+ let needDictionaryReset = true;
374
+ let propertiesSet = false;
375
+
376
+ const takeByte = (): number => {
377
+ if (pos >= bytes.byteLength) throw new ArchiveError("Invalid LZMA2 stream: truncated chunk header");
378
+ return bytes[pos++]!;
379
+ };
380
+
381
+ try {
382
+ for (;;) {
383
+ const control = takeByte();
384
+ if (control === 0) break;
385
+ if (control < 0x80) {
386
+ if (control !== 1 && control !== 2)
387
+ throw new ArchiveError(`Invalid LZMA2 control byte 0x${control.toString(16)}`);
388
+ if (control === 2 && needDictionaryReset)
389
+ throw new ArchiveError("Invalid LZMA2 stream: dictionary was not initialized");
390
+ const unpackSize = (takeByte() << 8) + takeByte() + 1;
391
+ if (unpackSize > maxOutput - decoder.outputPos || unpackSize > bytes.byteLength - pos) {
392
+ throw new ArchiveError("Invalid LZMA2 uncompressed chunk size");
393
+ }
394
+ decoder.appendUncompressed(bytes.subarray(pos, pos + unpackSize), control === 1);
395
+ pos += unpackSize;
396
+ if (control === 1) {
397
+ needDictionaryReset = false;
398
+ propertiesSet = false;
399
+ }
400
+ continue;
401
+ }
402
+
403
+ const unpackSize = ((control & 0x1f) << 16) + (takeByte() << 8) + takeByte() + 1;
404
+ const packSize = (takeByte() << 8) + takeByte() + 1;
405
+ const resetsDictionary = control >= 0xe0;
406
+ const resetsState = control >= 0xa0;
407
+ const setsProperties = control >= 0xc0;
408
+ if (needDictionaryReset && !resetsDictionary)
409
+ throw new ArchiveError("Invalid LZMA2 stream: dictionary was not initialized");
410
+ if (!propertiesSet && !setsProperties)
411
+ throw new ArchiveError("Invalid LZMA2 stream: properties were not initialized");
412
+ if (setsProperties) {
413
+ let property = takeByte();
414
+ if (property >= 9 * 5 * 5) throw new ArchiveError("Invalid LZMA2 properties");
415
+ const lc = property % 9;
416
+ property = Math.floor(property / 9);
417
+ const lp = property % 5;
418
+ const pb = Math.floor(property / 5);
419
+ if (lc + lp > 4) throw new ArchiveError("Invalid LZMA2 literal properties");
420
+ decoder.setProperties(lc, lp, pb);
421
+ propertiesSet = true;
422
+ }
423
+ if (packSize > bytes.byteLength - pos || unpackSize > maxOutput - decoder.outputPos)
424
+ throw new ArchiveError("Invalid LZMA2 chunk size");
425
+ if (resetsDictionary) decoder.resetDictionary();
426
+ else if (resetsState) decoder.resetState();
427
+ decoder.decodeChunk(bytes.subarray(pos, pos + packSize), unpackSize);
428
+ pos += packSize;
429
+ needDictionaryReset = false;
430
+ }
431
+ if (pos !== bytes.byteLength) throw new ArchiveError("Invalid LZMA2 stream: trailing data");
432
+ return output.subarray(0, decoder.outputPos);
433
+ } catch (error) {
434
+ if (error instanceof ArchiveError) throw error;
435
+ throw new ArchiveError(`Invalid LZMA2 stream: ${error instanceof Error ? error.message : String(error)}`);
436
+ }
437
+ }
@@ -0,0 +1,191 @@
1
+ import { ArchiveError } from "../error";
2
+
3
+ const CLEAR_CODE = 256;
4
+ const FIRST_BLOCK_CODE = 257;
5
+ const MIN_BITS = 9;
6
+ const MAX_BITS = 16;
7
+
8
+ class LsbCodeReader {
9
+ readonly #bytes: Uint8Array;
10
+ #bitPosition = 0;
11
+ #groupStart = 0;
12
+
13
+ constructor(bytes: Uint8Array) {
14
+ this.#bytes = bytes;
15
+ }
16
+
17
+ get remainingBits(): number {
18
+ return this.#bytes.byteLength * 8 - this.#bitPosition;
19
+ }
20
+
21
+ read(width: number): number | undefined {
22
+ if (this.remainingBits < width) {
23
+ return undefined;
24
+ }
25
+ let value = 0;
26
+ for (let bit = 0; bit < width; bit++) {
27
+ const position = this.#bitPosition + bit;
28
+ value += ((this.#bytes[position >>> 3]! >>> (position & 7)) & 1) * 2 ** bit;
29
+ }
30
+ this.#bitPosition += width;
31
+ return value;
32
+ }
33
+
34
+ alignCodeGroup(width: number): void {
35
+ const groupBits = width * 8;
36
+ const aligned = this.#groupStart + Math.ceil((this.#bitPosition - this.#groupStart) / groupBits) * groupBits;
37
+ if (aligned > this.#bytes.byteLength * 8) {
38
+ throw new ArchiveError("Truncated compress (.Z) code group");
39
+ }
40
+ this.#bitPosition = aligned;
41
+ this.#groupStart = aligned;
42
+ }
43
+
44
+ assertFinalPadding(): void {
45
+ this.#assertZeroBits(this.#bytes.byteLength * 8);
46
+ }
47
+
48
+ #assertZeroBits(end: number): void {
49
+ for (let position = this.#bitPosition; position < end; position++) {
50
+ if (((this.#bytes[position >>> 3]! >>> (position & 7)) & 1) !== 0) {
51
+ throw new ArchiveError("Invalid non-zero compress (.Z) padding");
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ class BoundedOutput {
58
+ readonly #limit: number;
59
+ #bytes: Uint8Array;
60
+ #length = 0;
61
+
62
+ constructor(limit: number, inputSize: number) {
63
+ this.#limit = limit;
64
+ this.#bytes = new Uint8Array(Math.min(limit, Math.max(64, Math.min(inputSize * 2, 64 * 1024))));
65
+ }
66
+
67
+ appendReversed(stack: Uint8Array, length: number): void {
68
+ this.#ensure(length);
69
+ for (let index = length - 1; index >= 0; index--) {
70
+ this.#bytes[this.#length++] = stack[index]!;
71
+ }
72
+ }
73
+
74
+ finish(): Uint8Array {
75
+ return this.#bytes.slice(0, this.#length);
76
+ }
77
+
78
+ #ensure(additional: number): void {
79
+ const needed = this.#length + additional;
80
+ if (!Number.isSafeInteger(needed) || needed > this.#limit) {
81
+ throw new ArchiveError(`Compress (.Z) output exceeds the ${this.#limit}-byte limit`);
82
+ }
83
+ if (needed <= this.#bytes.byteLength) {
84
+ return;
85
+ }
86
+ let capacity = Math.max(needed, Math.min(this.#limit, Math.max(64, this.#bytes.byteLength * 2)));
87
+ if (capacity > this.#limit) {
88
+ capacity = this.#limit;
89
+ }
90
+ const grown = new Uint8Array(capacity);
91
+ grown.set(this.#bytes.subarray(0, this.#length));
92
+ this.#bytes = grown;
93
+ }
94
+ }
95
+
96
+ function decode(bytes: Uint8Array, maxOutput: number): Uint8Array {
97
+ if (!Number.isSafeInteger(maxOutput) || maxOutput < 0) {
98
+ throw new ArchiveError("Invalid compress (.Z) output limit");
99
+ }
100
+ if (bytes.byteLength < 3) {
101
+ throw new ArchiveError("Truncated compress (.Z) header");
102
+ }
103
+ if (bytes[0] !== 0x1f || bytes[1] !== 0x9d) {
104
+ throw new ArchiveError("Invalid compress (.Z) header");
105
+ }
106
+
107
+ const flags = bytes[2]!;
108
+ if ((flags & 0x60) !== 0) {
109
+ throw new ArchiveError("Unsupported compress (.Z) header flags");
110
+ }
111
+ const maxBits = flags & 0x1f;
112
+ if (maxBits < MIN_BITS || maxBits > MAX_BITS) {
113
+ throw new ArchiveError(`Invalid compress (.Z) maximum code width ${maxBits}`);
114
+ }
115
+ const blockMode = (flags & 0x80) !== 0;
116
+ const dictionaryLimit = 2 ** maxBits;
117
+ const parents = new Uint16Array(dictionaryLimit);
118
+ const suffixes = new Uint8Array(dictionaryLimit);
119
+ const stack = new Uint8Array(dictionaryLimit);
120
+ const reader = new LsbCodeReader(bytes.subarray(3));
121
+ const output = new BoundedOutput(maxOutput, bytes.byteLength);
122
+
123
+ let width = MIN_BITS;
124
+ let dictionaryHead = blockMode ? FIRST_BLOCK_CODE : 256;
125
+ let needsPreviousSuffix = false;
126
+
127
+ for (;;) {
128
+ const code = reader.read(width);
129
+ if (code === undefined) {
130
+ reader.assertFinalPadding();
131
+ return output.finish();
132
+ }
133
+ if (code >= dictionaryHead) {
134
+ throw new ArchiveError(`Corrupt compress (.Z) dictionary code ${code}`);
135
+ }
136
+ if (blockMode && code === CLEAR_CODE) {
137
+ reader.alignCodeGroup(width);
138
+ width = MIN_BITS;
139
+ dictionaryHead = FIRST_BLOCK_CODE;
140
+ needsPreviousSuffix = false;
141
+ continue;
142
+ }
143
+
144
+ let current = code;
145
+ let stackLength = 0;
146
+ while (current >= 256) {
147
+ if (current >= dictionaryHead || stackLength >= stack.length - 1) {
148
+ throw new ArchiveError("Corrupt compress (.Z) dictionary chain");
149
+ }
150
+ stack[stackLength++] = suffixes[current]!;
151
+ current = parents[current]!;
152
+ }
153
+ stack[stackLength++] = current;
154
+
155
+ if (needsPreviousSuffix) {
156
+ suffixes[dictionaryHead - 1] = current;
157
+ if (code === dictionaryHead - 1) {
158
+ stack[0] = current;
159
+ }
160
+ }
161
+ output.appendReversed(stack, stackLength);
162
+
163
+ if (dictionaryHead < dictionaryLimit) {
164
+ needsPreviousSuffix = true;
165
+ parents[dictionaryHead++] = code;
166
+ if (dictionaryHead > 2 ** width && width < maxBits) {
167
+ reader.alignCodeGroup(width);
168
+ width++;
169
+ }
170
+ } else {
171
+ needsPreviousSuffix = false;
172
+ }
173
+ }
174
+ }
175
+
176
+ /** Return whether bytes begin with the ncompress `.Z` magic number. */
177
+ export function isCompressZ(bytes: Uint8Array): boolean {
178
+ return bytes.byteLength >= 2 && bytes[0] === 0x1f && bytes[1] === 0x9d;
179
+ }
180
+
181
+ /** Decompress an ncompress `.Z` stream while enforcing a hard output bound. */
182
+ export async function lzwDecompress(bytes: Uint8Array, maxOutput: number): Promise<Uint8Array> {
183
+ try {
184
+ return decode(bytes, maxOutput);
185
+ } catch (error) {
186
+ if (error instanceof ArchiveError) {
187
+ throw error;
188
+ }
189
+ throw new ArchiveError(error instanceof Error ? error.message : String(error));
190
+ }
191
+ }