stormlib-js 0.1.0 → 0.1.1
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/README.md +29 -0
- package/dist/chunk-RRBXXQVG.mjs +2410 -0
- package/dist/chunk-RRBXXQVG.mjs.map +1 -0
- package/dist/index.browser.d.mts +195 -0
- package/dist/index.browser.d.ts +195 -0
- package/dist/index.browser.js +3002 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +693 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +25 -241
- package/dist/index.d.ts +25 -241
- package/dist/index.js +87 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +155 -2338
- package/dist/index.mjs.map +1 -1
- package/dist/storm-buffer-nHXHoPVG.d.mts +242 -0
- package/dist/storm-buffer-nHXHoPVG.d.ts +242 -0
- package/package.json +20 -3
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ATTRIBUTES_NAME,
|
|
3
|
+
BLOCK_INDEX_MASK,
|
|
4
|
+
ID_MPQ,
|
|
5
|
+
ID_MPQ_USERDATA,
|
|
6
|
+
LISTFILE_NAME,
|
|
7
|
+
MPQ_COMPRESSION_ADPCM_MONO,
|
|
8
|
+
MPQ_COMPRESSION_ADPCM_STEREO,
|
|
9
|
+
MPQ_COMPRESSION_BZIP2,
|
|
10
|
+
MPQ_COMPRESSION_HUFFMANN,
|
|
11
|
+
MPQ_COMPRESSION_LZMA,
|
|
12
|
+
MPQ_COMPRESSION_PKWARE,
|
|
13
|
+
MPQ_COMPRESSION_SPARSE,
|
|
14
|
+
MPQ_COMPRESSION_ZLIB,
|
|
15
|
+
MPQ_FILE_COMPRESS,
|
|
16
|
+
MPQ_FILE_ENCRYPTED,
|
|
17
|
+
MPQ_FILE_EXISTS,
|
|
18
|
+
MPQ_FILE_IMPLODE,
|
|
19
|
+
MPQ_FILE_KEY_V2,
|
|
20
|
+
MPQ_FILE_SECTOR_CRC,
|
|
21
|
+
MPQ_FILE_SINGLE_UNIT,
|
|
22
|
+
MPQ_FORMAT_VERSION_1,
|
|
23
|
+
MPQ_FORMAT_VERSION_2,
|
|
24
|
+
MPQ_FORMAT_VERSION_3,
|
|
25
|
+
MPQ_FORMAT_VERSION_4,
|
|
26
|
+
MpqCompressionError,
|
|
27
|
+
MpqCorruptError,
|
|
28
|
+
MpqEncryptionError,
|
|
29
|
+
MpqError,
|
|
30
|
+
MpqNotFoundError,
|
|
31
|
+
MpqUnsupportedError,
|
|
32
|
+
SIGNATURE_NAME,
|
|
33
|
+
applyFileNames,
|
|
34
|
+
buildFileTable,
|
|
35
|
+
buildFileTableFromHetBet,
|
|
36
|
+
decompress,
|
|
37
|
+
decompressPkware,
|
|
38
|
+
decryptBlock,
|
|
39
|
+
decryptFileKey,
|
|
40
|
+
encryptBlock,
|
|
41
|
+
findHashEntry,
|
|
42
|
+
findHeader,
|
|
43
|
+
findInHetTable,
|
|
44
|
+
getBetTableOffset,
|
|
45
|
+
getBlockTableOffset,
|
|
46
|
+
getHashTableOffset,
|
|
47
|
+
getHetTableOffset,
|
|
48
|
+
getHiBlockTableOffset,
|
|
49
|
+
getSectorSize,
|
|
50
|
+
getStormBuffer,
|
|
51
|
+
hashFileKey,
|
|
52
|
+
hashNameA,
|
|
53
|
+
hashNameB,
|
|
54
|
+
hashString,
|
|
55
|
+
hashTableIndex,
|
|
56
|
+
init_pkware,
|
|
57
|
+
jenkinsHash,
|
|
58
|
+
loadBetTable,
|
|
59
|
+
loadBlockTable,
|
|
60
|
+
loadHashTable,
|
|
61
|
+
loadHetTable,
|
|
62
|
+
loadHiBlockTable,
|
|
63
|
+
parseAttributes,
|
|
64
|
+
parseListfile
|
|
65
|
+
} from "./chunk-RRBXXQVG.mjs";
|
|
66
|
+
|
|
67
|
+
// src/index.browser.ts
|
|
68
|
+
import { Buffer as BufferPolyfill } from "buffer";
|
|
69
|
+
|
|
70
|
+
// src/stream/file-stream.browser.ts
|
|
71
|
+
var FileStream = class _FileStream {
|
|
72
|
+
constructor(fileSize, filePath, memoryData) {
|
|
73
|
+
this.fileSize = fileSize;
|
|
74
|
+
this.filePath = filePath;
|
|
75
|
+
this.memoryData = memoryData;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Path-based open is not available in browsers.
|
|
79
|
+
*/
|
|
80
|
+
static open(_path) {
|
|
81
|
+
throw new Error("Path-based archive open is not available in browser runtime. Use MpqArchive.openFromBuffer().");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create an in-memory stream from binary data.
|
|
85
|
+
*/
|
|
86
|
+
static fromBuffer(data, filePath = "[buffer]") {
|
|
87
|
+
const buffer = _FileStream.normalizeBuffer(data);
|
|
88
|
+
return new _FileStream(BigInt(buffer.length), filePath, buffer);
|
|
89
|
+
}
|
|
90
|
+
static normalizeBuffer(data) {
|
|
91
|
+
if (Buffer.isBuffer(data)) {
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
if (data instanceof Uint8Array) {
|
|
95
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
96
|
+
}
|
|
97
|
+
return Buffer.from(data);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Read bytes at a given offset.
|
|
101
|
+
*/
|
|
102
|
+
read(offset, length) {
|
|
103
|
+
if (!this.memoryData) {
|
|
104
|
+
return Buffer.alloc(0);
|
|
105
|
+
}
|
|
106
|
+
const start = Number(offset);
|
|
107
|
+
if (start >= this.memoryData.length || length <= 0) {
|
|
108
|
+
return Buffer.alloc(0);
|
|
109
|
+
}
|
|
110
|
+
const end = Math.min(start + length, this.memoryData.length);
|
|
111
|
+
return this.memoryData.subarray(start, end);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read bytes into an existing buffer.
|
|
115
|
+
*/
|
|
116
|
+
readInto(offset, buffer, bufOffset, length) {
|
|
117
|
+
if (!this.memoryData) {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
const start = Number(offset);
|
|
121
|
+
if (start >= this.memoryData.length || length <= 0) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const end = Math.min(start + length, this.memoryData.length);
|
|
125
|
+
return this.memoryData.copy(buffer, bufOffset, start, end);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get the total file size.
|
|
129
|
+
*/
|
|
130
|
+
getSize() {
|
|
131
|
+
return this.fileSize;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the file path.
|
|
135
|
+
*/
|
|
136
|
+
getPath() {
|
|
137
|
+
return this.filePath;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Close the stream.
|
|
141
|
+
*/
|
|
142
|
+
close() {
|
|
143
|
+
this.memoryData = null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/file/sector-reader.browser.ts
|
|
148
|
+
init_pkware();
|
|
149
|
+
function readSectorOffsets(stream, fileOffset, fileEntry, sectorSize, fileKey) {
|
|
150
|
+
const sectorCount = Math.ceil(fileEntry.fileSize / sectorSize);
|
|
151
|
+
let offsetCount = sectorCount + 1;
|
|
152
|
+
if (fileEntry.flags & MPQ_FILE_SECTOR_CRC) {
|
|
153
|
+
offsetCount++;
|
|
154
|
+
}
|
|
155
|
+
const tableSize = offsetCount * 4;
|
|
156
|
+
const tableData = stream.read(fileOffset, tableSize);
|
|
157
|
+
if (tableData.length < tableSize) {
|
|
158
|
+
throw new MpqCorruptError("Sector offset table is truncated");
|
|
159
|
+
}
|
|
160
|
+
if (fileEntry.flags & MPQ_FILE_ENCRYPTED) {
|
|
161
|
+
decryptBlock(tableData, fileKey - 1 >>> 0);
|
|
162
|
+
}
|
|
163
|
+
const offsets = new Uint32Array(offsetCount);
|
|
164
|
+
for (let i = 0; i < offsetCount; i++) {
|
|
165
|
+
offsets[i] = tableData.readUInt32LE(i * 4);
|
|
166
|
+
}
|
|
167
|
+
return offsets;
|
|
168
|
+
}
|
|
169
|
+
function readSector(stream, fileOffset, sectorOffsets, sectorIndex, fileEntry, sectorSize, fileKey) {
|
|
170
|
+
const rawOffset = sectorOffsets[sectorIndex];
|
|
171
|
+
const rawEnd = sectorOffsets[sectorIndex + 1];
|
|
172
|
+
const rawSize = rawEnd - rawOffset;
|
|
173
|
+
const sectorStart = sectorIndex * sectorSize;
|
|
174
|
+
const expectedSize = Math.min(sectorSize, fileEntry.fileSize - sectorStart);
|
|
175
|
+
let sectorData = stream.read(fileOffset + BigInt(rawOffset), rawSize);
|
|
176
|
+
if (sectorData.length < rawSize) {
|
|
177
|
+
throw new MpqCorruptError(`Sector ${sectorIndex} data is truncated`);
|
|
178
|
+
}
|
|
179
|
+
if (fileEntry.flags & MPQ_FILE_ENCRYPTED) {
|
|
180
|
+
sectorData = Buffer.from(sectorData);
|
|
181
|
+
decryptBlock(sectorData, fileKey + sectorIndex >>> 0);
|
|
182
|
+
}
|
|
183
|
+
if (rawSize < expectedSize) {
|
|
184
|
+
if (fileEntry.flags & MPQ_FILE_COMPRESS) {
|
|
185
|
+
sectorData = decompress(sectorData, expectedSize);
|
|
186
|
+
} else if (fileEntry.flags & MPQ_FILE_IMPLODE) {
|
|
187
|
+
sectorData = decompressPkware(sectorData, expectedSize);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return sectorData;
|
|
191
|
+
}
|
|
192
|
+
function readSectorFile(stream, archiveOffset, fileEntry, sectorSize, fileKey) {
|
|
193
|
+
const fileOffset = archiveOffset + fileEntry.byteOffset;
|
|
194
|
+
const sectorOffsets = readSectorOffsets(stream, fileOffset, fileEntry, sectorSize, fileKey);
|
|
195
|
+
const sectorCount = Math.ceil(fileEntry.fileSize / sectorSize);
|
|
196
|
+
const output = Buffer.alloc(fileEntry.fileSize);
|
|
197
|
+
let outPos = 0;
|
|
198
|
+
for (let i = 0; i < sectorCount; i++) {
|
|
199
|
+
const sectorData = readSector(
|
|
200
|
+
stream,
|
|
201
|
+
fileOffset,
|
|
202
|
+
sectorOffsets,
|
|
203
|
+
i,
|
|
204
|
+
fileEntry,
|
|
205
|
+
sectorSize,
|
|
206
|
+
fileKey
|
|
207
|
+
);
|
|
208
|
+
const copyLen = Math.min(sectorData.length, fileEntry.fileSize - outPos);
|
|
209
|
+
sectorData.copy(output, outPos, 0, copyLen);
|
|
210
|
+
outPos += copyLen;
|
|
211
|
+
}
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
function readSingleUnitFile(stream, archiveOffset, fileEntry, fileKey) {
|
|
215
|
+
const fileOffset = archiveOffset + fileEntry.byteOffset;
|
|
216
|
+
let data = stream.read(fileOffset, fileEntry.cmpSize);
|
|
217
|
+
if (data.length < fileEntry.cmpSize) {
|
|
218
|
+
throw new MpqCorruptError("Single-unit file data is truncated");
|
|
219
|
+
}
|
|
220
|
+
if (fileEntry.flags & MPQ_FILE_ENCRYPTED) {
|
|
221
|
+
data = Buffer.from(data);
|
|
222
|
+
decryptBlock(data, fileKey);
|
|
223
|
+
}
|
|
224
|
+
if (fileEntry.cmpSize < fileEntry.fileSize) {
|
|
225
|
+
if (fileEntry.flags & MPQ_FILE_COMPRESS) {
|
|
226
|
+
data = decompress(data, fileEntry.fileSize);
|
|
227
|
+
} else if (fileEntry.flags & MPQ_FILE_IMPLODE) {
|
|
228
|
+
data = decompressPkware(data, fileEntry.fileSize);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return data;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/file/mpq-file.browser.ts
|
|
235
|
+
var MpqFile = class {
|
|
236
|
+
constructor(stream, archiveOffset, entry, sectorSize) {
|
|
237
|
+
this.closed = false;
|
|
238
|
+
this.stream = stream;
|
|
239
|
+
this.archiveOffset = archiveOffset;
|
|
240
|
+
this.entry = entry;
|
|
241
|
+
this.sectorSize = sectorSize;
|
|
242
|
+
if (entry.flags & MPQ_FILE_ENCRYPTED) {
|
|
243
|
+
this.fileKey = decryptFileKey(
|
|
244
|
+
entry.fileName,
|
|
245
|
+
entry.byteOffset,
|
|
246
|
+
entry.fileSize,
|
|
247
|
+
entry.flags
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
this.fileKey = 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Uncompressed file size */
|
|
254
|
+
get size() {
|
|
255
|
+
return this.entry.fileSize;
|
|
256
|
+
}
|
|
257
|
+
/** Compressed file size */
|
|
258
|
+
get compressedSize() {
|
|
259
|
+
return this.entry.cmpSize;
|
|
260
|
+
}
|
|
261
|
+
/** File flags */
|
|
262
|
+
get flags() {
|
|
263
|
+
return this.entry.flags;
|
|
264
|
+
}
|
|
265
|
+
/** File name */
|
|
266
|
+
get name() {
|
|
267
|
+
return this.entry.fileName;
|
|
268
|
+
}
|
|
269
|
+
/** The underlying file entry */
|
|
270
|
+
get fileEntry() {
|
|
271
|
+
return this.entry;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Read the entire file contents.
|
|
275
|
+
*
|
|
276
|
+
* @returns Buffer containing the decompressed file data
|
|
277
|
+
*/
|
|
278
|
+
read() {
|
|
279
|
+
if (this.closed) {
|
|
280
|
+
throw new Error("File handle is closed");
|
|
281
|
+
}
|
|
282
|
+
if (this.entry.fileSize === 0) {
|
|
283
|
+
return Buffer.alloc(0);
|
|
284
|
+
}
|
|
285
|
+
if (this.entry.flags & MPQ_FILE_SINGLE_UNIT) {
|
|
286
|
+
return readSingleUnitFile(
|
|
287
|
+
this.stream,
|
|
288
|
+
this.archiveOffset,
|
|
289
|
+
this.entry,
|
|
290
|
+
this.fileKey
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return readSectorFile(
|
|
294
|
+
this.stream,
|
|
295
|
+
this.archiveOffset,
|
|
296
|
+
this.entry,
|
|
297
|
+
this.sectorSize,
|
|
298
|
+
this.fileKey
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Close the file handle.
|
|
303
|
+
*/
|
|
304
|
+
close() {
|
|
305
|
+
this.closed = true;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/archive/mpq-archive.browser.ts
|
|
310
|
+
var asNodeStream = (stream) => stream;
|
|
311
|
+
var MpqArchive = class _MpqArchive {
|
|
312
|
+
constructor(stream, header, archiveOffset) {
|
|
313
|
+
this.hashTable = [];
|
|
314
|
+
this.blockTable = [];
|
|
315
|
+
this.hiBlockTable = null;
|
|
316
|
+
this.hetTable = null;
|
|
317
|
+
this.betTable = null;
|
|
318
|
+
this.fileEntries = [];
|
|
319
|
+
this.attributes = null;
|
|
320
|
+
this.closed = false;
|
|
321
|
+
this.stream = stream;
|
|
322
|
+
this.header = header;
|
|
323
|
+
this.archiveOffset = archiveOffset;
|
|
324
|
+
this.sectorSize = getSectorSize(header);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Open an MPQ archive from a file path.
|
|
328
|
+
*
|
|
329
|
+
* @param path - Path to the MPQ file
|
|
330
|
+
* @param options - Open options
|
|
331
|
+
* @returns MpqArchive instance
|
|
332
|
+
*/
|
|
333
|
+
static open(path, options) {
|
|
334
|
+
const stream = FileStream.open(path);
|
|
335
|
+
return _MpqArchive.openFromStream(stream, options);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Open an MPQ archive from in-memory binary data.
|
|
339
|
+
*
|
|
340
|
+
* This API works in both Node.js and browser runtimes.
|
|
341
|
+
*
|
|
342
|
+
* @param data - MPQ binary contents
|
|
343
|
+
* @param options - Open options
|
|
344
|
+
* @returns MpqArchive instance
|
|
345
|
+
*/
|
|
346
|
+
static openFromBuffer(data, options) {
|
|
347
|
+
const stream = FileStream.fromBuffer(data);
|
|
348
|
+
return _MpqArchive.openFromStream(stream, options);
|
|
349
|
+
}
|
|
350
|
+
static openFromStream(stream, options) {
|
|
351
|
+
try {
|
|
352
|
+
const noHeaderSearch = options?.noHeaderSearch ?? false;
|
|
353
|
+
const { header, headerOffset } = findHeader(asNodeStream(stream), noHeaderSearch);
|
|
354
|
+
if (options?.forceMpqV1) {
|
|
355
|
+
header.wFormatVersion = MPQ_FORMAT_VERSION_1;
|
|
356
|
+
}
|
|
357
|
+
const archive = new _MpqArchive(stream, header, headerOffset);
|
|
358
|
+
archive.loadTables();
|
|
359
|
+
if (!options?.noListfile) {
|
|
360
|
+
archive.loadListfile();
|
|
361
|
+
}
|
|
362
|
+
if (!options?.noAttributes) {
|
|
363
|
+
archive.loadAttributes();
|
|
364
|
+
}
|
|
365
|
+
return archive;
|
|
366
|
+
} catch (err) {
|
|
367
|
+
stream.close();
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Load all archive tables (hash, block, HET, BET).
|
|
373
|
+
*/
|
|
374
|
+
loadTables() {
|
|
375
|
+
const h = this.header;
|
|
376
|
+
const stream = asNodeStream(this.stream);
|
|
377
|
+
if (h.dwHashTableSize > 0) {
|
|
378
|
+
const hashOffset = getHashTableOffset(h, this.archiveOffset);
|
|
379
|
+
this.hashTable = loadHashTable(stream, hashOffset, h.dwHashTableSize);
|
|
380
|
+
}
|
|
381
|
+
if (h.dwBlockTableSize > 0) {
|
|
382
|
+
const blockOffset = getBlockTableOffset(h, this.archiveOffset);
|
|
383
|
+
this.blockTable = loadBlockTable(stream, blockOffset, h.dwBlockTableSize);
|
|
384
|
+
}
|
|
385
|
+
const hiBlockOffset = getHiBlockTableOffset(h, this.archiveOffset);
|
|
386
|
+
if (hiBlockOffset !== 0n && this.blockTable.length > 0) {
|
|
387
|
+
this.hiBlockTable = loadHiBlockTable(stream, hiBlockOffset, this.blockTable.length);
|
|
388
|
+
}
|
|
389
|
+
if (h.wFormatVersion >= MPQ_FORMAT_VERSION_3) {
|
|
390
|
+
const hetOffset = getHetTableOffset(h, this.archiveOffset);
|
|
391
|
+
if (hetOffset !== 0n) {
|
|
392
|
+
this.hetTable = loadHetTable(stream, hetOffset, h.hetTableSize64);
|
|
393
|
+
}
|
|
394
|
+
const betOffset = getBetTableOffset(h, this.archiveOffset);
|
|
395
|
+
if (betOffset !== 0n) {
|
|
396
|
+
this.betTable = loadBetTable(stream, betOffset, h.betTableSize64);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (this.hetTable && this.betTable) {
|
|
400
|
+
this.fileEntries = buildFileTableFromHetBet(this.hetTable, this.betTable);
|
|
401
|
+
} else {
|
|
402
|
+
this.fileEntries = buildFileTable(this.hashTable, this.blockTable, this.hiBlockTable);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Load and parse the (listfile) to populate file names.
|
|
407
|
+
*/
|
|
408
|
+
loadListfile() {
|
|
409
|
+
try {
|
|
410
|
+
const data = this.extractFileByName(LISTFILE_NAME);
|
|
411
|
+
if (data) {
|
|
412
|
+
const names = parseListfile(data);
|
|
413
|
+
applyFileNames(this.fileEntries, this.hashTable, names);
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Load and parse the (attributes) file.
|
|
420
|
+
*/
|
|
421
|
+
loadAttributes() {
|
|
422
|
+
try {
|
|
423
|
+
const data = this.extractFileByName(ATTRIBUTES_NAME);
|
|
424
|
+
if (data) {
|
|
425
|
+
this.attributes = parseAttributes(data, this.fileEntries.length);
|
|
426
|
+
if (this.attributes) {
|
|
427
|
+
for (let i = 0; i < this.fileEntries.length; i++) {
|
|
428
|
+
if (i < this.attributes.crc32s.length) {
|
|
429
|
+
this.fileEntries[i].crc32 = this.attributes.crc32s[i];
|
|
430
|
+
}
|
|
431
|
+
if (i < this.attributes.fileTimes.length) {
|
|
432
|
+
this.fileEntries[i].fileTime = this.attributes.fileTimes[i];
|
|
433
|
+
}
|
|
434
|
+
if (i < this.attributes.md5s.length) {
|
|
435
|
+
this.fileEntries[i].md5 = this.attributes.md5s[i];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Internal: extract a file by name without relying on the listfile.
|
|
445
|
+
* Used for bootstrapping (listfile) and (attributes) loading.
|
|
446
|
+
*/
|
|
447
|
+
extractFileByName(fileName) {
|
|
448
|
+
const hashEntry = findHashEntry(
|
|
449
|
+
this.hashTable,
|
|
450
|
+
fileName,
|
|
451
|
+
0,
|
|
452
|
+
this.fileEntries.length
|
|
453
|
+
);
|
|
454
|
+
if (!hashEntry) return null;
|
|
455
|
+
const blockIndex = hashEntry.dwBlockIndex & BLOCK_INDEX_MASK;
|
|
456
|
+
if (blockIndex >= this.fileEntries.length) return null;
|
|
457
|
+
const entry = this.fileEntries[blockIndex];
|
|
458
|
+
if (!(entry.flags & MPQ_FILE_EXISTS)) return null;
|
|
459
|
+
const savedName = entry.fileName;
|
|
460
|
+
entry.fileName = fileName;
|
|
461
|
+
try {
|
|
462
|
+
const file = new MpqFile(this.stream, this.archiveOffset, entry, this.sectorSize);
|
|
463
|
+
const data = file.read();
|
|
464
|
+
file.close();
|
|
465
|
+
return data;
|
|
466
|
+
} finally {
|
|
467
|
+
entry.fileName = savedName;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Check if a file exists in the archive.
|
|
472
|
+
*/
|
|
473
|
+
hasFile(name) {
|
|
474
|
+
this.ensureOpen();
|
|
475
|
+
if (this.hetTable && this.betTable) {
|
|
476
|
+
const betIndex = findInHetTable(this.hetTable, name);
|
|
477
|
+
if (betIndex >= 0 && betIndex < this.fileEntries.length) {
|
|
478
|
+
return !!(this.fileEntries[betIndex].flags & MPQ_FILE_EXISTS);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const hashEntry = findHashEntry(this.hashTable, name, 0, this.fileEntries.length);
|
|
482
|
+
if (!hashEntry) return false;
|
|
483
|
+
const blockIndex = hashEntry.dwBlockIndex & BLOCK_INDEX_MASK;
|
|
484
|
+
return blockIndex < this.fileEntries.length && !!(this.fileEntries[blockIndex].flags & MPQ_FILE_EXISTS);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Open a file within the archive for reading.
|
|
488
|
+
*
|
|
489
|
+
* @param name - File path within the archive
|
|
490
|
+
* @returns MpqFile handle
|
|
491
|
+
*/
|
|
492
|
+
openFile(name) {
|
|
493
|
+
this.ensureOpen();
|
|
494
|
+
const entry = this.resolveFile(name);
|
|
495
|
+
if (!entry) {
|
|
496
|
+
throw new MpqNotFoundError(`File not found: ${name}`);
|
|
497
|
+
}
|
|
498
|
+
return new MpqFile(this.stream, this.archiveOffset, entry, this.sectorSize);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Extract a file's contents directly.
|
|
502
|
+
*
|
|
503
|
+
* @param name - File path within the archive
|
|
504
|
+
* @returns Buffer with the file contents
|
|
505
|
+
*/
|
|
506
|
+
extractFile(name) {
|
|
507
|
+
const file = this.openFile(name);
|
|
508
|
+
try {
|
|
509
|
+
return file.read();
|
|
510
|
+
} finally {
|
|
511
|
+
file.close();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Get the list of all known file names (from listfile).
|
|
516
|
+
*/
|
|
517
|
+
getFileList() {
|
|
518
|
+
this.ensureOpen();
|
|
519
|
+
return this.fileEntries.filter((e) => e.fileName && e.flags & MPQ_FILE_EXISTS).map((e) => e.fileName);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Find files matching a wildcard pattern.
|
|
523
|
+
*
|
|
524
|
+
* @param mask - Wildcard pattern (supports '*' and '?')
|
|
525
|
+
* @returns Array of matching file entries
|
|
526
|
+
*/
|
|
527
|
+
findFiles(mask = "*") {
|
|
528
|
+
this.ensureOpen();
|
|
529
|
+
const regex = wildcardToRegex(mask);
|
|
530
|
+
const results = [];
|
|
531
|
+
for (let i = 0; i < this.fileEntries.length; i++) {
|
|
532
|
+
const entry = this.fileEntries[i];
|
|
533
|
+
if (!(entry.flags & MPQ_FILE_EXISTS)) continue;
|
|
534
|
+
if (!entry.fileName) continue;
|
|
535
|
+
if (regex.test(entry.fileName)) {
|
|
536
|
+
const plainName = entry.fileName.replace(/^.*[\\/]/, "");
|
|
537
|
+
results.push({
|
|
538
|
+
fileName: entry.fileName,
|
|
539
|
+
plainName,
|
|
540
|
+
hashIndex: 0,
|
|
541
|
+
blockIndex: i,
|
|
542
|
+
fileSize: entry.fileSize,
|
|
543
|
+
compSize: entry.cmpSize,
|
|
544
|
+
fileFlags: entry.flags,
|
|
545
|
+
locale: 0
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return results;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Apply an external list of file names to resolve unnamed entries.
|
|
553
|
+
* Equivalent to providing an external listfile to SFileFindFirstFile.
|
|
554
|
+
*
|
|
555
|
+
* @param names - Array of file names to try
|
|
556
|
+
* @returns Number of newly resolved names
|
|
557
|
+
*/
|
|
558
|
+
addListfile(names) {
|
|
559
|
+
this.ensureOpen();
|
|
560
|
+
const before = this.fileEntries.filter((e) => e.fileName).length;
|
|
561
|
+
applyFileNames(this.fileEntries, this.hashTable, names);
|
|
562
|
+
return this.fileEntries.filter((e) => e.fileName).length - before;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Enumerate all existing file entries, including those without names.
|
|
566
|
+
* Unnamed entries get a synthetic name like "File00001234.xxx".
|
|
567
|
+
*/
|
|
568
|
+
enumerateFiles() {
|
|
569
|
+
this.ensureOpen();
|
|
570
|
+
const results = [];
|
|
571
|
+
for (let i = 0; i < this.fileEntries.length; i++) {
|
|
572
|
+
const entry = this.fileEntries[i];
|
|
573
|
+
if (!(entry.flags & MPQ_FILE_EXISTS)) continue;
|
|
574
|
+
const fileName = entry.fileName || `File${String(i).padStart(8, "0")}.xxx`;
|
|
575
|
+
const plainName = fileName.replace(/^.*[\\/]/, "");
|
|
576
|
+
results.push({
|
|
577
|
+
fileName,
|
|
578
|
+
plainName,
|
|
579
|
+
hashIndex: 0,
|
|
580
|
+
blockIndex: i,
|
|
581
|
+
fileSize: entry.fileSize,
|
|
582
|
+
compSize: entry.cmpSize,
|
|
583
|
+
fileFlags: entry.flags,
|
|
584
|
+
locale: 0
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return results;
|
|
588
|
+
}
|
|
589
|
+
/** Get the archive header */
|
|
590
|
+
getHeader() {
|
|
591
|
+
return this.header;
|
|
592
|
+
}
|
|
593
|
+
/** Get the number of file entries */
|
|
594
|
+
get fileCount() {
|
|
595
|
+
return this.fileEntries.length;
|
|
596
|
+
}
|
|
597
|
+
/** Get the sector size */
|
|
598
|
+
getSectorSize() {
|
|
599
|
+
return this.sectorSize;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Close the archive and release resources.
|
|
603
|
+
*/
|
|
604
|
+
close() {
|
|
605
|
+
if (!this.closed) {
|
|
606
|
+
this.stream.close();
|
|
607
|
+
this.closed = true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Resolve a file name to its FileEntry.
|
|
612
|
+
*/
|
|
613
|
+
resolveFile(name) {
|
|
614
|
+
if (this.hetTable && this.betTable) {
|
|
615
|
+
const betIndex = findInHetTable(this.hetTable, name);
|
|
616
|
+
if (betIndex >= 0 && betIndex < this.fileEntries.length) {
|
|
617
|
+
const entry2 = this.fileEntries[betIndex];
|
|
618
|
+
if (entry2.flags & MPQ_FILE_EXISTS) {
|
|
619
|
+
if (!entry2.fileName) entry2.fileName = name;
|
|
620
|
+
return entry2;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const hashEntry = findHashEntry(this.hashTable, name, 0, this.fileEntries.length);
|
|
625
|
+
if (!hashEntry) return null;
|
|
626
|
+
const blockIndex = hashEntry.dwBlockIndex & BLOCK_INDEX_MASK;
|
|
627
|
+
if (blockIndex >= this.fileEntries.length) return null;
|
|
628
|
+
const entry = this.fileEntries[blockIndex];
|
|
629
|
+
if (!(entry.flags & MPQ_FILE_EXISTS)) return null;
|
|
630
|
+
if (!entry.fileName) {
|
|
631
|
+
entry.fileName = name;
|
|
632
|
+
}
|
|
633
|
+
return entry;
|
|
634
|
+
}
|
|
635
|
+
ensureOpen() {
|
|
636
|
+
if (this.closed) {
|
|
637
|
+
throw new MpqError("Archive is closed");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
function wildcardToRegex(pattern) {
|
|
642
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
643
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/index.browser.ts
|
|
647
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.Buffer === "undefined") {
|
|
648
|
+
globalThis.Buffer = BufferPolyfill;
|
|
649
|
+
}
|
|
650
|
+
export {
|
|
651
|
+
ATTRIBUTES_NAME,
|
|
652
|
+
ID_MPQ,
|
|
653
|
+
ID_MPQ_USERDATA,
|
|
654
|
+
LISTFILE_NAME,
|
|
655
|
+
MPQ_COMPRESSION_ADPCM_MONO,
|
|
656
|
+
MPQ_COMPRESSION_ADPCM_STEREO,
|
|
657
|
+
MPQ_COMPRESSION_BZIP2,
|
|
658
|
+
MPQ_COMPRESSION_HUFFMANN,
|
|
659
|
+
MPQ_COMPRESSION_LZMA,
|
|
660
|
+
MPQ_COMPRESSION_PKWARE,
|
|
661
|
+
MPQ_COMPRESSION_SPARSE,
|
|
662
|
+
MPQ_COMPRESSION_ZLIB,
|
|
663
|
+
MPQ_FILE_COMPRESS,
|
|
664
|
+
MPQ_FILE_ENCRYPTED,
|
|
665
|
+
MPQ_FILE_EXISTS,
|
|
666
|
+
MPQ_FILE_IMPLODE,
|
|
667
|
+
MPQ_FILE_KEY_V2,
|
|
668
|
+
MPQ_FILE_SINGLE_UNIT,
|
|
669
|
+
MPQ_FORMAT_VERSION_1,
|
|
670
|
+
MPQ_FORMAT_VERSION_2,
|
|
671
|
+
MPQ_FORMAT_VERSION_3,
|
|
672
|
+
MPQ_FORMAT_VERSION_4,
|
|
673
|
+
MpqArchive,
|
|
674
|
+
MpqCompressionError,
|
|
675
|
+
MpqCorruptError,
|
|
676
|
+
MpqEncryptionError,
|
|
677
|
+
MpqError,
|
|
678
|
+
MpqFile,
|
|
679
|
+
MpqNotFoundError,
|
|
680
|
+
MpqUnsupportedError,
|
|
681
|
+
SIGNATURE_NAME,
|
|
682
|
+
decryptBlock,
|
|
683
|
+
decryptFileKey,
|
|
684
|
+
encryptBlock,
|
|
685
|
+
getStormBuffer,
|
|
686
|
+
hashFileKey,
|
|
687
|
+
hashNameA,
|
|
688
|
+
hashNameB,
|
|
689
|
+
hashString,
|
|
690
|
+
hashTableIndex,
|
|
691
|
+
jenkinsHash
|
|
692
|
+
};
|
|
693
|
+
//# sourceMappingURL=index.browser.mjs.map
|