@adhisang/minecraft-modding-mcp 5.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/access-widener-parser.d.ts +1 -0
- package/dist/access-widener-parser.js +6 -3
- package/dist/cache-registry.js +35 -31
- package/dist/decompiler/vineflower.js +8 -2
- package/dist/entry-tools/analyze-mod-service.js +2 -1
- package/dist/entry-tools/analyze-symbol-service.js +18 -9
- package/dist/entry-tools/compare-minecraft-service.js +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
- package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
- package/dist/entry-tools/response-contract.d.ts +2 -0
- package/dist/entry-tools/response-contract.js +2 -2
- package/dist/entry-tools/validate-project-service.d.ts +4 -4
- package/dist/entry-tools/verify-mixin-target-service.js +7 -7
- package/dist/index.js +20 -16
- package/dist/java-process.js +1 -1
- package/dist/json-rpc-framing.d.ts +4 -0
- package/dist/json-rpc-framing.js +23 -1
- package/dist/mapping/internal-types.d.ts +17 -0
- package/dist/mapping/loaders/tiny-maven.js +46 -20
- package/dist/mapping/lookup.d.ts +12 -1
- package/dist/mapping/lookup.js +53 -1
- package/dist/mapping-service.js +36 -21
- package/dist/mixin/annotation-validators.js +21 -3
- package/dist/mixin/parsed-validator.js +1 -0
- package/dist/mixin-parser.js +71 -3
- package/dist/mod-decompile-service.d.ts +2 -0
- package/dist/mod-decompile-service.js +19 -2
- package/dist/mod-remap-service.js +6 -5
- package/dist/mojang-tiny-mapping-service.js +5 -2
- package/dist/nbt/java-nbt-codec.js +40 -16
- package/dist/nbt/json-patch.js +51 -12
- package/dist/nbt/typed-json.d.ts +1 -0
- package/dist/nbt/typed-json.js +12 -2
- package/dist/nbt/types.d.ts +2 -2
- package/dist/path-converter.js +10 -3
- package/dist/registry-service.d.ts +2 -0
- package/dist/registry-service.js +16 -1
- package/dist/repo-downloader.js +4 -3
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/class-source/snippet-builder.js +6 -0
- package/dist/source/class-source-helpers.d.ts +4 -4
- package/dist/source/class-source-helpers.js +12 -11
- package/dist/source/class-source.d.ts +19 -0
- package/dist/source/class-source.js +48 -21
- package/dist/source/file-access.js +3 -2
- package/dist/source/indexer.js +24 -7
- package/dist/source/lifecycle/mapping-helpers.js +28 -3
- package/dist/source/lifecycle/runtime-check.js +20 -6
- package/dist/source/search.js +23 -8
- package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
- package/dist/source/workspace-target.js +2 -1
- package/dist/source-resolver.js +2 -2
- package/dist/source-service.js +9 -1
- package/dist/stdio-supervisor.d.ts +2 -2
- package/dist/stdio-supervisor.js +29 -9
- package/dist/storage/db.js +24 -1
- package/dist/storage/files-repo.d.ts +3 -0
- package/dist/storage/files-repo.js +43 -37
- package/dist/storage/symbols-repo.d.ts +9 -0
- package/dist/storage/symbols-repo.js +47 -19
- package/dist/symbols/symbol-extractor.js +1 -1
- package/dist/tool-guidance.d.ts +2 -1
- package/dist/tool-guidance.js +26 -1
- package/dist/tool-schemas.d.ts +4 -4
- package/dist/tool-schemas.js +91 -91
- package/dist/warning-details.d.ts +12 -0
- package/dist/warning-details.js +17 -0
- package/dist/workspace-mapping-service.js +9 -0
- package/package.json +1 -1
package/dist/mixin-parser.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
/* ------------------------------------------------------------------ */
|
|
6
6
|
/* Regex patterns */
|
|
7
7
|
/* ------------------------------------------------------------------ */
|
|
8
|
-
const CLASS_DECL_RE =
|
|
8
|
+
const CLASS_DECL_RE = /^\s*(?:@[\w$.]+(?:\([^)]*\))?\s+)*(?:public\s+|final\s+|abstract\s+|sealed\s+)*\b(?:class|interface)\s+(\w+)/;
|
|
9
9
|
// import statements for FQCN resolution
|
|
10
10
|
const IMPORT_RE = /^\s*import\s+([\w.]+)\s*;/;
|
|
11
11
|
// @Mixin(Foo.class) or @Mixin({Foo.class, Bar.class}) or @Mixin(value = Foo.class)
|
|
12
12
|
// Also handles @Mixin(value = {Foo.class, Bar.class}, priority = 900)
|
|
13
13
|
const MIXIN_ANNOTATION_START_RE = /^\s*@Mixin\s*\(/;
|
|
14
|
-
|
|
14
|
+
// `$` is a legal Java identifier character (e.g. generated classes like My$Class);
|
|
15
|
+
// without it the capture would start mid-name and yield a wrong target.
|
|
16
|
+
const MIXIN_TARGET_RE = /([\w$][\w.$]*?)\.class/g;
|
|
15
17
|
const MIXIN_PRIORITY_RE = /priority\s*=\s*(\d+)/;
|
|
16
18
|
// String-form targets: @Mixin(targets = "pkg.Class") or @Mixin(targets = {"pkg.A", "pkg.B"})
|
|
17
19
|
const MIXIN_TARGETS_STRING_RE = /targets\s*=\s*(?:\{([^}]+)\}|"([^"]+)")/;
|
|
@@ -55,6 +57,62 @@ function collectMultilineAnnotation(lines, startIndex) {
|
|
|
55
57
|
}
|
|
56
58
|
return { text, endIndex: lines.length - 1 };
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Blank out `/* *\/` block comments and `//` line comments (outside string literals)
|
|
62
|
+
* while preserving line count and line numbers, so the annotation scans never match
|
|
63
|
+
* inside commented-out code.
|
|
64
|
+
*/
|
|
65
|
+
function stripComments(source) {
|
|
66
|
+
let out = "";
|
|
67
|
+
let inBlock = false;
|
|
68
|
+
let inString = false;
|
|
69
|
+
for (let i = 0; i < source.length; i++) {
|
|
70
|
+
const ch = source[i];
|
|
71
|
+
const next = source[i + 1];
|
|
72
|
+
if (inBlock) {
|
|
73
|
+
if (ch === "*" && next === "/") {
|
|
74
|
+
out += " ";
|
|
75
|
+
i++;
|
|
76
|
+
inBlock = false;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
out += ch === "\n" ? "\n" : " ";
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (inString) {
|
|
84
|
+
out += ch;
|
|
85
|
+
if (ch === "\\" && next !== undefined) {
|
|
86
|
+
out += next;
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
else if (ch === '"') {
|
|
90
|
+
inString = false;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch === '"') {
|
|
95
|
+
inString = true;
|
|
96
|
+
out += ch;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === "/" && next === "*") {
|
|
100
|
+
out += " ";
|
|
101
|
+
i++;
|
|
102
|
+
inBlock = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "/" && next === "/") {
|
|
106
|
+
while (i < source.length && source[i] !== "\n") {
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
i--;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
out += ch;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
58
116
|
/**
|
|
59
117
|
* Skip past annotations (including multi-line ones) starting at `startIndex`.
|
|
60
118
|
* Returns the index of the first non-annotation line.
|
|
@@ -169,7 +227,7 @@ function inferAccessorTarget(methodName) {
|
|
|
169
227
|
/* Main parser */
|
|
170
228
|
/* ------------------------------------------------------------------ */
|
|
171
229
|
export function parseMixinSource(source) {
|
|
172
|
-
const lines = source.split(/\r?\n/);
|
|
230
|
+
const lines = stripComments(source).split(/\r?\n/);
|
|
173
231
|
const parseWarnings = [];
|
|
174
232
|
const targets = [];
|
|
175
233
|
const injections = [];
|
|
@@ -323,6 +381,16 @@ export function parseMixinSource(source) {
|
|
|
323
381
|
}
|
|
324
382
|
i = endIndex;
|
|
325
383
|
}
|
|
384
|
+
// Try same-line declaration first (e.g. `@Accessor("x") int getX();`)
|
|
385
|
+
const inlineDecl = stripInlineAnnotations(line);
|
|
386
|
+
const inlineMethodMatch = inlineDecl.includes("(") ? METHOD_DECL_RE.exec(inlineDecl) : null;
|
|
387
|
+
if (inlineMethodMatch) {
|
|
388
|
+
const methodName = inlineMethodMatch[2];
|
|
389
|
+
const targetName = explicitTarget ?? inferAccessorTarget(methodName);
|
|
390
|
+
accessors.push({ annotation, name: methodName, targetName, line: lineNum });
|
|
391
|
+
i++;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
326
394
|
// Find the method declaration following the annotation (skip multi-line annotations)
|
|
327
395
|
let methodLine = i + 1;
|
|
328
396
|
methodLine = skipAnnotations(lines, methodLine);
|
|
@@ -43,8 +43,10 @@ export type GetModClassSourceOutput = {
|
|
|
43
43
|
export declare class ModDecompileService {
|
|
44
44
|
private readonly config;
|
|
45
45
|
private readonly decompileCache;
|
|
46
|
+
private readonly inflightDecompiles;
|
|
46
47
|
constructor(config: Config);
|
|
47
48
|
decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
|
|
48
49
|
getModClassSource(input: GetModClassSourceInput): Promise<GetModClassSourceOutput>;
|
|
49
50
|
private ensureDecompiled;
|
|
51
|
+
private decompileAndCache;
|
|
50
52
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
@@ -6,11 +7,12 @@ import { log } from "./logger.js";
|
|
|
6
7
|
import { decompileBinaryJar } from "./decompiler/vineflower.js";
|
|
7
8
|
import { resolveVineflowerJar } from "./vineflower-resolver.js";
|
|
8
9
|
import { analyzeModJar } from "./mod-analyzer.js";
|
|
9
|
-
import { validateAndNormalizeJarPath } from "./path-resolver.js";
|
|
10
|
+
import { buildJarSignature, validateAndNormalizeJarPath } from "./path-resolver.js";
|
|
10
11
|
import { sliceToMaxCharsSafe } from "./text-truncate.js";
|
|
11
12
|
const DECOMPILE_TIMEOUT_MS = 300_000;
|
|
12
13
|
function modDecompileCacheKey(jarPath) {
|
|
13
|
-
|
|
14
|
+
const stats = statSync(jarPath);
|
|
15
|
+
return createHash("sha256").update(`${jarPath}|${buildJarSignature(stats)}`).digest("hex");
|
|
14
16
|
}
|
|
15
17
|
function classNameToFilePath(className) {
|
|
16
18
|
return className.replaceAll(".", "/") + ".java";
|
|
@@ -28,6 +30,7 @@ export class ModDecompileService {
|
|
|
28
30
|
config;
|
|
29
31
|
// Cache: jarPath hash → decompiled output dir + file list
|
|
30
32
|
decompileCache = new Map();
|
|
33
|
+
inflightDecompiles = new Map();
|
|
31
34
|
constructor(config) {
|
|
32
35
|
this.config = config;
|
|
33
36
|
}
|
|
@@ -155,6 +158,20 @@ export class ModDecompileService {
|
|
|
155
158
|
this.decompileCache.set(cacheKey, cached);
|
|
156
159
|
return cached;
|
|
157
160
|
}
|
|
161
|
+
const existingDecompile = this.inflightDecompiles.get(cacheKey);
|
|
162
|
+
if (existingDecompile) {
|
|
163
|
+
return existingDecompile;
|
|
164
|
+
}
|
|
165
|
+
const decompilePromise = this.decompileAndCache(jarPath, cacheKey, warnings);
|
|
166
|
+
this.inflightDecompiles.set(cacheKey, decompilePromise);
|
|
167
|
+
try {
|
|
168
|
+
return await decompilePromise;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
this.inflightDecompiles.delete(cacheKey);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async decompileAndCache(jarPath, cacheKey, warnings) {
|
|
158
175
|
log("info", "mod-decompile.start", { jarPath });
|
|
159
176
|
const startedAt = Date.now();
|
|
160
177
|
// Analyze mod metadata
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
@@ -225,8 +225,7 @@ export async function remapModJar(input, config, deps = {}) {
|
|
|
225
225
|
}
|
|
226
226
|
mkdirSync(dirname(outputJar), { recursive: true });
|
|
227
227
|
// 7. Use a temporary directory for intermediate work
|
|
228
|
-
const tempDir = join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}
|
|
229
|
-
mkdirSync(tempDir, { recursive: true });
|
|
228
|
+
const tempDir = mkdtempSync(join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}-`));
|
|
230
229
|
try {
|
|
231
230
|
let passInput = normalizedInput;
|
|
232
231
|
let tempOutput = "";
|
|
@@ -266,7 +265,9 @@ export async function remapModJar(input, config, deps = {}) {
|
|
|
266
265
|
copyFileSync(tempOutput, outputJar);
|
|
267
266
|
if (outputJar !== cachedOutput) {
|
|
268
267
|
mkdirSync(dirname(cachedOutput), { recursive: true });
|
|
269
|
-
|
|
268
|
+
const tempCachedOutput = `${cachedOutput}.${randomBytes(4).toString("hex")}.tmp`;
|
|
269
|
+
copyFileSync(tempOutput, tempCachedOutput);
|
|
270
|
+
renameSync(tempCachedOutput, cachedOutput);
|
|
270
271
|
}
|
|
271
272
|
const durationMs = Date.now() - startedAt;
|
|
272
273
|
log("info", "remap.pipeline.done", {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
5
6
|
import { log } from "./logger.js";
|
|
@@ -339,7 +340,9 @@ export async function resolveMojangTinyFile(version, config, deps = {}) {
|
|
|
339
340
|
const membersByOwner = normalizeMemberMappings(parsedMappings, classMap, warnings);
|
|
340
341
|
const tinyContent = renderTinyV2(classMap, membersByOwner);
|
|
341
342
|
await mkdir(dirname(cachedTiny), { recursive: true });
|
|
342
|
-
|
|
343
|
+
const tempTiny = `${cachedTiny}.${randomBytes(4).toString("hex")}.tmp`;
|
|
344
|
+
await writeFile(tempTiny, tinyContent, "utf8");
|
|
345
|
+
await rename(tempTiny, cachedTiny);
|
|
343
346
|
log("info", "mojang-tiny.generated", {
|
|
344
347
|
version,
|
|
345
348
|
path: cachedTiny,
|
|
@@ -225,12 +225,12 @@ class NbtWriter {
|
|
|
225
225
|
}
|
|
226
226
|
writeFloat32(value) {
|
|
227
227
|
const chunk = Buffer.allocUnsafe(4);
|
|
228
|
-
chunk.writeFloatBE(value, 0);
|
|
228
|
+
chunk.writeFloatBE(Number(value), 0);
|
|
229
229
|
this.chunks.push(chunk);
|
|
230
230
|
}
|
|
231
231
|
writeFloat64(value) {
|
|
232
232
|
const chunk = Buffer.allocUnsafe(8);
|
|
233
|
-
chunk.writeDoubleBE(value, 0);
|
|
233
|
+
chunk.writeDoubleBE(Number(value), 0);
|
|
234
234
|
this.chunks.push(chunk);
|
|
235
235
|
}
|
|
236
236
|
writeInt64(value) {
|
|
@@ -238,6 +238,27 @@ class NbtWriter {
|
|
|
238
238
|
chunk.writeBigInt64BE(value, 0);
|
|
239
239
|
this.chunks.push(chunk);
|
|
240
240
|
}
|
|
241
|
+
writeInt8Array(values) {
|
|
242
|
+
const chunk = Buffer.allocUnsafe(values.length);
|
|
243
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
244
|
+
chunk.writeInt8(values[i], i);
|
|
245
|
+
}
|
|
246
|
+
this.chunks.push(chunk);
|
|
247
|
+
}
|
|
248
|
+
writeInt32Array(values) {
|
|
249
|
+
const chunk = Buffer.allocUnsafe(values.length * 4);
|
|
250
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
251
|
+
chunk.writeInt32BE(values[i], i * 4);
|
|
252
|
+
}
|
|
253
|
+
this.chunks.push(chunk);
|
|
254
|
+
}
|
|
255
|
+
writeInt64Array(values) {
|
|
256
|
+
const chunk = Buffer.allocUnsafe(values.length * 8);
|
|
257
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
258
|
+
chunk.writeBigInt64BE(values[i], i * 8);
|
|
259
|
+
}
|
|
260
|
+
this.chunks.push(chunk);
|
|
261
|
+
}
|
|
241
262
|
writeString(value) {
|
|
242
263
|
const encoded = encodeMutf8(value);
|
|
243
264
|
if (encoded.length > 0xffff) {
|
|
@@ -261,10 +282,14 @@ function readPayload(reader, tagId, pointer) {
|
|
|
261
282
|
return { type: "int", value: reader.readInt32() };
|
|
262
283
|
case "long":
|
|
263
284
|
return { type: "long", value: reader.readInt64().toString() };
|
|
264
|
-
case "float":
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
case "float": {
|
|
286
|
+
const value = reader.readFloat32();
|
|
287
|
+
return { type: "float", value: Number.isFinite(value) ? value : Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity" };
|
|
288
|
+
}
|
|
289
|
+
case "double": {
|
|
290
|
+
const value = reader.readFloat64();
|
|
291
|
+
return { type: "double", value: Number.isFinite(value) ? value : Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity" };
|
|
292
|
+
}
|
|
268
293
|
case "byteArray": {
|
|
269
294
|
const length = reader.readInt32();
|
|
270
295
|
if (length < 0) {
|
|
@@ -315,7 +340,12 @@ function readPayload(reader, tagId, pointer) {
|
|
|
315
340
|
break;
|
|
316
341
|
}
|
|
317
342
|
const childName = reader.readString();
|
|
318
|
-
value
|
|
343
|
+
Object.defineProperty(value, childName, {
|
|
344
|
+
value: readPayload(reader, childType, `${pointer}/value/${childName}`),
|
|
345
|
+
enumerable: true,
|
|
346
|
+
writable: true,
|
|
347
|
+
configurable: true
|
|
348
|
+
});
|
|
319
349
|
}
|
|
320
350
|
return { type: "compound", value };
|
|
321
351
|
}
|
|
@@ -380,9 +410,7 @@ function writePayload(writer, node, pointer) {
|
|
|
380
410
|
return;
|
|
381
411
|
case "byteArray":
|
|
382
412
|
writer.writeInt32(node.value.length);
|
|
383
|
-
|
|
384
|
-
writer.writeInt8(value);
|
|
385
|
-
}
|
|
413
|
+
writer.writeInt8Array(node.value);
|
|
386
414
|
return;
|
|
387
415
|
case "string":
|
|
388
416
|
writer.writeString(node.value);
|
|
@@ -404,15 +432,11 @@ function writePayload(writer, node, pointer) {
|
|
|
404
432
|
return;
|
|
405
433
|
case "intArray":
|
|
406
434
|
writer.writeInt32(node.value.length);
|
|
407
|
-
|
|
408
|
-
writer.writeInt32(value);
|
|
409
|
-
}
|
|
435
|
+
writer.writeInt32Array(node.value);
|
|
410
436
|
return;
|
|
411
437
|
case "longArray":
|
|
412
438
|
writer.writeInt32(node.value.length);
|
|
413
|
-
|
|
414
|
-
writer.writeInt64(BigInt(value));
|
|
415
|
-
}
|
|
439
|
+
writer.writeInt64Array(node.value.map((value) => BigInt(value)));
|
|
416
440
|
return;
|
|
417
441
|
default:
|
|
418
442
|
throw encodeError("Unsupported typed NBT node for encoding.", {
|
package/dist/nbt/json-patch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { createError, ERROR_CODES } from "../errors.js";
|
|
3
|
-
import { assertValidTypedNbtDocument, validateTypedNbtDocument } from "./typed-json.js";
|
|
3
|
+
import { assertValidTypedNbtDocument, validateTypedNbtDocument, validateTypedNbtNode } from "./typed-json.js";
|
|
4
4
|
function isRecord(value) {
|
|
5
5
|
return (typeof value === "object" &&
|
|
6
6
|
value !== null &&
|
|
@@ -136,6 +136,9 @@ function parseArrayIndex(token, length, options) {
|
|
|
136
136
|
}
|
|
137
137
|
return index;
|
|
138
138
|
}
|
|
139
|
+
function isTypedNbtNode(value) {
|
|
140
|
+
return isRecord(value) && typeof value.type === "string";
|
|
141
|
+
}
|
|
139
142
|
function resolveParent(root, tokens, opIndex, path) {
|
|
140
143
|
if (tokens.length === 0) {
|
|
141
144
|
patchConflict("Operation path does not reference a child location.", {
|
|
@@ -144,6 +147,8 @@ function resolveParent(root, tokens, opIndex, path) {
|
|
|
144
147
|
});
|
|
145
148
|
}
|
|
146
149
|
let cursor = root;
|
|
150
|
+
let enclosingNode;
|
|
151
|
+
let enclosingPointer = "";
|
|
147
152
|
for (let i = 0; i < tokens.length - 1; i += 1) {
|
|
148
153
|
const token = tokens[i];
|
|
149
154
|
const pointer = `/${tokens.slice(0, i + 1).join("/")}`;
|
|
@@ -154,6 +159,10 @@ function resolveParent(root, tokens, opIndex, path) {
|
|
|
154
159
|
path: pointer
|
|
155
160
|
});
|
|
156
161
|
cursor = cursor[index];
|
|
162
|
+
if (isTypedNbtNode(cursor)) {
|
|
163
|
+
enclosingNode = cursor;
|
|
164
|
+
enclosingPointer = pointer;
|
|
165
|
+
}
|
|
157
166
|
continue;
|
|
158
167
|
}
|
|
159
168
|
if (!isRecord(cursor)) {
|
|
@@ -169,8 +178,12 @@ function resolveParent(root, tokens, opIndex, path) {
|
|
|
169
178
|
});
|
|
170
179
|
}
|
|
171
180
|
cursor = cursor[token];
|
|
181
|
+
if (isTypedNbtNode(cursor)) {
|
|
182
|
+
enclosingNode = cursor;
|
|
183
|
+
enclosingPointer = pointer;
|
|
184
|
+
}
|
|
172
185
|
}
|
|
173
|
-
return { parent: cursor, key: tokens[tokens.length - 1] };
|
|
186
|
+
return { parent: cursor, key: tokens[tokens.length - 1], enclosingNode, enclosingPointer };
|
|
174
187
|
}
|
|
175
188
|
function readValueAtPath(root, tokens, opIndex, path) {
|
|
176
189
|
if (tokens.length === 0) {
|
|
@@ -205,8 +218,10 @@ function readValueAtPath(root, tokens, opIndex, path) {
|
|
|
205
218
|
}
|
|
206
219
|
return cursor;
|
|
207
220
|
}
|
|
208
|
-
function assertTypedNbtInvariant(
|
|
209
|
-
const validation =
|
|
221
|
+
function assertTypedNbtInvariant(subtree, opIndex, path) {
|
|
222
|
+
const validation = "root" in subtree
|
|
223
|
+
? validateTypedNbtDocument(subtree.root)
|
|
224
|
+
: validateTypedNbtNode(subtree.node, subtree.pointer);
|
|
210
225
|
if (!validation.ok) {
|
|
211
226
|
patchConflict("JSON Patch operation produced invalid typed NBT JSON.", {
|
|
212
227
|
opIndex,
|
|
@@ -241,11 +256,13 @@ export function applyJsonPatch(document, patch) {
|
|
|
241
256
|
}
|
|
242
257
|
if (operation.op === "add") {
|
|
243
258
|
const nextValue = structuredClone(operation.value);
|
|
259
|
+
let subtree = { root: working };
|
|
244
260
|
if (tokens.length === 0) {
|
|
245
261
|
working = nextValue;
|
|
262
|
+
subtree = { root: working };
|
|
246
263
|
}
|
|
247
264
|
else {
|
|
248
|
-
const { parent, key } = resolveParent(working, tokens, i, operation.path);
|
|
265
|
+
const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
|
|
249
266
|
if (Array.isArray(parent)) {
|
|
250
267
|
const index = parseArrayIndex(key, parent.length, {
|
|
251
268
|
allowAppend: true,
|
|
@@ -255,7 +272,12 @@ export function applyJsonPatch(document, patch) {
|
|
|
255
272
|
parent.splice(index, 0, nextValue);
|
|
256
273
|
}
|
|
257
274
|
else if (isRecord(parent)) {
|
|
258
|
-
parent
|
|
275
|
+
Object.defineProperty(parent, key, {
|
|
276
|
+
value: nextValue,
|
|
277
|
+
enumerable: true,
|
|
278
|
+
writable: true,
|
|
279
|
+
configurable: true
|
|
280
|
+
});
|
|
259
281
|
}
|
|
260
282
|
else {
|
|
261
283
|
patchConflict("Add target parent is not a container.", {
|
|
@@ -263,9 +285,13 @@ export function applyJsonPatch(document, patch) {
|
|
|
263
285
|
jsonPointer: operation.path
|
|
264
286
|
});
|
|
265
287
|
}
|
|
288
|
+
subtree =
|
|
289
|
+
enclosingNode === undefined
|
|
290
|
+
? { root: working }
|
|
291
|
+
: { node: enclosingNode, pointer: enclosingPointer };
|
|
266
292
|
}
|
|
267
293
|
changed = true;
|
|
268
|
-
assertTypedNbtInvariant(
|
|
294
|
+
assertTypedNbtInvariant(subtree, i, operation.path);
|
|
269
295
|
continue;
|
|
270
296
|
}
|
|
271
297
|
if (operation.op === "remove") {
|
|
@@ -275,7 +301,7 @@ export function applyJsonPatch(document, patch) {
|
|
|
275
301
|
jsonPointer: operation.path
|
|
276
302
|
});
|
|
277
303
|
}
|
|
278
|
-
const { parent, key } = resolveParent(working, tokens, i, operation.path);
|
|
304
|
+
const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
|
|
279
305
|
if (Array.isArray(parent)) {
|
|
280
306
|
const index = parseArrayIndex(key, parent.length, {
|
|
281
307
|
allowAppend: false,
|
|
@@ -300,16 +326,20 @@ export function applyJsonPatch(document, patch) {
|
|
|
300
326
|
});
|
|
301
327
|
}
|
|
302
328
|
changed = true;
|
|
303
|
-
assertTypedNbtInvariant(
|
|
329
|
+
assertTypedNbtInvariant(enclosingNode === undefined
|
|
330
|
+
? { root: working }
|
|
331
|
+
: { node: enclosingNode, pointer: enclosingPointer }, i, operation.path);
|
|
304
332
|
continue;
|
|
305
333
|
}
|
|
306
334
|
if (operation.op === "replace") {
|
|
307
335
|
const nextValue = structuredClone(operation.value);
|
|
336
|
+
let subtree = { root: working };
|
|
308
337
|
if (tokens.length === 0) {
|
|
309
338
|
working = nextValue;
|
|
339
|
+
subtree = { root: working };
|
|
310
340
|
}
|
|
311
341
|
else {
|
|
312
|
-
const { parent, key } = resolveParent(working, tokens, i, operation.path);
|
|
342
|
+
const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
|
|
313
343
|
if (Array.isArray(parent)) {
|
|
314
344
|
const index = parseArrayIndex(key, parent.length, {
|
|
315
345
|
allowAppend: false,
|
|
@@ -325,7 +355,12 @@ export function applyJsonPatch(document, patch) {
|
|
|
325
355
|
jsonPointer: operation.path
|
|
326
356
|
});
|
|
327
357
|
}
|
|
328
|
-
parent
|
|
358
|
+
Object.defineProperty(parent, key, {
|
|
359
|
+
value: nextValue,
|
|
360
|
+
enumerable: true,
|
|
361
|
+
writable: true,
|
|
362
|
+
configurable: true
|
|
363
|
+
});
|
|
329
364
|
}
|
|
330
365
|
else {
|
|
331
366
|
patchConflict("Replace target parent is not a container.", {
|
|
@@ -333,9 +368,13 @@ export function applyJsonPatch(document, patch) {
|
|
|
333
368
|
jsonPointer: operation.path
|
|
334
369
|
});
|
|
335
370
|
}
|
|
371
|
+
subtree =
|
|
372
|
+
enclosingNode === undefined
|
|
373
|
+
? { root: working }
|
|
374
|
+
: { node: enclosingNode, pointer: enclosingPointer };
|
|
336
375
|
}
|
|
337
376
|
changed = true;
|
|
338
|
-
assertTypedNbtInvariant(
|
|
377
|
+
assertTypedNbtInvariant(subtree, i, operation.path);
|
|
339
378
|
continue;
|
|
340
379
|
}
|
|
341
380
|
}
|
package/dist/nbt/typed-json.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type ValidationResult = {
|
|
|
5
5
|
ok: false;
|
|
6
6
|
issue: TypedNbtValidationIssue;
|
|
7
7
|
};
|
|
8
|
+
export declare function validateTypedNbtNode(value: unknown, pointer: string): ValidationResult;
|
|
8
9
|
export declare function validateTypedNbtDocument(value: unknown): ValidationResult;
|
|
9
10
|
export declare function assertValidTypedNbtDocument(value: unknown): asserts value is TypedNbtDocument;
|
|
10
11
|
export type { TypedNbtDocument, TypedNbtValidationIssue } from "./types.js";
|
package/dist/nbt/typed-json.js
CHANGED
|
@@ -16,6 +16,7 @@ const NODE_TYPES = [
|
|
|
16
16
|
const LIST_ELEMENT_TYPES = ["end", ...NODE_TYPES];
|
|
17
17
|
const NODE_TYPE_SET = new Set(NODE_TYPES);
|
|
18
18
|
const LIST_ELEMENT_TYPE_SET = new Set(LIST_ELEMENT_TYPES);
|
|
19
|
+
const NON_FINITE_FLOAT_SENTINELS = new Set(["NaN", "Infinity", "-Infinity"]);
|
|
19
20
|
const INT64_MIN = -9223372036854775808n;
|
|
20
21
|
const INT64_MAX = 9223372036854775807n;
|
|
21
22
|
function isRecord(value) {
|
|
@@ -119,8 +120,14 @@ function validateNode(value, pointer) {
|
|
|
119
120
|
return validateLongString(node.value, `${pointer}/value`);
|
|
120
121
|
case "float":
|
|
121
122
|
case "double":
|
|
122
|
-
if (typeof node.value
|
|
123
|
-
|
|
123
|
+
if (typeof node.value === "string") {
|
|
124
|
+
if (!NON_FINITE_FLOAT_SENTINELS.has(node.value)) {
|
|
125
|
+
return fail(`${pointer}/value`, "number-or-non-finite-sentinel", node.value);
|
|
126
|
+
}
|
|
127
|
+
return { ok: true };
|
|
128
|
+
}
|
|
129
|
+
if (typeof node.value !== "number") {
|
|
130
|
+
return fail(`${pointer}/value`, "number-or-non-finite-sentinel", node.value);
|
|
124
131
|
}
|
|
125
132
|
return { ok: true };
|
|
126
133
|
case "string":
|
|
@@ -180,6 +187,9 @@ function validateNode(value, pointer) {
|
|
|
180
187
|
return fail(`${pointer}/type`, "nbt-node-type", nodeType);
|
|
181
188
|
}
|
|
182
189
|
}
|
|
190
|
+
export function validateTypedNbtNode(value, pointer) {
|
|
191
|
+
return validateNode(value, pointer);
|
|
192
|
+
}
|
|
183
193
|
export function validateTypedNbtDocument(value) {
|
|
184
194
|
if (!isRecord(value)) {
|
|
185
195
|
return fail("", "typed-nbt-document", value);
|
package/dist/nbt/types.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export type NbtNode = {
|
|
|
15
15
|
value: string;
|
|
16
16
|
} | {
|
|
17
17
|
type: "float";
|
|
18
|
-
value: number;
|
|
18
|
+
value: number | "NaN" | "Infinity" | "-Infinity";
|
|
19
19
|
} | {
|
|
20
20
|
type: "double";
|
|
21
|
-
value: number;
|
|
21
|
+
value: number | "NaN" | "Infinity" | "-Infinity";
|
|
22
22
|
} | {
|
|
23
23
|
type: "byteArray";
|
|
24
24
|
value: number[];
|
package/dist/path-converter.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
3
3
|
const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
|
|
4
4
|
const MALFORMED_WINDOWS_DRIVE_PATH = /^[A-Za-z]:(?![\\/])/;
|
|
5
5
|
const WSL_MOUNT_PATH = /^\/mnt\/[a-z](?:\/|$)/i;
|
|
6
|
-
const UNC_WSL_PATH = /^(?:\\\\wsl
|
|
6
|
+
const UNC_WSL_PATH = /^(?:\\\\(?:wsl\$|wsl\.localhost)\\|\/\/(?:wsl\$|wsl\.localhost)\/)/i;
|
|
7
7
|
function normalizeToUnixSlashes(value) {
|
|
8
8
|
return value.replace(/\\/g, "/");
|
|
9
9
|
}
|
|
@@ -12,10 +12,11 @@ function normalizeToWindowsSlashes(value) {
|
|
|
12
12
|
}
|
|
13
13
|
function parseUncWslPath(pathValue) {
|
|
14
14
|
const normalized = normalizeToUnixSlashes(pathValue);
|
|
15
|
-
|
|
15
|
+
const prefixMatch = normalized.match(/^\/\/(?:wsl\$|wsl\.localhost)\//i);
|
|
16
|
+
if (!prefixMatch) {
|
|
16
17
|
return undefined;
|
|
17
18
|
}
|
|
18
|
-
const remainder = normalized.slice(
|
|
19
|
+
const remainder = normalized.slice(prefixMatch[0].length).replace(/^\/+/, "");
|
|
19
20
|
const slashIndex = remainder.indexOf("/");
|
|
20
21
|
if (slashIndex < 0) {
|
|
21
22
|
return {
|
|
@@ -99,6 +100,12 @@ function toWslPath(pathValue, runtime, field) {
|
|
|
99
100
|
}
|
|
100
101
|
const parsed = parseUncWslPath(pathValue);
|
|
101
102
|
if (parsed) {
|
|
103
|
+
const distro = runtime.wslDistro?.trim();
|
|
104
|
+
if (!distro || parsed.distro.toLowerCase() !== distro.toLowerCase()) {
|
|
105
|
+
throwInvalidPath(pathValue, distro
|
|
106
|
+
? `UNC WSL path targets distro "${parsed.distro}", but this server runs in distro "${distro}".`
|
|
107
|
+
: `Cannot resolve UNC WSL path for distro "${parsed.distro}" without a known WSL distro name.`, runtime, field);
|
|
108
|
+
}
|
|
102
109
|
const rest = parsed.innerPath.replace(/^\/+/, "");
|
|
103
110
|
return rest ? `/${rest}` : "/";
|
|
104
111
|
}
|
|
@@ -28,9 +28,11 @@ export declare class RegistryService {
|
|
|
28
28
|
private readonly config;
|
|
29
29
|
private readonly versionService;
|
|
30
30
|
private readonly registryCache;
|
|
31
|
+
private readonly loadLocks;
|
|
31
32
|
constructor(config: Config, versionService: VersionService);
|
|
32
33
|
getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
|
|
33
34
|
private loadRegistries;
|
|
35
|
+
private loadRegistriesInternal;
|
|
34
36
|
private loadExistingRegistries;
|
|
35
37
|
private readRegistryFileOrThrow;
|
|
36
38
|
private isInvalidRegistrySnapshot;
|
package/dist/registry-service.js
CHANGED
|
@@ -126,7 +126,7 @@ function runDataGen(serverJarPath, outputDir, version) {
|
|
|
126
126
|
];
|
|
127
127
|
log("info", "registry.datagen.start", { version, isLegacy, serverJarPath, outputDir });
|
|
128
128
|
const proc = spawn("java", args, {
|
|
129
|
-
stdio: ["ignore", "
|
|
129
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
130
130
|
cwd: outputDir
|
|
131
131
|
});
|
|
132
132
|
let stderr = "";
|
|
@@ -194,6 +194,7 @@ export class RegistryService {
|
|
|
194
194
|
config;
|
|
195
195
|
versionService;
|
|
196
196
|
registryCache = new Map();
|
|
197
|
+
loadLocks = new Map();
|
|
197
198
|
constructor(config, versionService) {
|
|
198
199
|
this.config = config;
|
|
199
200
|
this.versionService = versionService;
|
|
@@ -270,6 +271,20 @@ export class RegistryService {
|
|
|
270
271
|
};
|
|
271
272
|
}
|
|
272
273
|
async loadRegistries(version, warnings) {
|
|
274
|
+
const existingLock = this.loadLocks.get(version);
|
|
275
|
+
if (existingLock) {
|
|
276
|
+
return existingLock;
|
|
277
|
+
}
|
|
278
|
+
const loadPromise = this.loadRegistriesInternal(version, warnings);
|
|
279
|
+
this.loadLocks.set(version, loadPromise);
|
|
280
|
+
try {
|
|
281
|
+
return await loadPromise;
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
this.loadLocks.delete(version);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async loadRegistriesInternal(version, warnings) {
|
|
273
288
|
const cached = this.registryCache.get(version);
|
|
274
289
|
if (cached) {
|
|
275
290
|
// LRU touch: move to most-recently-used so the bound evicts the true
|
package/dist/repo-downloader.js
CHANGED
|
@@ -40,7 +40,6 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
|
|
|
40
40
|
const timer = setTimeout(() => timeout.abort(), timeoutMs);
|
|
41
41
|
try {
|
|
42
42
|
const response = await fetchFn(url, { signal: timeout.signal });
|
|
43
|
-
clearTimeout(timer);
|
|
44
43
|
const status = response.status;
|
|
45
44
|
if (status === 404) {
|
|
46
45
|
return { ok: false, statusCode: status };
|
|
@@ -71,7 +70,7 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
|
|
|
71
70
|
}
|
|
72
71
|
else {
|
|
73
72
|
const readable = Readable.fromWeb(response.body);
|
|
74
|
-
await pipeline(readable, createWriteStream(tempPath));
|
|
73
|
+
await pipeline(readable, createWriteStream(tempPath), { signal: timeout.signal });
|
|
75
74
|
}
|
|
76
75
|
const contentLength = statSync(tempPath).size;
|
|
77
76
|
renameSync(tempPath, destinationPath);
|
|
@@ -95,13 +94,15 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
|
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
catch (caughtError) {
|
|
98
|
-
clearTimeout(timer);
|
|
99
97
|
if (attempt >= maxRetries) {
|
|
100
98
|
throw caughtError instanceof Error ? caughtError : new Error(String(caughtError));
|
|
101
99
|
}
|
|
102
100
|
await sleep(retryDelay(200, attempt));
|
|
103
101
|
attempt += 1;
|
|
104
102
|
}
|
|
103
|
+
finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
export function defaultDownloadPath(cacheDir, url) {
|