@rhyster/wow-casc-dbc 2.7.6 → 2.8.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.
@@ -0,0 +1,19 @@
1
+ interface InstallTag {
2
+ name: string;
3
+ type: number;
4
+ files: boolean[];
5
+ }
6
+ interface InstallFile {
7
+ name: string;
8
+ hash: string;
9
+ size: number;
10
+ tags: InstallTag[];
11
+ }
12
+ interface InstallData {
13
+ tags: InstallTag[];
14
+ files: InstallFile[];
15
+ }
16
+ declare const parseInstallFile: (inputBuffer: Buffer, eKey: string, cKey: string) => InstallData;
17
+ export default parseInstallFile;
18
+ export type { InstallFile, InstallData };
19
+ //# sourceMappingURL=installFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installFile.d.ts","sourceRoot":"","sources":["../../src/parsers/installFile.ts"],"names":[],"mappings":"AAcA,UAAU,UAAU;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,EAAE,CAAC;CACpB;AAED,UAAU,WAAW;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,UAAU,WAAW;IACjB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,KAAK,EAAE,WAAW,EAAE,CAAC;CACxB;AAED,QAAA,MAAM,gBAAgB,gBAAiB,MAAM,QAAQ,MAAM,QAAQ,MAAM,KAAG,WA4E3E,CAAC;AAEF,eAAe,gBAAgB,CAAC;AAEhC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhyster/wow-casc-dbc",
3
- "version": "2.7.6",
3
+ "version": "2.8.1",
4
4
  "description": "Fetch World of Warcraft data files from CASC and parse DBC/DB2 files.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -38,10 +38,10 @@
38
38
  "author": "Rhythm",
39
39
  "license": "MIT",
40
40
  "devDependencies": {
41
- "@rhyster/eslint-config": "^1.6.12",
41
+ "@rhyster/eslint-config": "^1.6.13",
42
42
  "@types/async": "^3.2.24",
43
43
  "@types/cli-progress": "^3.11.6",
44
- "@types/node": "^22.10.2",
44
+ "@types/node": "^22.10.3",
45
45
  "eslint": "^9.17.0",
46
46
  "tsx": "^4.19.2",
47
47
  "typescript": "^5.7.2",
package/src/client.ts CHANGED
@@ -15,6 +15,7 @@ import getNameHash from './jenkins96.ts';
15
15
  import parseArchiveIndex from './parsers/archiveIndex.ts';
16
16
  import { parseCDNConfig, parseBuildConfig } from './parsers/config.ts';
17
17
  import parseEncodingFile from './parsers/encodingFile.ts';
18
+ import parseInstallFile from './parsers/installFile.ts';
18
19
  import { parseProductVersions, parseProductCDNs } from './parsers/productConfig.ts';
19
20
  import parseRootFile, { LocaleFlags, ContentFlags } from './parsers/rootFile.ts';
20
21
  import { resolveCDNHost, formatFileSize } from './utils.ts';
@@ -24,6 +25,7 @@ import type ADBReader from './adb.ts';
24
25
  import type { MissingKeyBlock } from './blte.ts';
25
26
  import type { ArchiveIndex } from './parsers/archiveIndex.ts';
26
27
  import type { EncodingData } from './parsers/encodingFile.ts';
28
+ import type { InstallFile, InstallData } from './parsers/installFile.ts';
27
29
  import type { Version } from './parsers/productConfig.ts';
28
30
  import type { FileInfo, RootData } from './parsers/rootFile.ts';
29
31
 
@@ -32,6 +34,7 @@ interface ClientPreloadData {
32
34
  archives: Map<string, ArchiveIndex>,
33
35
  encoding: EncodingData,
34
36
  rootFile: RootData,
37
+ install: InstallData,
35
38
  }
36
39
 
37
40
  interface FileFetchResultFull {
@@ -196,11 +199,23 @@ export default class CASCClient {
196
199
  const encoding = parseEncodingFile(encodingBuffer, encodingEKey, encodingCKey);
197
200
  this.log(LogLevel.info, `Parsed encoding table (${encoding.cKey2EKey.size.toString()} entries)`);
198
201
 
202
+ const getBuildConfigKeys = (configText: string): [string, string] => {
203
+ if (configText.includes(' ')) {
204
+ const [cKey, eKey] = configText.split(' ');
205
+ return [cKey, eKey];
206
+ }
207
+
208
+ const cKey = configText;
209
+ const eKeys = encoding.cKey2EKey.get(cKey);
210
+ assert(eKeys !== undefined, `Failing to find encoding key for ${cKey}`);
211
+
212
+ const eKey = typeof eKeys === 'string' ? eKeys : eKeys[0];
213
+
214
+ return [cKey, eKey];
215
+ };
216
+
199
217
  this.log(LogLevel.info, 'Loading root table...');
200
- const rootCKey = buildConfig.root;
201
- const rootEKeys = encoding.cKey2EKey.get(rootCKey);
202
- assert(rootEKeys !== undefined, 'Failing to find EKey for root table.');
203
- const rootEKey = typeof rootEKeys === 'string' ? rootEKeys : rootEKeys[0];
218
+ const [rootCKey, rootEKey] = getBuildConfigKeys(buildConfig.root);
204
219
  const rootBuffer = await getDataFile(prefixes, rootEKey, 'build', this.version.BuildConfig, {
205
220
  name: 'root',
206
221
  showProgress: this.logLevel >= LogLevel.info,
@@ -212,11 +227,25 @@ export default class CASCClient {
212
227
  const rootFile = parseRootFile(rootBuffer, rootEKey, rootCKey);
213
228
  this.log(LogLevel.info, `Parsed root file (${rootFile.fileDataID2CKey.size.toString()} entries, ${rootFile.nameHash2FileDataID.size.toString()} hashes)`);
214
229
 
230
+ this.log(LogLevel.info, 'Loading install manifest...');
231
+ const [installCKey, installEKey] = getBuildConfigKeys(buildConfig.install);
232
+ const installBuffer = await getDataFile(prefixes, installEKey, 'build', this.version.BuildConfig, {
233
+ name: 'install',
234
+ showProgress: this.logLevel >= LogLevel.info,
235
+ showAttemptFail: this.logLevel >= LogLevel.warn,
236
+ });
237
+ this.log(LogLevel.info, `Loaded install manifest (${formatFileSize(installBuffer.byteLength)})`);
238
+
239
+ this.log(LogLevel.info, 'Parsing install manifest...');
240
+ const install = parseInstallFile(installBuffer, installEKey, installCKey);
241
+ this.log(LogLevel.info, `Parsed install manifest (${install.tags.length.toString()} tags, ${install.files.length.toString()} files)`);
242
+
215
243
  this.preload = {
216
244
  prefixes,
217
245
  archives,
218
246
  encoding,
219
247
  rootFile,
248
+ install,
220
249
  };
221
250
  }
222
251
 
@@ -337,6 +366,14 @@ export default class CASCClient {
337
366
  return rootFile.fileDataID2CKey.get(fileDataID);
338
367
  }
339
368
 
369
+ getContentKeysFromInstall(name: string): InstallFile[] | undefined {
370
+ assert(this.preload, 'Client not initialized');
371
+
372
+ const { install } = this.preload;
373
+
374
+ return install.files.filter((file) => file.name === name);
375
+ }
376
+
340
377
  async getFileByContentKey(cKey: string, allowMissingKey?: false): Promise<FileFetchResultFull>;
341
378
  async getFileByContentKey(cKey: string, allowMissingKey: true): Promise<FileFetchResult>;
342
379
  async getFileByContentKey(cKey: string, allowMissingKey = false): Promise<FileFetchResult> {
@@ -402,6 +439,8 @@ export type {
402
439
  ClientPreloadData,
403
440
  ArchiveIndex,
404
441
  EncodingData,
442
+ InstallFile,
443
+ InstallData,
405
444
  RootData,
406
445
  FileInfo,
407
446
  FileFetchResultFull,
package/src/dbd.ts CHANGED
@@ -135,8 +135,8 @@ export default class DBDParser {
135
135
  const versionChunk = chunks.find((chunk) => chunk.find((line) => {
136
136
  const layoutsMatch = PATTERN_LAYOUT.exec(line);
137
137
  const layouts = layoutsMatch?.[1].split(',').map((v) => v.trim().toLowerCase());
138
- return layouts?.includes(layoutHashHex);
139
- }));
138
+ return layouts?.includes(layoutHashHex) === true;
139
+ }) !== undefined);
140
140
 
141
141
  assert(versionChunk, `No version definition found for layout hash ${layoutHashHex}`);
142
142
 
package/src/index.ts CHANGED
@@ -11,6 +11,8 @@ export type {
11
11
  ClientPreloadData,
12
12
  ArchiveIndex,
13
13
  EncodingData,
14
+ InstallFile,
15
+ InstallData,
14
16
  RootData,
15
17
  FileInfo,
16
18
  FileFetchResultFull,
@@ -0,0 +1,113 @@
1
+ import assert from 'node:assert';
2
+ import crypto from 'node:crypto';
3
+
4
+ import BLTEReader from '../blte.ts';
5
+
6
+ const INSTALL_MAGIC = 0x494e;
7
+
8
+ const MAGIC_OFFSET = 0;
9
+ const VERSION_OFFSET = 2;
10
+ const HASH_SIZE_OFFSET = 3;
11
+ const NUM_TAGS_OFFSET = 4;
12
+ const NUM_ENTRIES_OFFSET = 6;
13
+ const TAGS_OFFSET = 10;
14
+
15
+ interface InstallTag {
16
+ name: string,
17
+ type: number,
18
+ files: boolean[],
19
+ }
20
+
21
+ interface InstallFile {
22
+ name: string,
23
+ hash: string,
24
+ size: number,
25
+ tags: InstallTag[],
26
+ }
27
+
28
+ interface InstallData {
29
+ tags: InstallTag[],
30
+ files: InstallFile[],
31
+ }
32
+
33
+ const parseInstallFile = (inputBuffer: Buffer, eKey: string, cKey: string): InstallData => {
34
+ const reader = new BLTEReader(inputBuffer, eKey);
35
+ reader.processBytes();
36
+
37
+ const { buffer } = reader;
38
+
39
+ const installHash = crypto.createHash('md5').update(buffer).digest('hex');
40
+ assert(installHash === cKey, `Invalid root hash: expected ${cKey}, got ${installHash}`);
41
+
42
+ const magic = buffer.readUInt16BE(MAGIC_OFFSET);
43
+ assert(magic === INSTALL_MAGIC, `Invalid install magic: ${magic.toString(16).padStart(4, '0')}`);
44
+
45
+ const version = buffer.readUInt8(VERSION_OFFSET);
46
+ const hashSize = buffer.readUInt8(HASH_SIZE_OFFSET);
47
+ const numTags = buffer.readUInt16BE(NUM_TAGS_OFFSET);
48
+ const numEntries = buffer.readUInt32BE(NUM_ENTRIES_OFFSET);
49
+
50
+ assert(version === 1, `Invalid install version: ${version.toString()}`);
51
+
52
+ let pointer = TAGS_OFFSET;
53
+
54
+ const tags: InstallTag[] = [];
55
+ for (let i = 0; i < numTags; i += 1) {
56
+ const startOffset = pointer;
57
+ while (buffer[pointer] !== 0x00) {
58
+ pointer += 1;
59
+ }
60
+
61
+ const name = buffer.toString('utf-8', startOffset, pointer);
62
+ pointer += 1;
63
+
64
+ const type = buffer.readUInt16BE(pointer);
65
+ pointer += 2;
66
+
67
+ const files = [];
68
+ const finalOffset = pointer + Math.ceil(numEntries / 8);
69
+ while (pointer < finalOffset) {
70
+ const byte = buffer.readUInt8(pointer);
71
+ pointer += 1;
72
+
73
+ for (let j = 7; j >= 0; j -= 1) {
74
+ // eslint-disable-next-line no-bitwise
75
+ files.push((byte & (1 << j)) > 0);
76
+ }
77
+ }
78
+
79
+ tags.push({ name, type, files });
80
+ }
81
+
82
+ const files: InstallFile[] = [];
83
+ for (let i = 0; i < numEntries; i += 1) {
84
+ const startOffset = pointer;
85
+ while (buffer[pointer] !== 0x00) {
86
+ pointer += 1;
87
+ }
88
+
89
+ const name = buffer.toString('utf-8', startOffset, pointer);
90
+ pointer += 1;
91
+
92
+ const hash = buffer.toString('hex', pointer, pointer + hashSize);
93
+ pointer += hashSize;
94
+
95
+ const size = buffer.readUInt32BE(pointer);
96
+ pointer += 4;
97
+
98
+ const fileTags = tags.filter((tag) => tag.files[i]);
99
+
100
+ files.push({
101
+ name,
102
+ hash,
103
+ size,
104
+ tags: fileTags,
105
+ });
106
+ }
107
+
108
+ return { tags, files };
109
+ };
110
+
111
+ export default parseInstallFile;
112
+
113
+ export type { InstallFile, InstallData };