wowdump 0.0.0 → 0.2.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/adapters.js +101 -0
  4. package/dist/agent.js +1335 -0
  5. package/dist/analysis-path.js +38 -0
  6. package/dist/analysis-process-log.js +146 -0
  7. package/dist/broker-client.js +411 -0
  8. package/dist/broker-codec.js +148 -0
  9. package/dist/broker-core.js +1045 -0
  10. package/dist/broker-gateway.js +447 -0
  11. package/dist/broker-ledger.js +196 -0
  12. package/dist/broker-main.js +291 -0
  13. package/dist/broker-protocol.js +119 -0
  14. package/dist/broker-runtime.js +1283 -0
  15. package/dist/broker-server.js +466 -0
  16. package/dist/build-bundle-loader.js +183 -0
  17. package/dist/build-bundle.js +11 -0
  18. package/dist/discovery.js +59 -0
  19. package/dist/dry-run.js +38 -0
  20. package/dist/error-log.js +71 -0
  21. package/dist/focus-errors.js +63 -0
  22. package/dist/focus-service.js +1855 -0
  23. package/dist/focused-session.js +1357 -0
  24. package/dist/frida-runtime.js +711 -0
  25. package/dist/mcp-main.js +51 -0
  26. package/dist/mcp.js +924 -0
  27. package/dist/observability.js +41 -0
  28. package/dist/process-log-lock.js +195 -0
  29. package/dist/processes.js +47 -0
  30. package/dist/runtime-config.js +399 -0
  31. package/dist/session.js +145 -0
  32. package/dist/storage.js +12 -0
  33. package/dist/types.js +26 -0
  34. package/dist/wow-analysis.js +1430 -0
  35. package/package.json +64 -13
  36. package/resources/builds/retail/12.0.7.68974/build-profile.json +290 -0
  37. package/resources/builds/retail/12.0.7.68974/data-sources.json +1633 -0
  38. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +5130 -0
  39. package/resources/builds/retail/12.0.7.68974/manifest.json +63 -0
  40. package/resources/builds/retail/12.0.7.68974/signatures.json +260 -0
  41. package/index.js +0 -1
@@ -0,0 +1,41 @@
1
+ export const OBSERVATION_TIERS = [
2
+ "error_text",
3
+ "source_location",
4
+ "stack_trace",
5
+ "vm_metadata"
6
+ ];
7
+ export const OBSERVATION_TIER_DEFINITIONS = Object.freeze([
8
+ Object.freeze({
9
+ tier: "error_text",
10
+ capability: "errorMessage",
11
+ description: "Read the existing error text at a verified error boundary"
12
+ }),
13
+ Object.freeze({
14
+ tier: "source_location",
15
+ capability: "sourceLocation",
16
+ description: "Read source and line metadata when its layout is proven"
17
+ }),
18
+ Object.freeze({
19
+ tier: "stack_trace",
20
+ capability: "stackTrace",
21
+ description: "Read an existing stack representation with bounded traversal"
22
+ }),
23
+ Object.freeze({
24
+ tier: "vm_metadata",
25
+ capability: "vmMetadata",
26
+ description: "Read build-specific VM metadata only after an exact proof"
27
+ })
28
+ ]);
29
+ export function getObservationTiers(adapter) {
30
+ if (!adapter)
31
+ return Object.freeze([]);
32
+ return Object.freeze(OBSERVATION_TIER_DEFINITIONS
33
+ .filter(definition => adapter.capabilities[definition.capability])
34
+ .map(definition => definition.tier));
35
+ }
36
+ export function createObservationPolicy(adapter) {
37
+ return Object.freeze({
38
+ semanticReadOnly: true,
39
+ tiers: getObservationTiers(adapter)
40
+ });
41
+ }
@@ -0,0 +1,195 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
3
+ const DEFAULT_RETRY_DELAY_MS = 20;
4
+ const DEFAULT_STALE_AFTER_MS = 2_000;
5
+ const DEFAULT_TRANSIENT_RETRIES = 50;
6
+ const defaultFileSystem = { mkdir, readFile, readdir, rename, rm, stat, utimes, writeFile };
7
+ export async function acquireProcessLogLock(file, options = {}) {
8
+ const fileSystem = { ...defaultFileSystem, ...options.fileSystem };
9
+ const now = options.now ?? Date.now;
10
+ const sleep = options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
11
+ const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
12
+ const staleAfterMs = Math.max(options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS, 2_000);
13
+ const transientRetries = options.transientRetries ?? DEFAULT_TRANSIENT_RETRIES;
14
+ const operationTimeoutMs = options.timeoutMs ?? 10_000;
15
+ const platform = options.platform ?? process.platform;
16
+ const ownerPid = options.ownerPid ?? process.pid;
17
+ const token = (options.tokenFactory ?? randomUUID)();
18
+ const tokenHash = createHash("sha256").update(token).digest("hex");
19
+ const ownerName = `owner.${tokenHash}.json`;
20
+ const ownerPath = `${file}/${ownerName}`;
21
+ const candidate = `${file}.candidate-${ownerPid}-${tokenHash}`;
22
+ const deadline = now() + operationTimeoutMs;
23
+ const isProcessAlive = options.isProcessAlive ?? processIsAlive;
24
+ while (now() <= deadline) {
25
+ if (await publishCandidate(file, candidate, ownerName, ownerPid, token, fileSystem, now, platform)) {
26
+ const heartbeat = setInterval(() => { void touch(ownerPath, fileSystem, now); }, Math.max(1_000, Math.floor(staleAfterMs / 2)));
27
+ heartbeat.unref?.();
28
+ let released = false;
29
+ return async () => {
30
+ if (released)
31
+ return;
32
+ released = true;
33
+ clearInterval(heartbeat);
34
+ const claimName = `claim.${tokenHash}.json`;
35
+ const releaseDeadline = now() + operationTimeoutMs;
36
+ try {
37
+ await renameWithTransientWindowsRetry(ownerPath, `${file}/${claimName}`, fileSystem, platform, retryDelayMs, transientRetries, releaseDeadline, now, sleep);
38
+ }
39
+ catch (error) {
40
+ if (errorCode(error) === "ENOENT")
41
+ return;
42
+ throw error;
43
+ }
44
+ await quarantineClaimed(file, ownerPid, tokenHash, "release", fileSystem, platform, retryDelayMs, transientRetries, releaseDeadline, now, sleep);
45
+ };
46
+ }
47
+ const state = await inspectState(file, fileSystem);
48
+ if (state === null)
49
+ continue;
50
+ if (state.kind === "legacy") {
51
+ const claimed = now() - state.mtimeMs >= staleAfterMs
52
+ && await claimLegacy(file, ownerPid, tokenHash, fileSystem);
53
+ if (claimed) {
54
+ await quarantineClaimed(file, ownerPid, tokenHash, "legacy", fileSystem, platform, retryDelayMs, transientRetries, deadline, now, sleep);
55
+ continue;
56
+ }
57
+ }
58
+ else {
59
+ const stale = now() - state.mtimeMs >= staleAfterMs;
60
+ const reclaimable = (state.kind === "owner" ? stale : true) && !isProcessAlive(state.pid);
61
+ if (reclaimable) {
62
+ const claimName = `claim.${tokenHash}.json`;
63
+ try {
64
+ await renameWithTransientWindowsRetry(`${file}/${state.name}`, `${file}/${claimName}`, fileSystem, platform, retryDelayMs, transientRetries, deadline, now, sleep);
65
+ await quarantineClaimed(file, ownerPid, tokenHash, "stale", fileSystem, platform, retryDelayMs, transientRetries, deadline, now, sleep);
66
+ continue;
67
+ }
68
+ catch (error) {
69
+ if (errorCode(error) !== "ENOENT")
70
+ throw error;
71
+ }
72
+ }
73
+ }
74
+ await sleep(retryDelayMs);
75
+ }
76
+ throw new Error(`analysis process log lock timed out: ${file}`);
77
+ }
78
+ async function publishCandidate(file, candidate, ownerName, ownerPid, token, fileSystem, now, platform) {
79
+ await fileSystem.rm(candidate, { recursive: true, force: true });
80
+ await fileSystem.mkdir(candidate);
81
+ const ownerPath = `${candidate}/${ownerName}`;
82
+ try {
83
+ await fileSystem.writeFile(ownerPath, JSON.stringify({ kind: "owner", pid: ownerPid, token, acquiredAt: new Date(now()).toISOString() }), "utf8");
84
+ await touch(ownerPath, fileSystem, now);
85
+ await fileSystem.rename(candidate, file);
86
+ return true;
87
+ }
88
+ catch (error) {
89
+ await fileSystem.rm(candidate, { recursive: true, force: true });
90
+ const code = errorCode(error);
91
+ if (code === "EEXIST" || code === "ENOTEMPTY" || isTransientWindowsLockError(error, platform))
92
+ return false;
93
+ throw error;
94
+ }
95
+ }
96
+ async function inspectState(file, fileSystem) {
97
+ let names;
98
+ try {
99
+ names = await fileSystem.readdir(file);
100
+ }
101
+ catch (error) {
102
+ if (errorCode(error) === "ENOENT")
103
+ return null;
104
+ throw error;
105
+ }
106
+ const name = names.find(value => /^(owner|released|claim)\.[a-f0-9]{64}\.json$/.test(value)) ?? (names.includes("legacy-claim.json") ? "legacy-claim.json" : null);
107
+ if (!name)
108
+ return legacyState(file, fileSystem);
109
+ try {
110
+ const value = JSON.parse(await fileSystem.readFile(`${file}/${name}`, "utf8"));
111
+ const details = await fileSystem.stat(`${file}/${name}`);
112
+ if ((value.kind === "owner" || value.kind === "claim") && Number.isSafeInteger(value.pid)) {
113
+ return { name, kind: value.kind, pid: Number(value.pid), mtimeMs: details.mtimeMs };
114
+ }
115
+ return legacyState(file, fileSystem);
116
+ }
117
+ catch (error) {
118
+ if (errorCode(error) === "ENOENT")
119
+ return null;
120
+ if (error instanceof SyntaxError)
121
+ return legacyState(file, fileSystem);
122
+ throw error;
123
+ }
124
+ }
125
+ async function legacyState(file, fileSystem) {
126
+ try {
127
+ return { name: "legacy-claim.json", kind: "legacy", pid: 0, mtimeMs: (await fileSystem.stat(file)).mtimeMs };
128
+ }
129
+ catch (error) {
130
+ if (errorCode(error) === "ENOENT")
131
+ return null;
132
+ throw error;
133
+ }
134
+ }
135
+ async function claimLegacy(file, ownerPid, tokenHash, fileSystem) {
136
+ try {
137
+ await fileSystem.writeFile(`${file}/legacy-claim.json`, JSON.stringify({ kind: "claim", pid: ownerPid, tokenHash }), { encoding: "utf8", flag: "wx" });
138
+ return true;
139
+ }
140
+ catch (error) {
141
+ if (errorCode(error) === "EEXIST" || errorCode(error) === "ENOENT")
142
+ return false;
143
+ throw error;
144
+ }
145
+ }
146
+ async function quarantineClaimed(file, ownerPid, tokenHash, reason, fileSystem, platform, retryDelayMs, transientRetries, deadline, now, sleep) {
147
+ const quarantine = `${file}.${reason}-${ownerPid}-${tokenHash}`;
148
+ try {
149
+ await renameWithTransientWindowsRetry(file, quarantine, fileSystem, platform, retryDelayMs, transientRetries, deadline, now, sleep);
150
+ }
151
+ catch (error) {
152
+ if (errorCode(error) === "ENOENT")
153
+ return;
154
+ throw error;
155
+ }
156
+ await fileSystem.rm(quarantine, { recursive: true, force: true });
157
+ }
158
+ async function renameWithTransientWindowsRetry(oldPath, newPath, fileSystem, platform, retryDelayMs, transientRetries, deadline, now, sleep) {
159
+ let firstTransientError;
160
+ for (let attempt = 0;; attempt += 1) {
161
+ try {
162
+ await fileSystem.rename(oldPath, newPath);
163
+ return;
164
+ }
165
+ catch (error) {
166
+ if (!isTransientWindowsLockError(error, platform))
167
+ throw error;
168
+ firstTransientError ??= error;
169
+ if (attempt >= transientRetries || now() + retryDelayMs > deadline)
170
+ throw firstTransientError;
171
+ await sleep(retryDelayMs);
172
+ }
173
+ }
174
+ }
175
+ async function touch(path, fileSystem, now) {
176
+ const date = new Date(now());
177
+ await fileSystem.utimes(path, date, date).catch(error => { if (errorCode(error) !== "ENOENT")
178
+ throw error; });
179
+ }
180
+ function processIsAlive(pid) {
181
+ try {
182
+ process.kill(pid, 0);
183
+ return true;
184
+ }
185
+ catch (error) {
186
+ return errorCode(error) !== "ESRCH";
187
+ }
188
+ }
189
+ function errorCode(error) {
190
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : undefined;
191
+ }
192
+ export function isTransientWindowsLockError(error, platform = process.platform) {
193
+ const code = errorCode(error);
194
+ return platform === "win32" && (code === "EPERM" || code === "EACCES");
195
+ }
@@ -0,0 +1,47 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { basename, resolve } from "node:path";
4
+ const execFileAsync = promisify(execFile);
5
+ export function parseWowProcessRows(stdout, installs) {
6
+ if (!stdout.trim() || stdout.trim() === "null")
7
+ return [];
8
+ const decoded = JSON.parse(stdout);
9
+ const rows = (Array.isArray(decoded) ? decoded : [decoded]).filter((row) => typeof row === "object" && row !== null && !Array.isArray(row));
10
+ return rows.flatMap(row => {
11
+ const pid = Number(row.ProcessId);
12
+ if (!Number.isSafeInteger(pid) || pid <= 0)
13
+ return [];
14
+ const observedExecutable = String(row.ExecutablePath ?? "");
15
+ let executable = observedExecutable;
16
+ let install;
17
+ if (observedExecutable) {
18
+ if (basename(observedExecutable).toLowerCase() !== "wow.exe")
19
+ return [];
20
+ install = installs.find(candidate => resolve(candidate.executable).toLowerCase() === resolve(observedExecutable).toLowerCase());
21
+ }
22
+ else if (row.Name === "Wow.exe" && installs.length === 1) {
23
+ install = installs[0];
24
+ executable = install.executable;
25
+ }
26
+ if (!install)
27
+ return [];
28
+ return [{
29
+ pid,
30
+ executable,
31
+ install,
32
+ commandLine: String(row.CommandLine ?? ""),
33
+ ...(typeof row.StartTime === "string" && row.StartTime.length > 0
34
+ ? { startTime: row.StartTime }
35
+ : {})
36
+ }];
37
+ });
38
+ }
39
+ export async function listWowProcesses(installs) {
40
+ const { stdout } = await execFileAsync("powershell.exe", [
41
+ "-NoProfile",
42
+ "-NonInteractive",
43
+ "-Command",
44
+ "Get-CimInstance Win32_Process -Filter \"Name = 'Wow.exe'\" | Select-Object Name,ProcessId,ExecutablePath,CommandLine,@{Name='StartTime';Expression={$_.CreationDate.ToUniversalTime().ToString('o')}} | ConvertTo-Json -Compress"
45
+ ], { windowsHide: true });
46
+ return parseWowProcessRows(stdout, installs);
47
+ }
@@ -0,0 +1,399 @@
1
+ import { accessSync, existsSync, readFileSync, statSync } from "node:fs";
2
+ import { constants } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, delimiter, isAbsolute, join, normalize, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const STABLE_KEYS = new Set(["game_roots", "profile_root", "runtime_root", "log_level", "broker_idle_ms"]);
7
+ const RUNTIME_STATE_KEYS = /^(?:pid|module_?base|build|build_?key|session_?id|hook|frida|attach(?:ment)?(?:_state)?)$/i;
8
+ const DEFAULT_BROKER_IDLE_MS = 20 * 60 * 1000;
9
+ const MAX_BATTLENET_PRODUCT_DB_BYTES = 16 * 1024 * 1024;
10
+ const MAX_BATTLENET_STRING_BYTES = 32 * 1024;
11
+ export function parseRuntimeArguments(argv) {
12
+ const result = { gameRoots: [] };
13
+ for (let index = 0; index < argv.length; index += 1) {
14
+ const argument = argv[index];
15
+ if (argument === "--game-root" || argument === "--config") {
16
+ const value = argv[++index];
17
+ if (!value || value.startsWith("--"))
18
+ throw new Error(`${argument} requires a path`);
19
+ if (argument === "--game-root")
20
+ result.gameRoots.push(value);
21
+ else if (result.configPath)
22
+ throw new Error("--config may only be specified once");
23
+ else
24
+ result.configPath = value;
25
+ continue;
26
+ }
27
+ if (argument.startsWith("--game-root="))
28
+ result.gameRoots.push(requiredInlineValue(argument, "--game-root"));
29
+ else if (argument.startsWith("--config=")) {
30
+ if (result.configPath)
31
+ throw new Error("--config may only be specified once");
32
+ result.configPath = requiredInlineValue(argument, "--config");
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+ export function parseRuntimeConfigToml(text, source = "config.toml") {
38
+ const result = {};
39
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
40
+ let pending = "";
41
+ for (let lineNumber = 1; lineNumber <= lines.length; lineNumber += 1) {
42
+ pending = pending ? `${pending}\n${lines[lineNumber - 1]}` : lines[lineNumber - 1];
43
+ const line = stripTomlComment(pending).trim();
44
+ if (!line) {
45
+ pending = "";
46
+ continue;
47
+ }
48
+ if (line.startsWith("[") && !line.startsWith("[["))
49
+ throw new Error(`${source}:${lineNumber}: TOML tables are not supported`);
50
+ if (!isCompleteTomlValue(line))
51
+ continue;
52
+ pending = "";
53
+ const match = /^([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*([\s\S]+)$/.exec(line);
54
+ if (!match)
55
+ throw new Error(`${source}:${lineNumber}: invalid TOML assignment`);
56
+ const [, key, rawValue] = match;
57
+ if (!STABLE_KEYS.has(key)) {
58
+ const reason = RUNTIME_STATE_KEYS.test(key) ? "runtime state is not allowed" : "unknown setting";
59
+ throw new Error(`${source}:${lineNumber}: ${reason}: ${key}`);
60
+ }
61
+ if (key === "game_roots")
62
+ result.gameRoots = parseStringArray(rawValue, source, lineNumber);
63
+ else if (key === "profile_root")
64
+ result.profileRoot = parseTomlString(rawValue, source, lineNumber);
65
+ else if (key === "runtime_root")
66
+ result.runtimeRoot = parseTomlString(rawValue, source, lineNumber);
67
+ else if (key === "log_level")
68
+ result.logLevel = parseTomlString(rawValue, source, lineNumber);
69
+ else {
70
+ const value = Number(rawValue.replaceAll("_", ""));
71
+ if (!Number.isSafeInteger(value) || value < 0)
72
+ throw new Error(`${source}:${lineNumber}: broker_idle_ms must be a non-negative integer`);
73
+ result.brokerIdleMs = value;
74
+ }
75
+ }
76
+ if (pending.trim())
77
+ throw new Error(`${source}:${lines.length}: unterminated TOML value`);
78
+ return result;
79
+ }
80
+ export function resolveRuntimeConfig(options = {}) {
81
+ const argv = options.argv ?? process.argv.slice(2);
82
+ const env = options.env ?? process.env;
83
+ const cwd = options.cwd ?? process.cwd();
84
+ const packageRoot = resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), ".."));
85
+ const args = parseRuntimeArguments(argv);
86
+ const localAppData = resolve(env.LOCALAPPDATA || join(homedir(), "AppData", "Local"));
87
+ const userConfigPath = join(localAppData, "wowdump", "config.toml");
88
+ const diagnostics = [];
89
+ const explicitConfigPath = args.configPath ? normalizePath(args.configPath, cwd) : undefined;
90
+ const userConfig = readConfig(userConfigPath, false, diagnostics);
91
+ const explicitConfig = explicitConfigPath ? readConfig(explicitConfigPath, true, diagnostics) : undefined;
92
+ const profileSetting = explicitConfig?.profileRoot !== undefined
93
+ ? { value: explicitConfig.profileRoot, base: dirname(explicitConfigPath) }
94
+ : userConfig?.profileRoot !== undefined
95
+ ? { value: userConfig.profileRoot, base: dirname(userConfigPath) }
96
+ : undefined;
97
+ const runtimeSetting = explicitConfig?.runtimeRoot !== undefined
98
+ ? { value: explicitConfig.runtimeRoot, base: dirname(explicitConfigPath) }
99
+ : userConfig?.runtimeRoot !== undefined
100
+ ? { value: userConfig.runtimeRoot, base: dirname(userConfigPath) }
101
+ : undefined;
102
+ const candidates = [
103
+ { source: "cli", roots: args.gameRoots, base: cwd },
104
+ { source: "environment", roots: splitEnvironmentPaths(env.WOWDUMP_GAME_ROOTS), base: cwd },
105
+ { source: "explicit-config", roots: explicitConfig?.gameRoots ?? [], base: explicitConfigPath ? dirname(explicitConfigPath) : cwd },
106
+ { source: "user-config", roots: userConfig?.gameRoots ?? [], base: dirname(userConfigPath) },
107
+ { source: "deprecated-environment", roots: splitEnvironmentPaths(env.WOW_ROOT), base: cwd },
108
+ { source: "auto-detect", roots: (options.autoDetect ?? detectGameRoots)(env), base: cwd }
109
+ ];
110
+ const selected = candidates.find(candidate => candidate.roots.some(root => root.trim())) ?? { source: "none", roots: [], base: cwd };
111
+ if (selected.source === "deprecated-environment")
112
+ diagnostics.push({
113
+ level: "warning", code: "deprecated-wow-root", message: "WOW_ROOT is deprecated; use --game-root or WOWDUMP_GAME_ROOTS"
114
+ });
115
+ const gameRoots = normalizePaths(selected.roots, selected.base);
116
+ for (const path of gameRoots)
117
+ validateGameRoot(path, diagnostics);
118
+ return Object.freeze({
119
+ gameRoots: Object.freeze(gameRoots),
120
+ gameRootSource: selected.source,
121
+ configPath: explicitConfigPath,
122
+ userConfigPath,
123
+ profileRoot: profileSetting ? normalizePath(profileSetting.value, profileSetting.base) : join(packageRoot, "resources", "builds"),
124
+ runtimeRoot: runtimeSetting ? normalizePath(runtimeSetting.value, runtimeSetting.base) : join(localAppData, "wowdump"),
125
+ logLevel: explicitConfig?.logLevel ?? userConfig?.logLevel ?? "info",
126
+ brokerIdleMs: explicitConfig?.brokerIdleMs ?? userConfig?.brokerIdleMs ?? DEFAULT_BROKER_IDLE_MS,
127
+ diagnostics: Object.freeze(diagnostics)
128
+ });
129
+ }
130
+ export function detectGameRoots(env = process.env) {
131
+ const drives = [env.ProgramFiles, env["ProgramFiles(x86)"], env.PUBLIC && join(dirname(env.PUBLIC), "Program Files"), "C:/Program Files (x86)"]
132
+ .filter((value) => Boolean(value));
133
+ const candidates = drives.flatMap(root => [join(root, "World of Warcraft"), join(root, "World of Warcraft Beta")]);
134
+ const battleNetRoots = readBattleNetProductRoots(env).sort(comparePaths);
135
+ return normalizePaths([...battleNetRoots, ...candidates.filter(looksLikeGameRoot)], process.cwd());
136
+ }
137
+ function readBattleNetProductRoots(env) {
138
+ const programData = env.ProgramData || "C:/ProgramData";
139
+ const databasePath = join(programData, "Battle.net", "Agent", "product.db");
140
+ try {
141
+ const stat = statSync(databasePath);
142
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_BATTLENET_PRODUCT_DB_BYTES)
143
+ return [];
144
+ const database = readFileSync(databasePath);
145
+ const records = protobufFields(database);
146
+ if (!records)
147
+ return [];
148
+ const roots = [];
149
+ for (const record of records) {
150
+ if (record.number !== 1 || record.wire !== 2)
151
+ continue;
152
+ const productFields = protobufFields(record.value);
153
+ if (!productFields)
154
+ return [];
155
+ const uid = protobufString(lengthDelimitedValue(productFields, 1));
156
+ if (!uid || !/^wow(?:_|$)/i.test(uid))
157
+ continue;
158
+ for (const installField of productFields) {
159
+ if (installField.number !== 3 || installField.wire !== 2)
160
+ continue;
161
+ const installFields = protobufFields(installField.value);
162
+ if (!installFields)
163
+ return [];
164
+ const root = protobufString(lengthDelimitedValue(installFields, 1));
165
+ if (root && isAbsolute(root) && looksLikeGameRoot(root))
166
+ roots.push(root);
167
+ }
168
+ }
169
+ return roots;
170
+ }
171
+ catch {
172
+ return [];
173
+ }
174
+ }
175
+ function lengthDelimitedValue(fields, number) {
176
+ const field = fields.find(candidate => candidate.number === number && candidate.wire === 2);
177
+ return field?.wire === 2 ? field.value : undefined;
178
+ }
179
+ function protobufFields(buffer) {
180
+ const fields = [];
181
+ let offset = 0;
182
+ while (offset < buffer.length) {
183
+ const key = readProtobufVarint(buffer, offset);
184
+ if (!key || key.value === 0)
185
+ return undefined;
186
+ offset = key.next;
187
+ const number = Math.floor(key.value / 8);
188
+ const wire = key.value & 7;
189
+ if (number <= 0)
190
+ return undefined;
191
+ if (wire === 0) {
192
+ const value = readProtobufVarint(buffer, offset);
193
+ if (!value)
194
+ return undefined;
195
+ fields.push({ number, wire, value: value.value });
196
+ offset = value.next;
197
+ }
198
+ else if (wire === 1 || wire === 5) {
199
+ const length = wire === 1 ? 8 : 4;
200
+ if (offset + length > buffer.length)
201
+ return undefined;
202
+ fields.push({ number, wire, value: buffer.subarray(offset, offset + length) });
203
+ offset += length;
204
+ }
205
+ else if (wire === 2) {
206
+ const length = readProtobufVarint(buffer, offset);
207
+ if (!length || length.value > buffer.length - length.next)
208
+ return undefined;
209
+ offset = length.next;
210
+ fields.push({ number, wire, value: buffer.subarray(offset, offset + length.value) });
211
+ offset += length.value;
212
+ }
213
+ else {
214
+ return undefined;
215
+ }
216
+ }
217
+ return fields;
218
+ }
219
+ function readProtobufVarint(buffer, offset) {
220
+ let value = 0n;
221
+ for (let index = 0; index < 10 && offset + index < buffer.length; index += 1) {
222
+ const byte = buffer[offset + index];
223
+ value |= BigInt(byte & 0x7f) << BigInt(index * 7);
224
+ if ((byte & 0x80) === 0) {
225
+ if (value > BigInt(Number.MAX_SAFE_INTEGER))
226
+ return undefined;
227
+ return { value: Number(value), next: offset + index + 1 };
228
+ }
229
+ }
230
+ return undefined;
231
+ }
232
+ function protobufString(value) {
233
+ if (!value || value.length === 0 || value.length > MAX_BATTLENET_STRING_BYTES)
234
+ return undefined;
235
+ try {
236
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(value);
237
+ if (decoded.includes("\0"))
238
+ return undefined;
239
+ return decoded;
240
+ }
241
+ catch {
242
+ return undefined;
243
+ }
244
+ }
245
+ function comparePaths(left, right) {
246
+ return left.toLowerCase().localeCompare(right.toLowerCase(), "en") || left.localeCompare(right, "en");
247
+ }
248
+ function readConfig(path, required, diagnostics) {
249
+ try {
250
+ return parseRuntimeConfigToml(readFileSync(path, "utf8"), path);
251
+ }
252
+ catch (error) {
253
+ const code = error.code;
254
+ if (code === "ENOENT" && !required)
255
+ return undefined;
256
+ diagnostics.push({ level: "error", code: code === "ENOENT" ? "config-not-found" : "invalid-config", message: error instanceof Error ? error.message : String(error), path });
257
+ return undefined;
258
+ }
259
+ }
260
+ function validateGameRoot(path, diagnostics) {
261
+ try {
262
+ accessSync(path, constants.R_OK);
263
+ if (!statSync(path).isDirectory())
264
+ throw new Error("path is not a directory");
265
+ if (!looksLikeGameRoot(path))
266
+ diagnostics.push({ level: "warning", code: "unrecognized-game-root", message: `No .build.info or WoW installation was found at ${path}`, path });
267
+ }
268
+ catch (error) {
269
+ diagnostics.push({ level: "error", code: "unreadable-game-root", message: `Game root is not a readable directory: ${path} (${error instanceof Error ? error.message : String(error)})`, path });
270
+ }
271
+ }
272
+ function looksLikeGameRoot(path) {
273
+ if (!existsSync(path))
274
+ return false;
275
+ if (existsSync(join(path, ".build.info")))
276
+ return true;
277
+ for (const flavor of ["_retail_", "_classic_", "_classic_era_", "_beta_"])
278
+ if (existsSync(join(path, flavor, "Wow.exe")))
279
+ return true;
280
+ return false;
281
+ }
282
+ function splitEnvironmentPaths(value) {
283
+ if (!value?.trim())
284
+ return [];
285
+ const separator = value.includes(";") ? ";" : delimiter;
286
+ return value.split(separator).map(item => item.trim()).filter(Boolean);
287
+ }
288
+ function normalizePaths(paths, base) {
289
+ const seen = new Set();
290
+ const result = [];
291
+ for (const value of paths) {
292
+ if (!value.trim())
293
+ continue;
294
+ const path = normalizePath(value, base);
295
+ const key = process.platform === "win32" ? path.toLowerCase() : path;
296
+ if (!seen.has(key)) {
297
+ seen.add(key);
298
+ result.push(path);
299
+ }
300
+ }
301
+ return result;
302
+ }
303
+ function normalizePath(value, base) {
304
+ const expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
305
+ return normalize(isAbsolute(expanded) ? resolve(expanded) : resolve(base, expanded));
306
+ }
307
+ function requiredInlineValue(argument, name) {
308
+ const value = argument.slice(name.length + 1);
309
+ if (!value)
310
+ throw new Error(`${name} requires a path`);
311
+ return value;
312
+ }
313
+ function stripTomlComment(line) {
314
+ let quoted = false;
315
+ let escaped = false;
316
+ for (let index = 0; index < line.length; index += 1) {
317
+ const char = line[index];
318
+ if (escaped) {
319
+ escaped = false;
320
+ continue;
321
+ }
322
+ if (char === "\\" && quoted) {
323
+ escaped = true;
324
+ continue;
325
+ }
326
+ if (char === '"')
327
+ quoted = !quoted;
328
+ if (char === "#" && !quoted)
329
+ return line.slice(0, index);
330
+ }
331
+ return line;
332
+ }
333
+ function isCompleteTomlValue(line) {
334
+ let quoted = false;
335
+ let escaped = false;
336
+ let brackets = 0;
337
+ for (const char of line) {
338
+ if (escaped) {
339
+ escaped = false;
340
+ continue;
341
+ }
342
+ if (char === "\\" && quoted) {
343
+ escaped = true;
344
+ continue;
345
+ }
346
+ if (char === '"')
347
+ quoted = !quoted;
348
+ else if (!quoted && char === "[")
349
+ brackets += 1;
350
+ else if (!quoted && char === "]")
351
+ brackets -= 1;
352
+ }
353
+ return !quoted && brackets === 0;
354
+ }
355
+ function parseStringArray(raw, source, line) {
356
+ const value = raw.trim();
357
+ if (!value.startsWith("[") || !value.endsWith("]"))
358
+ throw new Error(`${source}:${line}: game_roots must be an array of strings`);
359
+ const inner = value.slice(1, -1).trim();
360
+ if (!inner)
361
+ return [];
362
+ const items = [];
363
+ let start = 0;
364
+ let quoted = false;
365
+ let escaped = false;
366
+ for (let index = 0; index <= inner.length; index += 1) {
367
+ const char = inner[index];
368
+ if (escaped) {
369
+ escaped = false;
370
+ continue;
371
+ }
372
+ if (char === "\\" && quoted) {
373
+ escaped = true;
374
+ continue;
375
+ }
376
+ if (char === '"')
377
+ quoted = !quoted;
378
+ if ((char === "," && !quoted) || index === inner.length) {
379
+ const item = inner.slice(start, index).trim();
380
+ if (item)
381
+ items.push(parseTomlString(item, source, line));
382
+ start = index + 1;
383
+ }
384
+ }
385
+ if (quoted)
386
+ throw new Error(`${source}:${line}: unterminated string`);
387
+ return items;
388
+ }
389
+ function parseTomlString(raw, source, line) {
390
+ const value = raw.trim();
391
+ if (!/^"(?:[^"\\]|\\.)*"$/.test(value))
392
+ throw new Error(`${source}:${line}: expected a basic TOML string`);
393
+ try {
394
+ return JSON.parse(value);
395
+ }
396
+ catch {
397
+ throw new Error(`${source}:${line}: invalid string escape`);
398
+ }
399
+ }