@adhisang/minecraft-modding-mcp 7.0.0 → 7.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/CHANGELOG.md +72 -0
- package/README.md +4 -3
- package/dist/access-transformer-parser.d.ts +8 -0
- package/dist/access-transformer-parser.js +8 -1
- package/dist/access-widener-parser.d.ts +17 -0
- package/dist/access-widener-parser.js +12 -1
- package/dist/cache-registry.js +19 -6
- package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-members-service.js +20 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-source-service.js +10 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
- package/dist/entry-tools/compare-minecraft-service.js +65 -4
- package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
- package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.js +10 -14
- package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
- package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
- package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
- package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/entry-tools/verify-mixin-target-service.js +19 -8
- package/dist/index.js +38 -15
- package/dist/java-process.d.ts +1 -0
- package/dist/java-process.js +14 -0
- package/dist/mapping/lookup.js +16 -1
- package/dist/mapping-service.d.ts +14 -0
- package/dist/mapping-service.js +35 -15
- package/dist/minecraft-explorer-service.js +70 -8
- package/dist/mixin/access-validators.js +38 -2
- package/dist/mixin/annotation-validators.js +137 -43
- package/dist/mixin/parsed-validator.js +21 -7
- package/dist/mixin-parser.d.ts +52 -0
- package/dist/mixin-parser.js +709 -130
- package/dist/mod-decompile-service.js +11 -1
- package/dist/mod-remap-service.js +6 -6
- package/dist/nbt/java-nbt-codec.js +7 -1
- package/dist/source/access-validate.js +10 -0
- package/dist/source/artifact-resolver.d.ts +27 -3
- package/dist/source/artifact-resolver.js +235 -30
- package/dist/source/class-source/members-builder.d.ts +7 -0
- package/dist/source/class-source/members-builder.js +4 -1
- package/dist/source/class-source.d.ts +9 -2
- package/dist/source/class-source.js +186 -27
- package/dist/source/indexer.js +69 -1
- package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
- package/dist/source/lifecycle/mapping-helpers.js +29 -3
- package/dist/source/lifecycle/runtime-check.d.ts +25 -0
- package/dist/source/lifecycle/runtime-check.js +68 -39
- package/dist/source/nested-jars.d.ts +15 -1
- package/dist/source/nested-jars.js +14 -5
- package/dist/source/search.d.ts +10 -2
- package/dist/source/search.js +60 -13
- package/dist/source/symbol-resolver.js +88 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
- package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
- package/dist/source/validate-mixin.d.ts +5 -0
- package/dist/source/validate-mixin.js +136 -21
- package/dist/source/workspace-target.js +75 -7
- package/dist/source-jar-reader.d.ts +48 -1
- package/dist/source-jar-reader.js +93 -3
- package/dist/source-resolver.d.ts +7 -0
- package/dist/source-resolver.js +22 -14
- package/dist/source-service.d.ts +5 -0
- package/dist/source-service.js +7 -0
- package/dist/stdio-supervisor.d.ts +35 -1
- package/dist/stdio-supervisor.js +77 -2
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +181 -20
- package/dist/storage/files-repo.d.ts +7 -0
- package/dist/storage/files-repo.js +17 -4
- package/dist/storage/sqlite.d.ts +31 -1
- package/dist/storage/sqlite.js +125 -16
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +2 -1
- package/dist/tool-guidance.js +4 -1
- package/dist/tool-schemas.d.ts +64 -52
- package/dist/tool-schemas.js +9 -7
- package/dist/types.d.ts +9 -0
- package/dist/v1-parity-schemas.js +36 -2
- package/dist/version-diff-service.d.ts +23 -0
- package/dist/version-diff-service.js +101 -0
- package/dist/version-service.d.ts +14 -0
- package/dist/version-service.js +52 -3
- package/dist/workspace-context-cache.d.ts +25 -0
- package/dist/workspace-context-cache.js +52 -2
- package/dist/workspace-mapping-service.d.ts +8 -0
- package/dist/workspace-mapping-service.js +151 -21
- package/docs/README-ja.md +3 -1
- package/docs/tool-reference.md +69 -22
- package/package.json +1 -1
|
@@ -124,9 +124,9 @@ export declare const validateProjectShape: {
|
|
|
124
124
|
preferProjectVersion: z.ZodOptional<z.ZodBoolean>;
|
|
125
125
|
preferProjectMapping: z.ZodDefault<z.ZodBoolean>;
|
|
126
126
|
detail: z.ZodOptional<z.ZodEnum<{
|
|
127
|
+
full: "full";
|
|
127
128
|
summary: "summary";
|
|
128
129
|
standard: "standard";
|
|
129
|
-
full: "full";
|
|
130
130
|
}>>;
|
|
131
131
|
include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
132
132
|
[x: string]: string;
|
|
@@ -236,9 +236,9 @@ export declare const validateProjectSchema: z.ZodObject<{
|
|
|
236
236
|
preferProjectVersion: z.ZodOptional<z.ZodBoolean>;
|
|
237
237
|
preferProjectMapping: z.ZodDefault<z.ZodBoolean>;
|
|
238
238
|
detail: z.ZodOptional<z.ZodEnum<{
|
|
239
|
+
full: "full";
|
|
239
240
|
summary: "summary";
|
|
240
241
|
standard: "standard";
|
|
241
|
-
full: "full";
|
|
242
242
|
}>>;
|
|
243
243
|
include: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
244
244
|
[x: string]: string;
|
|
@@ -51,14 +51,16 @@ function visibilityFromFlags(flags) {
|
|
|
51
51
|
function buildExampleSnippet(annotation, member, match, mixinMemberName) {
|
|
52
52
|
const accessFlags = match?.accessFlags ?? [];
|
|
53
53
|
const isFinal = accessFlags.includes("final");
|
|
54
|
+
const isStatic = accessFlags.includes("static");
|
|
54
55
|
const targetName = member.name;
|
|
55
56
|
if (annotation === "@Inject-only") {
|
|
56
|
-
return `// "${targetName}" is already visible to the mixin (target access: ${accessFlags.filter((f) => f === "public" || f === "protected" || f === "private" || f === "package-private")[0] ?? "unknown"}); use @Inject
|
|
57
|
+
return `// "${targetName}" is already visible to the mixin (target access: ${accessFlags.filter((f) => f === "public" || f === "protected" || f === "private" || f === "package-private")[0] ?? "unknown"}); no @Invoker needed — use @Inject to hook it, or @Shadow to call it from mixin code.`;
|
|
57
58
|
}
|
|
58
59
|
if (annotation === "@Shadow") {
|
|
59
60
|
if (member.kind === "field") {
|
|
60
61
|
const finalTag = isFinal ? "@Shadow @Final\n" : "@Shadow\n";
|
|
61
|
-
|
|
62
|
+
const modifiers = isStatic ? "private static" : "private";
|
|
63
|
+
return `${finalTag}${modifiers} <type> ${targetName};`;
|
|
62
64
|
}
|
|
63
65
|
return `@Shadow\nprivate <returnType> ${targetName}(<params>);`;
|
|
64
66
|
}
|
|
@@ -76,9 +78,18 @@ function buildExampleSnippet(annotation, member, match, mixinMemberName) {
|
|
|
76
78
|
function inferAnnotation(member, match, mixinMemberName) {
|
|
77
79
|
const visibility = match ? deriveVisibility(match.accessFlags) : "private";
|
|
78
80
|
if (visibility === "public" || visibility === "protected") {
|
|
81
|
+
if (member.kind === "field") {
|
|
82
|
+
// A visible field needs no @Accessor to be read/written from outside the
|
|
83
|
+
// mixin, but mixin code still needs @Shadow to reference the field.
|
|
84
|
+
return {
|
|
85
|
+
suggestedAnnotation: "@Shadow",
|
|
86
|
+
reasoning: `Target field "${member.name}" is ${visibility}; no @Accessor is needed to access it from outside, but the mixin still needs @Shadow to reference it.`,
|
|
87
|
+
exampleSnippet: buildExampleSnippet("@Shadow", member, match, mixinMemberName)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
79
90
|
return {
|
|
80
91
|
suggestedAnnotation: "@Inject-only",
|
|
81
|
-
reasoning: `Target "${member.name}" is ${visibility}; it is already visible to the mixin
|
|
92
|
+
reasoning: `Target method "${member.name}" is ${visibility}; it is already visible to the mixin, so no @Invoker is needed. Use @Inject to hook it, or @Shadow to call it from mixin code.`,
|
|
82
93
|
exampleSnippet: buildExampleSnippet("@Inject-only", member, match, mixinMemberName)
|
|
83
94
|
};
|
|
84
95
|
}
|
|
@@ -99,30 +110,30 @@ function inferAnnotation(member, match, mixinMemberName) {
|
|
|
99
110
|
}
|
|
100
111
|
return {
|
|
101
112
|
suggestedAnnotation: "@Shadow",
|
|
102
|
-
reasoning: `Target "${member.name}" is
|
|
113
|
+
reasoning: `Target "${member.name}" is ${visibility}. mixin member "${mixinMemberName}" does not match accessor/invoker prefix conventions; @Shadow is appropriate.`,
|
|
103
114
|
exampleSnippet: buildExampleSnippet("@Shadow", member, match, mixinMemberName)
|
|
104
115
|
};
|
|
105
116
|
}
|
|
106
117
|
if (member.kind === "field") {
|
|
107
118
|
return {
|
|
108
119
|
suggestedAnnotation: "@Shadow",
|
|
109
|
-
reasoning: `Target "${member.name}" is
|
|
120
|
+
reasoning: `Target "${member.name}" is ${visibility}; use @Shadow to access it from the mixin.`,
|
|
110
121
|
exampleSnippet: buildExampleSnippet("@Shadow", member, match, mixinMemberName)
|
|
111
122
|
};
|
|
112
123
|
}
|
|
113
124
|
return {
|
|
114
125
|
suggestedAnnotation: null,
|
|
115
|
-
reasoning: `Target method "${member.name}" is
|
|
126
|
+
reasoning: `Target method "${member.name}" is ${visibility} and no mixinMemberName was supplied; cannot decide between @Shadow and @Invoker. Choose @Shadow when the mixin overrides the method, @Invoker when it only needs to call it.`,
|
|
116
127
|
exampleSnippet: "// Provide mixinMemberName to receive a concrete @Shadow or @Invoker template.",
|
|
117
128
|
candidates: [
|
|
118
129
|
{
|
|
119
130
|
annotation: "@Shadow",
|
|
120
|
-
reasoning:
|
|
131
|
+
reasoning: `Use @Shadow to override or read the ${visibility} method body from the mixin.`,
|
|
121
132
|
exampleSnippet: buildExampleSnippet("@Shadow", member, match, mixinMemberName)
|
|
122
133
|
},
|
|
123
134
|
{
|
|
124
135
|
annotation: "@Invoker",
|
|
125
|
-
reasoning:
|
|
136
|
+
reasoning: `Use @Invoker when the mixin only needs to invoke the ${visibility} method without redeclaring the body.`,
|
|
126
137
|
exampleSnippet: buildExampleSnippet("@Invoker", member, match, mixinMemberName)
|
|
127
138
|
}
|
|
128
139
|
]
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { prepareToolInput } from "./tool-input.js";
|
|
|
11
11
|
import { DETAIL_ENABLED_TOOL_NAMES, DEFAULT_DETAIL_BY_TOOL, projectByDetail } from "./response-utils.js";
|
|
12
12
|
import { loadConfig } from "./config.js";
|
|
13
13
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
14
|
+
import { convertRuntimeSqliteCorruption } from "./storage/db.js";
|
|
14
15
|
import { log } from "./logger.js";
|
|
15
16
|
import { applyNbtJsonPatch, nbtBase64ToTypedJson, typedJsonToNbtBase64 } from "./nbt/pipeline.js";
|
|
16
17
|
import { analyzeModJar } from "./mod-analyzer.js";
|
|
@@ -136,7 +137,8 @@ const analyzeSymbolService = new AnalyzeSymbolService({
|
|
|
136
137
|
const compareMinecraftService = new CompareMinecraftService({
|
|
137
138
|
compareVersions: (input) => sourceService.compareVersions(input),
|
|
138
139
|
diffClassSignatures: (input) => sourceService.diffClassSignatures(input),
|
|
139
|
-
getRegistryData: (input) => sourceService.getRegistryData(input)
|
|
140
|
+
getRegistryData: (input) => sourceService.getRegistryData(input),
|
|
141
|
+
getVersionLibraries: (input) => sourceService.getVersionLibraries(input.version)
|
|
140
142
|
});
|
|
141
143
|
const analyzeModService = new AnalyzeModService({
|
|
142
144
|
analyzeModJar: (jarPath, options) => analyzeModJar(jarPath, options),
|
|
@@ -428,38 +430,58 @@ async function runTool(tool, rawInput, schema, action) {
|
|
|
428
430
|
});
|
|
429
431
|
}
|
|
430
432
|
catch (caughtError) {
|
|
431
|
-
|
|
433
|
+
// A raw SQLite corruption error surfaced mid-call (as opposed to at DB
|
|
434
|
+
// open, which is already a typed AppError by the time it gets here) means
|
|
435
|
+
// quick_check passed for this file but a live query still hit corruption.
|
|
436
|
+
// Escalate the next open to a full integrity_check and report a typed,
|
|
437
|
+
// restart-guiding ERR_DB_FAILURE instead of leaking ERR_INTERNAL.
|
|
438
|
+
const reportedError = convertRuntimeSqliteCorruption(caughtError, config.sqlitePath);
|
|
439
|
+
if (reportedError !== caughtError) {
|
|
440
|
+
// The public envelope only ever gets the generic ERR_DB_FAILURE message
|
|
441
|
+
// and nextAction (see convertRuntimeSqliteCorruption) - the original
|
|
442
|
+
// SQLite diagnostic is server-side-only, logged here with the request
|
|
443
|
+
// context convertRuntimeSqliteCorruption itself does not have access to.
|
|
444
|
+
const rawSqliteError = caughtError;
|
|
445
|
+
log("error", "tool.call.sqlite_runtime_corruption", {
|
|
446
|
+
requestId,
|
|
447
|
+
tool,
|
|
448
|
+
message: typeof rawSqliteError?.message === "string" ? rawSqliteError.message : String(caughtError),
|
|
449
|
+
code: rawSqliteError?.code,
|
|
450
|
+
errcode: rawSqliteError?.errcode
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const problem = mapErrorToProblem(reportedError, requestId, {
|
|
432
454
|
tool,
|
|
433
455
|
normalizedInput
|
|
434
456
|
});
|
|
435
|
-
if (isAppError(
|
|
436
|
-
const isSevere =
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
457
|
+
if (isAppError(reportedError)) {
|
|
458
|
+
const isSevere = reportedError.code === ERROR_CODES.DB_FAILURE ||
|
|
459
|
+
reportedError.code === ERROR_CODES.REPO_FETCH_FAILED ||
|
|
460
|
+
reportedError.code === ERROR_CODES.REGISTRY_GENERATION_FAILED ||
|
|
461
|
+
reportedError.code === ERROR_CODES.JAVA_UNAVAILABLE ||
|
|
462
|
+
reportedError.code.startsWith("ERR_DECOMPILER");
|
|
441
463
|
if (isSevere) {
|
|
442
464
|
log("error", "tool.call.failed", {
|
|
443
465
|
requestId,
|
|
444
466
|
tool,
|
|
445
|
-
code:
|
|
446
|
-
message:
|
|
467
|
+
code: reportedError.code,
|
|
468
|
+
message: reportedError.message
|
|
447
469
|
});
|
|
448
470
|
}
|
|
449
471
|
else {
|
|
450
472
|
log("warn", "tool.call.warning", {
|
|
451
473
|
requestId,
|
|
452
474
|
tool,
|
|
453
|
-
code:
|
|
454
|
-
message:
|
|
475
|
+
code: reportedError.code,
|
|
476
|
+
message: reportedError.message
|
|
455
477
|
});
|
|
456
478
|
}
|
|
457
479
|
}
|
|
458
|
-
else if (!(
|
|
480
|
+
else if (!(reportedError instanceof ZodError)) {
|
|
459
481
|
log("error", "tool.call.unhandled", {
|
|
460
482
|
requestId,
|
|
461
483
|
tool,
|
|
462
|
-
reason:
|
|
484
|
+
reason: reportedError instanceof Error ? reportedError.message : String(reportedError)
|
|
463
485
|
});
|
|
464
486
|
}
|
|
465
487
|
const errorDurationMs = Date.now() - startedAt;
|
|
@@ -469,7 +491,7 @@ async function runTool(tool, rawInput, schema, action) {
|
|
|
469
491
|
tool,
|
|
470
492
|
durationMs: errorDurationMs
|
|
471
493
|
};
|
|
472
|
-
applyErrorMetaExtensions(errorMeta,
|
|
494
|
+
applyErrorMetaExtensions(errorMeta, reportedError);
|
|
473
495
|
return objectResult({
|
|
474
496
|
error: problem,
|
|
475
497
|
meta: errorMeta
|
|
@@ -688,6 +710,7 @@ function buildServer(ctx) {
|
|
|
688
710
|
access: input.access,
|
|
689
711
|
includeSynthetic: input.includeSynthetic,
|
|
690
712
|
includeInherited: input.includeInherited,
|
|
713
|
+
includeAnnotations: input.includeAnnotations,
|
|
691
714
|
memberPattern: input.memberPattern,
|
|
692
715
|
maxMembers: input.maxMembers,
|
|
693
716
|
projection: input.projection,
|
package/dist/java-process.d.ts
CHANGED
package/dist/java-process.js
CHANGED
|
@@ -3,6 +3,11 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
3
3
|
import { normalizePathForHost } from "./path-converter.js";
|
|
4
4
|
const JAVA_CHECK_TIMEOUT_MS = 2_000;
|
|
5
5
|
export const MAX_STDIO_SNAPSHOT = 6_240;
|
|
6
|
+
// How long to wait after SIGTERM before escalating to SIGKILL on a timed-out
|
|
7
|
+
// Java process. This is a single leaf process (not a process tree — the
|
|
8
|
+
// supervisor's tree-termination path is a separate concern), so a direct
|
|
9
|
+
// SIGKILL here is the sanctioned escalation, not a workaround.
|
|
10
|
+
export const JAVA_PROCESS_KILL_GRACE_MS = 3_000;
|
|
6
11
|
let javaAvailabilitySpawn = spawn;
|
|
7
12
|
let javaAvailabilityPromise;
|
|
8
13
|
export function limitStdio(text) {
|
|
@@ -95,8 +100,15 @@ export function runJavaProcess(options) {
|
|
|
95
100
|
});
|
|
96
101
|
let stdout = "";
|
|
97
102
|
let stderr = "";
|
|
103
|
+
let killTimer;
|
|
98
104
|
const timer = setTimeout(() => {
|
|
99
105
|
proc.kill();
|
|
106
|
+
killTimer = setTimeout(() => {
|
|
107
|
+
// The process ignored SIGTERM past the grace period; escalate to
|
|
108
|
+
// SIGKILL so a hung JVM cannot outlive the timeout indefinitely.
|
|
109
|
+
proc.kill("SIGKILL");
|
|
110
|
+
}, JAVA_PROCESS_KILL_GRACE_MS);
|
|
111
|
+
killTimer.unref();
|
|
100
112
|
reject(createError({
|
|
101
113
|
code: ERROR_CODES.JAVA_PROCESS_FAILED,
|
|
102
114
|
message: "Java process timed out.",
|
|
@@ -118,6 +130,7 @@ export function runJavaProcess(options) {
|
|
|
118
130
|
});
|
|
119
131
|
proc.once("error", (error) => {
|
|
120
132
|
clearTimeout(timer);
|
|
133
|
+
clearTimeout(killTimer);
|
|
121
134
|
reject(createError({
|
|
122
135
|
code: ERROR_CODES.JAVA_PROCESS_FAILED,
|
|
123
136
|
message: "Java process failed to start.",
|
|
@@ -129,6 +142,7 @@ export function runJavaProcess(options) {
|
|
|
129
142
|
});
|
|
130
143
|
proc.once("close", (code) => {
|
|
131
144
|
clearTimeout(timer);
|
|
145
|
+
clearTimeout(killTimer);
|
|
132
146
|
resolve({
|
|
133
147
|
exitCode: code ?? -1,
|
|
134
148
|
stdoutTail: limitStdio(stdout),
|
package/dist/mapping/lookup.js
CHANGED
|
@@ -55,6 +55,15 @@ export function lookupCandidates(index, query) {
|
|
|
55
55
|
addCandidates(collected, index, index.simple.get(key), "simple-name", 0.75);
|
|
56
56
|
}
|
|
57
57
|
return [...collected.values()]
|
|
58
|
+
// A lookup answers only with candidates of the query's own kind. The index buckets
|
|
59
|
+
// deliberately mix kinds: `exactLookupKeys` registers every method a second time
|
|
60
|
+
// under the descriptorless `owner.name` key that a FIELD's symbol also uses, and
|
|
61
|
+
// `simpleLookupKeys` files classes, fields and methods under the same bare name. So
|
|
62
|
+
// a field query could be answered with a same-named method's target name at
|
|
63
|
+
// exact/confidence 1, or with a class that merely shares its simple name. Filtering
|
|
64
|
+
// here — before the MAX_CANDIDATES slice — also keeps wrong-kind rows from crowding
|
|
65
|
+
// the right one out of a busy name's bucket.
|
|
66
|
+
.filter((candidate) => candidate.record.kind === query.kind)
|
|
58
67
|
.sort((left, right) => {
|
|
59
68
|
if (right.confidence !== left.confidence) {
|
|
60
69
|
return right.confidence - left.confidence;
|
|
@@ -415,14 +424,20 @@ export function projectLookupCandidateDescriptor(candidate, sourceDescriptor, ta
|
|
|
415
424
|
// namespace. Multi-hop paths that already produced a target-side descriptor are left
|
|
416
425
|
// unchanged by design.
|
|
417
426
|
if (candidate.kind !== "method" ||
|
|
427
|
+
!candidate.owner ||
|
|
418
428
|
!candidate.descriptor ||
|
|
419
429
|
!targetDescriptor ||
|
|
420
430
|
candidate.descriptor !== sourceDescriptor) {
|
|
421
431
|
return candidate;
|
|
422
432
|
}
|
|
433
|
+
// `symbol` embeds the descriptor, so replacing only the `descriptor` field left the
|
|
434
|
+
// SOURCE-namespace descriptor inside an otherwise target-namespace symbol — one
|
|
435
|
+
// SymbolReference advertising two namespaces at once. Rebuild it from the target
|
|
436
|
+
// owner + name + descriptor with the same helper the parsers use.
|
|
423
437
|
return {
|
|
424
438
|
...candidate,
|
|
425
|
-
descriptor: targetDescriptor
|
|
439
|
+
descriptor: targetDescriptor,
|
|
440
|
+
symbol: createMethodSymbolRecord(candidate.owner, candidate.name, targetDescriptor).symbol
|
|
426
441
|
};
|
|
427
442
|
}
|
|
428
443
|
export function effectiveLoomSearchProjectPath(projectPath) {
|
|
@@ -56,6 +56,20 @@ export declare class MappingService {
|
|
|
56
56
|
* accepted alias so an owner that the mapping does not rename still matches itself.
|
|
57
57
|
*/
|
|
58
58
|
private acceptedOwnersAlongPath;
|
|
59
|
+
/**
|
|
60
|
+
* The descriptor forms a stored record may legitimately carry for a query written
|
|
61
|
+
* in `sourceMapping` coordinates: the caller's own descriptor, the target-space
|
|
62
|
+
* projection, and the projection into each base namespace.
|
|
63
|
+
*
|
|
64
|
+
* Tiny v2 writes one descriptor per method row, in the file's FIRST namespace, and
|
|
65
|
+
* shares it across every column. A merged Loom rendering puts that in obfuscated
|
|
66
|
+
* coordinates, but a workspace whose `official intermediary` and `intermediary named`
|
|
67
|
+
* mappings are SEPARATE files leaves the last hop's record carrying an
|
|
68
|
+
* intermediary-coordinate descriptor, which an obfuscated-only accepted set rejected
|
|
69
|
+
* as `not_found`. Projecting along every reachable base namespace is the same rule
|
|
70
|
+
* `checkSymbolExists` applies.
|
|
71
|
+
*/
|
|
72
|
+
private acceptedDescriptorForms;
|
|
59
73
|
private projectMethodDescriptorToTarget;
|
|
60
74
|
private provenanceForPath;
|
|
61
75
|
/**
|
package/dist/mapping-service.js
CHANGED
|
@@ -193,15 +193,10 @@ export class MappingService {
|
|
|
193
193
|
// only against the fully projected target descriptor dropped those valid candidates
|
|
194
194
|
// and produced false `not_found` for common Mojang -> Yarn lookups. Accept any
|
|
195
195
|
// candidate whose descriptor matches the caller's descriptor, the target-space
|
|
196
|
-
// projection, or
|
|
197
|
-
//
|
|
196
|
+
// projection, or a base-namespace projection — the forms that actually appear in
|
|
197
|
+
// the mapping graph.
|
|
198
198
|
const strictDescriptor = projectedDescriptor ?? queryRecord.descriptor;
|
|
199
|
-
const acceptedDescriptors =
|
|
200
|
-
const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
201
|
-
if (toObfuscatedPath) {
|
|
202
|
-
const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, queryRecord.descriptor);
|
|
203
|
-
acceptedDescriptors.add(obfuscatedProjection.descriptor);
|
|
204
|
-
}
|
|
199
|
+
const acceptedDescriptors = this.acceptedDescriptorForms(graph, sourceMapping, queryRecord.descriptor, strictDescriptor);
|
|
205
200
|
rawCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
|
|
206
201
|
}
|
|
207
202
|
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation, warnings);
|
|
@@ -402,12 +397,7 @@ export class MappingService {
|
|
|
402
397
|
const candidates = rawCandidates.map(toResolutionCandidate);
|
|
403
398
|
const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
|
|
404
399
|
const strictDescriptor = projectedDescriptor ?? descriptor;
|
|
405
|
-
const acceptedDescriptors =
|
|
406
|
-
const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
407
|
-
if (toObfuscatedPath) {
|
|
408
|
-
const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, descriptor);
|
|
409
|
-
acceptedDescriptors.add(obfuscatedProjection.descriptor);
|
|
410
|
-
}
|
|
400
|
+
const acceptedDescriptors = this.acceptedDescriptorForms(graph, sourceMapping, descriptor, strictDescriptor);
|
|
411
401
|
// "Strict" is the query's full advertised triple: owner + name + descriptor. The
|
|
412
402
|
// descriptor alone is not enough, because `lookupCandidates` also indexes methods
|
|
413
403
|
// under the OWNERLESS `<name><descriptor>` simple-name key — so a same-signature
|
|
@@ -1069,6 +1059,33 @@ export class MappingService {
|
|
|
1069
1059
|
}
|
|
1070
1060
|
return accepted;
|
|
1071
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* The descriptor forms a stored record may legitimately carry for a query written
|
|
1064
|
+
* in `sourceMapping` coordinates: the caller's own descriptor, the target-space
|
|
1065
|
+
* projection, and the projection into each base namespace.
|
|
1066
|
+
*
|
|
1067
|
+
* Tiny v2 writes one descriptor per method row, in the file's FIRST namespace, and
|
|
1068
|
+
* shares it across every column. A merged Loom rendering puts that in obfuscated
|
|
1069
|
+
* coordinates, but a workspace whose `official intermediary` and `intermediary named`
|
|
1070
|
+
* mappings are SEPARATE files leaves the last hop's record carrying an
|
|
1071
|
+
* intermediary-coordinate descriptor, which an obfuscated-only accepted set rejected
|
|
1072
|
+
* as `not_found`. Projecting along every reachable base namespace is the same rule
|
|
1073
|
+
* `checkSymbolExists` applies.
|
|
1074
|
+
*/
|
|
1075
|
+
acceptedDescriptorForms(graph, sourceMapping, descriptor, targetDescriptor) {
|
|
1076
|
+
const accepted = new Set([descriptor, targetDescriptor]);
|
|
1077
|
+
for (const baseNamespace of ["obfuscated", "intermediary"]) {
|
|
1078
|
+
if (sourceMapping === baseNamespace) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
const projectionPath = namespacePath(graph, sourceMapping, baseNamespace);
|
|
1082
|
+
if (!projectionPath) {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
accepted.add(this.projectMethodDescriptorToTarget(graph, projectionPath, descriptor).descriptor);
|
|
1086
|
+
}
|
|
1087
|
+
return accepted;
|
|
1088
|
+
}
|
|
1072
1089
|
projectMethodDescriptorToTarget(graph, path, descriptor) {
|
|
1073
1090
|
let hadClassReferences = false;
|
|
1074
1091
|
let complete = true;
|
|
@@ -1156,7 +1173,10 @@ export class MappingService {
|
|
|
1156
1173
|
return {
|
|
1157
1174
|
mojangMappingsAvailable: false,
|
|
1158
1175
|
tinyMappingsAvailable: !needsTinyMappings,
|
|
1159
|
-
|
|
1176
|
+
// Obfuscated requests never need a remap path (the target is already
|
|
1177
|
+
// obfuscated), same as the loaded-graph branch below — a failed graph
|
|
1178
|
+
// load does not degrade an obfuscated request's remap availability.
|
|
1179
|
+
memberRemapAvailable: input.requestedMapping === "obfuscated",
|
|
1160
1180
|
degradations: ["Mapping graph could not be loaded."]
|
|
1161
1181
|
};
|
|
1162
1182
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
|
-
import { createError, ERROR_CODES } from "./errors.js";
|
|
2
|
+
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
4
|
import { normalizeJarPath, readJarStatStamp } from "./path-resolver.js";
|
|
5
5
|
import { createJarEntryReader } from "./source-jar-reader.js";
|
|
@@ -447,22 +447,44 @@ class ByteReader {
|
|
|
447
447
|
constructor(buffer) {
|
|
448
448
|
this.buffer = buffer;
|
|
449
449
|
}
|
|
450
|
+
// Mirrors NbtReader's ensure() (src/nbt/java-nbt-codec.ts). Without this,
|
|
451
|
+
// Buffer.subarray silently clips a length past EOF instead of throwing, so
|
|
452
|
+
// a truncated attribute body (declared length intact, trailing bytes
|
|
453
|
+
// missing) reads as a shorter-than-declared Buffer and the offset still
|
|
454
|
+
// advances past the true end - readBytes "succeeds" on corrupted data.
|
|
455
|
+
ensure(length) {
|
|
456
|
+
if (this.offset + length > this.buffer.length) {
|
|
457
|
+
throw createError({
|
|
458
|
+
code: ERROR_CODES.INTERNAL,
|
|
459
|
+
message: "Unexpected end of class file data. Class file may be corrupted.",
|
|
460
|
+
details: {
|
|
461
|
+
offset: this.offset,
|
|
462
|
+
requiredBytes: length,
|
|
463
|
+
remainingBytes: this.buffer.length - this.offset
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
450
468
|
readU1() {
|
|
469
|
+
this.ensure(1);
|
|
451
470
|
const value = this.buffer.readUInt8(this.offset);
|
|
452
471
|
this.offset += 1;
|
|
453
472
|
return value;
|
|
454
473
|
}
|
|
455
474
|
readU2() {
|
|
475
|
+
this.ensure(2);
|
|
456
476
|
const value = this.buffer.readUInt16BE(this.offset);
|
|
457
477
|
this.offset += 2;
|
|
458
478
|
return value;
|
|
459
479
|
}
|
|
460
480
|
readU4() {
|
|
481
|
+
this.ensure(4);
|
|
461
482
|
const value = this.buffer.readUInt32BE(this.offset);
|
|
462
483
|
this.offset += 4;
|
|
463
484
|
return value;
|
|
464
485
|
}
|
|
465
486
|
readBytes(length) {
|
|
487
|
+
this.ensure(length);
|
|
466
488
|
const slice = this.buffer.subarray(this.offset, this.offset + length);
|
|
467
489
|
this.offset += length;
|
|
468
490
|
return slice;
|
|
@@ -773,7 +795,26 @@ export class MinecraftExplorerService {
|
|
|
773
795
|
details: { fqn, jarPath, classEntryPath }
|
|
774
796
|
});
|
|
775
797
|
}
|
|
776
|
-
|
|
798
|
+
try {
|
|
799
|
+
parsed = parseClassFile(classBuffer);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
// A primary-class parse failure of ANY origin - a raw Buffer bounds
|
|
803
|
+
// error (ByteReader has no bounds checks), or one of parseClassFile's
|
|
804
|
+
// own deliberate throws (bad magic, unsupported constant-pool tag) -
|
|
805
|
+
// is third-party jar content, never blamed on the caller as
|
|
806
|
+
// ERR_INTERNAL. An AppError that already carries ERR_CLASS_NOT_FOUND
|
|
807
|
+
// (readUtf8/readClassName) is already in the right shape and passes
|
|
808
|
+
// through unchanged.
|
|
809
|
+
if (isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND) {
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
throw createError({
|
|
813
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
814
|
+
message: `Class "${fqn}" in "${jarPath}" is malformed or truncated. Class file may be corrupted.`,
|
|
815
|
+
details: { fqn, jarPath, classEntryPath }
|
|
816
|
+
});
|
|
817
|
+
}
|
|
777
818
|
}
|
|
778
819
|
catch (error) {
|
|
779
820
|
reader.close();
|
|
@@ -863,12 +904,23 @@ export class MinecraftExplorerService {
|
|
|
863
904
|
reader.close();
|
|
864
905
|
const toSignatureMember = (ownerFqn, ownerSimpleClassName, member, category) => {
|
|
865
906
|
if (category === "field") {
|
|
866
|
-
|
|
867
|
-
|
|
907
|
+
// parseFieldType/parseMethodDescriptor throw ERR_INVALID_INPUT, which
|
|
908
|
+
// blames the caller; a member descriptor comes from third-party
|
|
909
|
+
// bytecode, not tool input, so a malformed one is treated like the
|
|
910
|
+
// corrupted-class failure above (src/source/descriptor-utils.ts
|
|
911
|
+
// degrades the same parsers rather than throwing at all).
|
|
912
|
+
let parsedField;
|
|
913
|
+
try {
|
|
914
|
+
parsedField = parseFieldType(member.descriptor, 0, { allowVoid: false });
|
|
915
|
+
if (parsedField.next !== member.descriptor.length) {
|
|
916
|
+
throw new Error("Trailing bytes after field descriptor.");
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
868
920
|
throw createError({
|
|
869
|
-
code: ERROR_CODES.
|
|
870
|
-
message: `
|
|
871
|
-
details: {
|
|
921
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
922
|
+
message: `Field "${member.name}" in "${ownerFqn}" has a malformed descriptor in "${jarPath}". Class file may be corrupted.`,
|
|
923
|
+
details: { ownerFqn, jarPath, member: member.name, descriptor: member.descriptor }
|
|
872
924
|
});
|
|
873
925
|
}
|
|
874
926
|
const fieldType = parsedField.type;
|
|
@@ -884,7 +936,17 @@ export class MinecraftExplorerService {
|
|
|
884
936
|
...(member.annotations ? { annotations: member.annotations } : {})
|
|
885
937
|
};
|
|
886
938
|
}
|
|
887
|
-
|
|
939
|
+
let parsedMethod;
|
|
940
|
+
try {
|
|
941
|
+
parsedMethod = parseMethodDescriptor(member.descriptor);
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
throw createError({
|
|
945
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
946
|
+
message: `Method "${member.name}" in "${ownerFqn}" has a malformed descriptor in "${jarPath}". Class file may be corrupted.`,
|
|
947
|
+
details: { ownerFqn, jarPath, member: member.name, descriptor: member.descriptor }
|
|
948
|
+
});
|
|
949
|
+
}
|
|
888
950
|
const modifiers = modifierPrefix(member.accessFlags, "method");
|
|
889
951
|
const args = parsedMethod.args.join(", ");
|
|
890
952
|
if (member.name === "<init>") {
|
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
import { accessLevelFromFlags, allFieldNames, allMethodNames, suggestSimilar } from "./helpers.js";
|
|
2
|
+
const VALID_AW_HEADER_VERSIONS = new Set(["v1", "v2"]);
|
|
3
|
+
/**
|
|
4
|
+
* Fabric access widener grammar rules (AUTHORITY): `accessible` applies to
|
|
5
|
+
* class, method, field; `extendable` applies to class and method only;
|
|
6
|
+
* `mutable` applies to field only; `transitive-` exists only under a v2
|
|
7
|
+
* header. Returns a line+rule message when `entry` breaks one of these, or
|
|
8
|
+
* undefined when the pairing is legal.
|
|
9
|
+
*/
|
|
10
|
+
function checkAccessWidenerEntryLegality(entry, headerVersion) {
|
|
11
|
+
if (entry.kind === "mutable" && entry.targetKind !== "field") {
|
|
12
|
+
return `Line ${entry.line}: "mutable" only applies to field, not ${entry.targetKind}.`;
|
|
13
|
+
}
|
|
14
|
+
if (entry.kind === "extendable" && entry.targetKind === "field") {
|
|
15
|
+
return `Line ${entry.line}: "extendable" does not apply to field (only class and method).`;
|
|
16
|
+
}
|
|
17
|
+
if (entry.transitive && headerVersion !== "v2") {
|
|
18
|
+
return `Line ${entry.line}: "transitive-" entries require a v2 header (declared "${headerVersion || "none"}").`;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
2
22
|
export function validateParsedAccessWidener(parsed, membersByClass, warnings, options) {
|
|
3
23
|
warnings.push(...parsed.parseWarnings);
|
|
24
|
+
let headerValid = true;
|
|
25
|
+
const headerLine = parsed.headerLine ?? 1;
|
|
26
|
+
if (!parsed.headerVersion) {
|
|
27
|
+
headerValid = false;
|
|
28
|
+
warnings.push(`Line ${headerLine}: accessWidener requires a header line ("accessWidener <v1|v2> <namespace>"); none was found.`);
|
|
29
|
+
}
|
|
30
|
+
else if (!VALID_AW_HEADER_VERSIONS.has(parsed.headerVersion)) {
|
|
31
|
+
headerValid = false;
|
|
32
|
+
warnings.push(`Line ${headerLine}: Unsupported accessWidener header version "${parsed.headerVersion}" (expected v1 or v2).`);
|
|
33
|
+
}
|
|
4
34
|
const validatedEntries = [];
|
|
5
35
|
let validCount = 0;
|
|
6
36
|
let invalidCount = 0;
|
|
7
37
|
for (const entry of parsed.entries) {
|
|
38
|
+
const legalityIssue = checkAccessWidenerEntryLegality(entry, parsed.headerVersion);
|
|
39
|
+
if (legalityIssue) {
|
|
40
|
+
validatedEntries.push({ ...entry, valid: false, issue: legalityIssue });
|
|
41
|
+
invalidCount++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
8
44
|
const ownerFqn = entry.target.replace(/\//g, ".");
|
|
9
45
|
if (entry.targetKind === "class") {
|
|
10
46
|
const members = membersByClass.get(ownerFqn);
|
|
@@ -130,7 +166,7 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings, op
|
|
|
130
166
|
return {
|
|
131
167
|
headerVersion: parsed.headerVersion,
|
|
132
168
|
namespace: parsed.namespace,
|
|
133
|
-
valid: invalidCount === 0,
|
|
169
|
+
valid: invalidCount === 0 && headerValid && (parsed.rejectedEntryCount ?? 0) === 0,
|
|
134
170
|
entries: validatedEntries,
|
|
135
171
|
summary: {
|
|
136
172
|
total: parsed.entries.length,
|
|
@@ -276,7 +312,7 @@ export function validateParsedAccessTransformer(parsed, membersByClass, warnings
|
|
|
276
312
|
validCount++;
|
|
277
313
|
}
|
|
278
314
|
return {
|
|
279
|
-
valid: invalidCount === 0,
|
|
315
|
+
valid: invalidCount === 0 && (parsed.rejectedEntryCount ?? 0) === 0,
|
|
280
316
|
entries: validatedEntries,
|
|
281
317
|
summary: {
|
|
282
318
|
total: parsed.entries.length,
|