@adhisang/minecraft-modding-mcp 7.0.0 → 7.1.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 +30 -0
- package/README.md +3 -2
- 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-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.d.ts +2 -2
- package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/index.js +37 -15
- package/dist/source/artifact-resolver.d.ts +14 -0
- package/dist/source/artifact-resolver.js +106 -12
- 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 +173 -25
- 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/symbol-resolver.js +88 -0
- package/dist/source-jar-reader.d.ts +33 -0
- package/dist/source-jar-reader.js +58 -0
- package/dist/source-resolver.d.ts +7 -0
- package/dist/source-resolver.js +20 -5
- package/dist/source-service.d.ts +5 -0
- package/dist/source-service.js +7 -0
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +181 -20
- package/dist/storage/sqlite.d.ts +31 -1
- package/dist/storage/sqlite.js +125 -16
- 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 +45 -3
- package/dist/workspace-mapping-service.d.ts +8 -0
- package/dist/workspace-mapping-service.js +35 -7
- package/docs/README-ja.md +2 -0
- package/docs/tool-reference.md +55 -11
- package/package.json +1 -1
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { ERROR_CODES, isAppError } from "../../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* check-symbol-exists's runtime fallback: the result of `runUnobfuscatedRuntimeCheck`
|
|
4
|
+
* alone. Its callers keep the fallback base's status whenever the check could not be
|
|
5
|
+
* performed, so they need no `verified` flag.
|
|
6
|
+
*/
|
|
2
7
|
export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbackBase) {
|
|
8
|
+
return (await runUnobfuscatedRuntimeCheck(svc, input, fallbackBase))?.result;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks a symbol against the Minecraft runtime jar of an unobfuscated version, whose
|
|
12
|
+
* names are the Mojang names. Undefined when the version or name is empty or the
|
|
13
|
+
* runtime jar cannot be resolved.
|
|
14
|
+
*/
|
|
15
|
+
export async function runUnobfuscatedRuntimeCheck(svc, input, fallbackBase) {
|
|
3
16
|
const version = input.version.trim();
|
|
4
17
|
const name = input.name.trim();
|
|
5
18
|
const owner = input.owner?.trim();
|
|
@@ -8,11 +21,14 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
8
21
|
}
|
|
9
22
|
if (input.kind === "class" && input.nameMode !== "fqcn" && !name.includes(".")) {
|
|
10
23
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
...fallbackBase
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
verified: false,
|
|
25
|
+
result: {
|
|
26
|
+
...fallbackBase,
|
|
27
|
+
warnings: [
|
|
28
|
+
...fallbackBase.warnings,
|
|
29
|
+
`Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
|
|
30
|
+
]
|
|
31
|
+
}
|
|
16
32
|
};
|
|
17
33
|
}
|
|
18
34
|
const querySymbol = input.kind === "class"
|
|
@@ -37,7 +53,7 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
37
53
|
};
|
|
38
54
|
const targetClass = input.kind === "class" ? name : owner;
|
|
39
55
|
if (!targetClass) {
|
|
40
|
-
return fallbackBase;
|
|
56
|
+
return { verified: false, result: fallbackBase };
|
|
41
57
|
}
|
|
42
58
|
let jarPath;
|
|
43
59
|
try {
|
|
@@ -61,16 +77,20 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
61
77
|
});
|
|
62
78
|
}
|
|
63
79
|
catch (error) {
|
|
80
|
+
// Only CLASS_NOT_FOUND is an answer; any other failure means the lookup never completed.
|
|
64
81
|
const classMissing = isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND;
|
|
65
82
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
verified: classMissing,
|
|
84
|
+
result: {
|
|
85
|
+
...fallbackBase,
|
|
86
|
+
querySymbol,
|
|
87
|
+
warnings: [
|
|
88
|
+
...fallbackBase.warnings,
|
|
89
|
+
classMissing
|
|
90
|
+
? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
|
|
91
|
+
: `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
|
|
92
|
+
]
|
|
93
|
+
}
|
|
74
94
|
};
|
|
75
95
|
}
|
|
76
96
|
const warnings = [...fallbackBase.warnings, ...signature.warnings];
|
|
@@ -80,33 +100,40 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
80
100
|
...fallbackBase.mappingContext,
|
|
81
101
|
runtimeValidated: true
|
|
82
102
|
};
|
|
103
|
+
// From here on the class loaded, so every answer below is a completed lookup.
|
|
83
104
|
const buildResolved = (resolvedSymbol) => ({
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
verified: true,
|
|
106
|
+
result: {
|
|
107
|
+
...fallbackBase,
|
|
108
|
+
mappingContext: runtimeValidatedContext,
|
|
109
|
+
querySymbol,
|
|
110
|
+
resolved: true,
|
|
111
|
+
status: "resolved",
|
|
112
|
+
resolvedSymbol,
|
|
113
|
+
candidates: resolvedSymbol
|
|
114
|
+
? [{
|
|
115
|
+
...resolvedSymbol,
|
|
116
|
+
matchKind: "exact",
|
|
117
|
+
confidence: 1
|
|
118
|
+
}]
|
|
119
|
+
: [],
|
|
120
|
+
candidateCount: resolvedSymbol ? 1 : 0,
|
|
121
|
+
warnings
|
|
122
|
+
}
|
|
99
123
|
});
|
|
100
124
|
const buildUnresolved = (status) => ({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
verified: true,
|
|
126
|
+
result: {
|
|
127
|
+
...fallbackBase,
|
|
128
|
+
mappingContext: runtimeValidatedContext,
|
|
129
|
+
querySymbol,
|
|
130
|
+
resolved: false,
|
|
131
|
+
status,
|
|
132
|
+
resolvedSymbol: undefined,
|
|
133
|
+
candidates: [],
|
|
134
|
+
candidateCount: 0,
|
|
135
|
+
warnings
|
|
136
|
+
}
|
|
110
137
|
});
|
|
111
138
|
if (input.kind === "class") {
|
|
112
139
|
return buildResolved({
|
|
@@ -127,7 +154,9 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
127
154
|
symbol: `${owner}.${name}`
|
|
128
155
|
});
|
|
129
156
|
}
|
|
130
|
-
|
|
157
|
+
// The bytecode reader lists constructors apart from methods (and only the owner's
|
|
158
|
+
// own: constructors are not inherited), so "<init>" is answered from them.
|
|
159
|
+
const methodCandidates = (name === "<init>" ? signature.constructors : signature.methods).filter((method) => method.name === name);
|
|
131
160
|
const signatureMode = input.signatureMode ?? "name-only";
|
|
132
161
|
if (signatureMode === "name-only") {
|
|
133
162
|
// Existence semantics: any overload with this name means the method exists. Multiple
|
|
@@ -1,4 +1,83 @@
|
|
|
1
1
|
import { ERROR_CODES, createError } from "../errors.js";
|
|
2
|
+
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
3
|
+
import { runUnobfuscatedRuntimeCheck } from "./lifecycle/runtime-check.js";
|
|
4
|
+
/**
|
|
5
|
+
* Minecraft 26.1+ ships its runtime in Mojang names, so a Loom project for it has no
|
|
6
|
+
* `mappings` declaration: it compiles against the runtime names directly. Resolving a
|
|
7
|
+
* workspace symbol there is an identity lookup, checked against runtime bytecode the
|
|
8
|
+
* same way checkSymbolExists validates unobfuscated versions.
|
|
9
|
+
*/
|
|
10
|
+
async function resolveUnobfuscatedWorkspaceSymbol(svc, input, context) {
|
|
11
|
+
const { version, querySymbol, sourcePriorityApplied } = context;
|
|
12
|
+
const workspaceDetection = {
|
|
13
|
+
resolved: true,
|
|
14
|
+
mappingApplied: "mojang",
|
|
15
|
+
evidence: [],
|
|
16
|
+
warnings: [
|
|
17
|
+
`Minecraft ${version} is unobfuscated; no mappings declaration is needed — symbols are resolved against runtime (Mojang) names.`
|
|
18
|
+
]
|
|
19
|
+
};
|
|
20
|
+
// The requested/defaulted sourceMapping label is echoed unchanged: on 26.1+ an
|
|
21
|
+
// "obfuscated" label already means the as-shipped Mojang names, and rewriting it to
|
|
22
|
+
// "mojang" would break callers that compare namespace labels for equality.
|
|
23
|
+
const base = {
|
|
24
|
+
querySymbol,
|
|
25
|
+
mappingContext: {
|
|
26
|
+
version,
|
|
27
|
+
sourceMapping: input.sourceMapping,
|
|
28
|
+
targetMapping: "mojang",
|
|
29
|
+
sourcePriorityApplied,
|
|
30
|
+
unobfuscatedRuntime: true
|
|
31
|
+
},
|
|
32
|
+
resolved: false,
|
|
33
|
+
status: "not_found",
|
|
34
|
+
candidates: [],
|
|
35
|
+
candidateCount: 0,
|
|
36
|
+
warnings: [...workspaceDetection.warnings]
|
|
37
|
+
};
|
|
38
|
+
// The runtime names are both the "obfuscated" (as-shipped) and the Mojang names;
|
|
39
|
+
// intermediary and yarn names do not exist for these versions.
|
|
40
|
+
if (input.sourceMapping !== "obfuscated" && input.sourceMapping !== "mojang") {
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
status: "mapping_unavailable",
|
|
44
|
+
workspaceDetection,
|
|
45
|
+
warnings: [
|
|
46
|
+
...base.warnings,
|
|
47
|
+
`sourceMapping "${input.sourceMapping}" has no names on unobfuscated Minecraft ${version}; pass sourceMapping "mojang" (or "obfuscated") to resolve against runtime names.`
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const runtime = await runUnobfuscatedRuntimeCheck(svc, {
|
|
52
|
+
version,
|
|
53
|
+
kind: querySymbol.kind,
|
|
54
|
+
name: querySymbol.name,
|
|
55
|
+
owner: querySymbol.owner,
|
|
56
|
+
descriptor: querySymbol.descriptor,
|
|
57
|
+
sourceMapping: input.sourceMapping,
|
|
58
|
+
signatureMode: "exact"
|
|
59
|
+
}, base);
|
|
60
|
+
if (!runtime) {
|
|
61
|
+
// Mirrors checkSymbolExists: a symbol that could not be checked is not reported missing.
|
|
62
|
+
return {
|
|
63
|
+
...base,
|
|
64
|
+
status: "mapping_unavailable",
|
|
65
|
+
workspaceDetection,
|
|
66
|
+
warnings: [
|
|
67
|
+
...base.warnings,
|
|
68
|
+
`Minecraft ${version} runtime jar could not be resolved; symbol existence was not verified.`
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!runtime.verified) {
|
|
73
|
+
// The jar was reached but did not answer (short class name, class that failed to
|
|
74
|
+
// load for a reason other than being absent): the result carries the reason, and
|
|
75
|
+
// an unanswered lookup is not a definitive miss. `not_found` stays reserved for a
|
|
76
|
+
// class confirmed missing or a member lookup that completed without a match.
|
|
77
|
+
return { ...runtime.result, status: "mapping_unavailable", workspaceDetection };
|
|
78
|
+
}
|
|
79
|
+
return { ...runtime.result, workspaceDetection };
|
|
80
|
+
}
|
|
2
81
|
export async function resolveWorkspaceSymbol(svc, input) {
|
|
3
82
|
const projectPath = input.projectPath?.trim();
|
|
4
83
|
const version = input.version?.trim();
|
|
@@ -77,6 +156,15 @@ export async function resolveWorkspaceSymbol(svc, input) {
|
|
|
77
156
|
const workspaceDetection = await svc.workspaceMappingService.detectCompileMapping({
|
|
78
157
|
projectPath
|
|
79
158
|
});
|
|
159
|
+
// No evidence also means no build script was found at all (a missing, mistyped or
|
|
160
|
+
// empty projectPath). Only a build that was read and declares no mappings is a
|
|
161
|
+
// 26.1+ Loom project compiling against the runtime names.
|
|
162
|
+
if (!workspaceDetection.resolved &&
|
|
163
|
+
workspaceDetection.evidence.length === 0 &&
|
|
164
|
+
isUnobfuscatedVersion(version) &&
|
|
165
|
+
(await svc.workspaceMappingService.hasReadableBuildScript(projectPath))) {
|
|
166
|
+
return resolveUnobfuscatedWorkspaceSymbol(svc, input, { version, querySymbol, sourcePriorityApplied });
|
|
167
|
+
}
|
|
80
168
|
const warnings = [...workspaceDetection.warnings];
|
|
81
169
|
if (!workspaceDetection.resolved || !workspaceDetection.mappingApplied) {
|
|
82
170
|
return {
|
|
@@ -53,6 +53,39 @@ export declare class EntryTooLargeError extends Error {
|
|
|
53
53
|
export declare function listJarEntries(jarPath: string): Promise<string[]>;
|
|
54
54
|
export declare function listJavaEntries(jarPath: string): Promise<string[]>;
|
|
55
55
|
export declare function hasAnyJarEntry(jarPath: string, predicate: (entryPath: string) => boolean): Promise<boolean>;
|
|
56
|
+
/** Root entry in which a Minecraft runtime jar names its own release (`"id": "26.2"`). */
|
|
57
|
+
export declare const MINECRAFT_VERSION_JSON_ENTRY = "version.json";
|
|
58
|
+
/** Class every Minecraft runtime jar ships, under its Mojang name, from 26.1 on. */
|
|
59
|
+
export declare const MINECRAFT_SHARED_CONSTANTS_ENTRY = "net/minecraft/SharedConstants.class";
|
|
60
|
+
/**
|
|
61
|
+
* What an archive walk saw that bears on "is this the Minecraft runtime jar?".
|
|
62
|
+
* Raw observations only: judging them is the resolver's job.
|
|
63
|
+
*/
|
|
64
|
+
export interface MinecraftRuntimeJarSignals {
|
|
65
|
+
hasSharedConstantsClass: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Text of the root `version.json`; absent when the entry is missing, larger
|
|
68
|
+
* than the bound, unreadable, or not UTF-8.
|
|
69
|
+
*/
|
|
70
|
+
versionJsonText?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface JavaSourceScan {
|
|
73
|
+
hasJavaSources: boolean;
|
|
74
|
+
/** Set only when the walk reached the end without meeting a `.java` entry. */
|
|
75
|
+
minecraftRuntimeSignals?: MinecraftRuntimeJarSignals;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `hasAnyJarEntry(jarPath, hasJavaSourceExtension)` that also collects the
|
|
79
|
+
* Minecraft runtime signals on the same walk.
|
|
80
|
+
*
|
|
81
|
+
* A jar without sources is already walked to its last entry to prove it has none,
|
|
82
|
+
* so noting two entry names on the way costs no extra open and no extra pass. The
|
|
83
|
+
* one entry read, `version.json`, uses the handle that is already open. A failure
|
|
84
|
+
* to read it only withholds the signal: it is evidence for a mapping decision, not
|
|
85
|
+
* the archive's verdict, so it never fails the scan. Errors opening or walking the
|
|
86
|
+
* archive propagate exactly as they do from `hasAnyJarEntry`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function scanJarForJavaSources(jarPath: string): Promise<JavaSourceScan>;
|
|
56
89
|
export declare function readJarEntryAsUtf8(jarPath: string, entryPath: string): Promise<string>;
|
|
57
90
|
/**
|
|
58
91
|
* Reads one entry fully into memory. `maxBytes` bounds that materialization and
|
|
@@ -183,6 +183,64 @@ export async function hasAnyJarEntry(jarPath, predicate) {
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
+
/** Root entry in which a Minecraft runtime jar names its own release (`"id": "26.2"`). */
|
|
187
|
+
export const MINECRAFT_VERSION_JSON_ENTRY = "version.json";
|
|
188
|
+
/** Class every Minecraft runtime jar ships, under its Mojang name, from 26.1 on. */
|
|
189
|
+
export const MINECRAFT_SHARED_CONSTANTS_ENTRY = "net/minecraft/SharedConstants.class";
|
|
190
|
+
/** The real file is under 1 KiB; anything far larger is not the file this looks for. */
|
|
191
|
+
const MAX_VERSION_JSON_BYTES = 64 * 1024;
|
|
192
|
+
/**
|
|
193
|
+
* `hasAnyJarEntry(jarPath, hasJavaSourceExtension)` that also collects the
|
|
194
|
+
* Minecraft runtime signals on the same walk.
|
|
195
|
+
*
|
|
196
|
+
* A jar without sources is already walked to its last entry to prove it has none,
|
|
197
|
+
* so noting two entry names on the way costs no extra open and no extra pass. The
|
|
198
|
+
* one entry read, `version.json`, uses the handle that is already open. A failure
|
|
199
|
+
* to read it only withholds the signal: it is evidence for a mapping decision, not
|
|
200
|
+
* the archive's verdict, so it never fails the scan. Errors opening or walking the
|
|
201
|
+
* archive propagate exactly as they do from `hasAnyJarEntry`.
|
|
202
|
+
*/
|
|
203
|
+
export async function scanJarForJavaSources(jarPath) {
|
|
204
|
+
return withZipFile(jarPath, async (zipFile) => {
|
|
205
|
+
let hasSharedConstantsClass = false;
|
|
206
|
+
let versionJsonEntry;
|
|
207
|
+
while (true) {
|
|
208
|
+
const entry = await readNextEntry(zipFile);
|
|
209
|
+
if (!entry) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (hasJavaSourceExtension(entry.fileName)) {
|
|
216
|
+
return { hasJavaSources: true };
|
|
217
|
+
}
|
|
218
|
+
if (entry.fileName === MINECRAFT_SHARED_CONSTANTS_ENTRY) {
|
|
219
|
+
hasSharedConstantsClass = true;
|
|
220
|
+
}
|
|
221
|
+
else if (entry.fileName === MINECRAFT_VERSION_JSON_ENTRY) {
|
|
222
|
+
versionJsonEntry = entry;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
let versionJsonText;
|
|
226
|
+
if (versionJsonEntry && versionJsonEntry.uncompressedSize <= MAX_VERSION_JSON_BYTES) {
|
|
227
|
+
try {
|
|
228
|
+
const buffer = await readEntryStream(zipFile, versionJsonEntry, jarPath, MAX_VERSION_JSON_BYTES);
|
|
229
|
+
versionJsonText = UTF8_DECODER.decode(buffer);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
versionJsonText = undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
hasJavaSources: false,
|
|
237
|
+
minecraftRuntimeSignals: {
|
|
238
|
+
hasSharedConstantsClass,
|
|
239
|
+
...(versionJsonText !== undefined ? { versionJsonText } : {})
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}
|
|
186
244
|
export async function readJarEntryAsUtf8(jarPath, entryPath) {
|
|
187
245
|
const contentBuffer = await readJarEntryAsBuffer(jarPath, entryPath);
|
|
188
246
|
return decodeUtf8OrThrow(contentBuffer, jarPath, entryPath);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Config, MappingVariant, ResolvedSourceArtifact, SourceTargetInput } from "./types.js";
|
|
2
2
|
import { type MavenCoordinate } from "./maven-resolver.js";
|
|
3
|
+
import { type JavaSourceScan } from "./source-jar-reader.js";
|
|
3
4
|
/**
|
|
4
5
|
* Every `~/.m2` path a coordinate could name, before any of them is checked
|
|
5
6
|
* against the filesystem.
|
|
@@ -59,6 +60,12 @@ export interface ResolveSourceTargetOptions {
|
|
|
59
60
|
* legacy hash for obfuscated and source-backed artifacts.
|
|
60
61
|
*/
|
|
61
62
|
mappingVariant?: MappingVariant;
|
|
63
|
+
/**
|
|
64
|
+
* Receives the result of walking the jar a `kind: "jar"` request names, once,
|
|
65
|
+
* when that walk ran. The resolver uses it to recognise a Minecraft runtime jar
|
|
66
|
+
* from its contents without walking the archive a second time.
|
|
67
|
+
*/
|
|
68
|
+
onSubjectJarScanned?: (scan: JavaSourceScan) => void;
|
|
62
69
|
onRepoFailover?: (event: {
|
|
63
70
|
stage: "source" | "binary";
|
|
64
71
|
repoUrl: string;
|
package/dist/source-resolver.js
CHANGED
|
@@ -7,15 +7,16 @@ import { buildRemoteBinaryUrls, buildRemoteSourceUrls, groupToPath, hasExistingJ
|
|
|
7
7
|
import { defaultDownloadPath, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
|
|
8
8
|
import { normalizeJarPath } from "./path-resolver.js";
|
|
9
9
|
import { composeArtifactId, contentDigestSignature, contentSignature, jarArtifactIdentity, DECOMPILE_SIGNATURE_QUALIFIER } from "./artifact-identity.js";
|
|
10
|
-
import { hasAnyJarEntry, hasJavaSourceExtension } from "./source-jar-reader.js";
|
|
10
|
+
import { hasAnyJarEntry, hasJavaSourceExtension, scanJarForJavaSources } from "./source-jar-reader.js";
|
|
11
11
|
/**
|
|
12
12
|
* Whether a jar contains java sources, with archive errors deliberately
|
|
13
13
|
* propagating.
|
|
14
14
|
*
|
|
15
15
|
* This is the check for a jar that IS the subject of the request - the
|
|
16
|
-
* `input.kind === "jar"` branch of `resolveSourceTarget`
|
|
17
|
-
* `resolvedJarPath
|
|
18
|
-
*
|
|
16
|
+
* `input.kind === "jar"` branch of `resolveSourceTarget` runs it on
|
|
17
|
+
* `resolvedJarPath` as `scanSubjectJarForJavaSources`, the same walk that also
|
|
18
|
+
* collects Minecraft runtime signals. What propagates is the reader's refusal
|
|
19
|
+
* to OPEN the archive: a truncated file, a non-zip file, an unreadable central directory,
|
|
19
20
|
* an I/O failure. On the subject jar that refusal is the verdict itself, and
|
|
20
21
|
* swallowing it would downgrade "this archive could not be read" into "this
|
|
21
22
|
* archive has no sources" - handing back a sibling `-sources.jar` as if the
|
|
@@ -73,6 +74,18 @@ async function candidateHasJavaSources(jarPath) {
|
|
|
73
74
|
return false;
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* `hasJavaSources` for the subject jar of a `kind: "jar"` request, returning the
|
|
79
|
+
* Minecraft runtime signals the same walk collected. Same contract on both counts
|
|
80
|
+
* that matter: an absent file answers "no sources" without opening anything, and
|
|
81
|
+
* an archive that cannot be opened propagates.
|
|
82
|
+
*/
|
|
83
|
+
async function scanSubjectJarForJavaSources(jarPath) {
|
|
84
|
+
if (!hasExistingJar(jarPath)) {
|
|
85
|
+
return { hasJavaSources: false };
|
|
86
|
+
}
|
|
87
|
+
return await scanJarForJavaSources(jarPath);
|
|
88
|
+
}
|
|
76
89
|
function resolveExactJarSourceCandidate(inputJarPath) {
|
|
77
90
|
const directory = dirname(inputJarPath);
|
|
78
91
|
const jarName = basename(inputJarPath);
|
|
@@ -526,7 +539,9 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
526
539
|
const adjacentSourceCandidates = await listAdjacentJarSourceCandidates(resolvedJarPath);
|
|
527
540
|
const maybeAdjacentSourceCandidates = adjacentSourceCandidates.length > 0 ? adjacentSourceCandidates : undefined;
|
|
528
541
|
const preferBinaryOnly = options.preferBinaryOnly ?? false;
|
|
529
|
-
|
|
542
|
+
const subjectScan = await scanSubjectJarForJavaSources(resolvedJarPath);
|
|
543
|
+
options.onSubjectJarScanned?.(subjectScan);
|
|
544
|
+
if (subjectScan.hasJavaSources) {
|
|
530
545
|
const siblingBinaryJarPath = resolveSiblingBinaryJarCandidate(resolvedJarPath);
|
|
531
546
|
const binaryJarPath = siblingBinaryJarPath ??
|
|
532
547
|
(basename(resolvedJarPath).endsWith("-sources.jar") ? undefined : resolvedJarPath);
|
package/dist/source-service.d.ts
CHANGED
|
@@ -671,6 +671,11 @@ export declare class SourceService {
|
|
|
671
671
|
listVersions(input?: ListVersionsInput): Promise<ListVersionsOutput>;
|
|
672
672
|
getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
|
|
673
673
|
compareVersions(input: CompareVersionsInput): Promise<CompareVersionsOutput>;
|
|
674
|
+
/**
|
|
675
|
+
* Thin passthrough so entry tools (migration-overview) can diff a version
|
|
676
|
+
* pair's `libraries` without reaching into VersionService directly.
|
|
677
|
+
*/
|
|
678
|
+
getVersionLibraries(version: string): Promise<string[]>;
|
|
674
679
|
decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
|
|
675
680
|
/**
|
|
676
681
|
* Member-level view of a third-party mod jar class, read from bytecode
|
package/dist/source-service.js
CHANGED
|
@@ -123,6 +123,13 @@ export class SourceService {
|
|
|
123
123
|
async compareVersions(input) {
|
|
124
124
|
return this.versionDiffService.compareVersions(input);
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Thin passthrough so entry tools (migration-overview) can diff a version
|
|
128
|
+
* pair's `libraries` without reaching into VersionService directly.
|
|
129
|
+
*/
|
|
130
|
+
async getVersionLibraries(version) {
|
|
131
|
+
return this.versionService.getVersionLibraries(version);
|
|
132
|
+
}
|
|
126
133
|
async decompileModJar(input) {
|
|
127
134
|
return this.modDecompileService.decompileModJar(input);
|
|
128
135
|
}
|
package/dist/storage/db.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import Database from "./sqlite.js";
|
|
1
|
+
import Database, { isRawSqliteCorruptionError } from "./sqlite.js";
|
|
2
2
|
import type { Config } from "../types.js";
|
|
3
|
+
export { isRawSqliteCorruptionError };
|
|
3
4
|
type SqliteDatabase = InstanceType<typeof Database>;
|
|
4
5
|
type Logger = {
|
|
5
6
|
warn: (message: string, details?: Record<string, unknown>) => void;
|
|
@@ -11,5 +12,64 @@ export interface InitializedDatabase {
|
|
|
11
12
|
schemaVersion: number;
|
|
12
13
|
}
|
|
13
14
|
type DatabaseConfig = Pick<Config, "sqlitePath"> & Partial<Pick<Config, "sqliteCacheKb" | "sqliteMmapSize">>;
|
|
15
|
+
/**
|
|
16
|
+
* Best-effort request that the NEXT `openDatabase` for `sqlitePath` run the
|
|
17
|
+
* full `PRAGMA integrity_check` instead of the default `quick_check`. Creates
|
|
18
|
+
* a new, uniquely-named request file (see the block comment above) with
|
|
19
|
+
* exclusive-create so it can never collide with or overwrite another
|
|
20
|
+
* pending request. Never throws: a failure to write the file only means the
|
|
21
|
+
* escalation is missed, which is logged rather than allowed to mask the
|
|
22
|
+
* caller's real error. Returns whether the file was actually written, so a
|
|
23
|
+
* caller that cannot schedule the follow-up check can say so instead of
|
|
24
|
+
* promising one.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requestFullIntegrityCheck(sqlitePath: string, logger?: Logger): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Lists the full paths of every currently pending full-integrity-check
|
|
29
|
+
* request file for `sqlitePath`. Best-effort: a missing or unreadable
|
|
30
|
+
* directory is treated as "no requests pending" rather than as an error,
|
|
31
|
+
* since the fallback (a quick_check) is always safe to run.
|
|
32
|
+
*/
|
|
33
|
+
export declare function listIntegrityCheckRequestFiles(sqlitePath: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Deletes EXACTLY the given request file paths - the ones an earlier
|
|
36
|
+
* `listIntegrityCheckRequestFiles` call returned, never a fresh listing - so
|
|
37
|
+
* a request file created after that decision point is never deleted by a
|
|
38
|
+
* clear that only ever meant to honor the requests it actually acted on.
|
|
39
|
+
* Best-effort per file: ENOENT (already gone) is ignored, anything else is
|
|
40
|
+
* logged, not thrown.
|
|
41
|
+
*
|
|
42
|
+
* Exported so a unit test can exercise the race guard directly:
|
|
43
|
+
* `openDatabase` lists-then-clears synchronously with no `await` in between,
|
|
44
|
+
* so there is no way to interleave a concurrent request file appearing
|
|
45
|
+
* between "decide" and "clear" from outside that call.
|
|
46
|
+
*/
|
|
47
|
+
export declare function clearIntegrityCheckRequestFiles(requestFiles: string[], logger?: Logger): void;
|
|
48
|
+
/**
|
|
49
|
+
* Wires a Database instance's corruption observer (see sqlite.ts) so that a
|
|
50
|
+
* raw corruption error thrown by ANY later statement on this handle - no
|
|
51
|
+
* matter what catches and wraps it further up the call stack, including a
|
|
52
|
+
* tool-specific error wrapper that never reaches runTool's own catch block -
|
|
53
|
+
* still schedules the next open's full integrity_check. Exported so a unit
|
|
54
|
+
* test can attach the same wiring to a Database instance opened directly
|
|
55
|
+
* (bypassing openDatabase's own open-time check, which would otherwise catch
|
|
56
|
+
* a test's injected corruption before a "successfully opened" handle ever
|
|
57
|
+
* existed to query against).
|
|
58
|
+
*/
|
|
59
|
+
export declare function attachRuntimeCorruptionObserver(db: SqliteDatabase, sqlitePath: string, logger?: Logger): void;
|
|
60
|
+
/**
|
|
61
|
+
* runTool catch-path helper: when `caughtError` is a raw SQLite corruption
|
|
62
|
+
* error observed while serving a tool call (not at open time - open-time
|
|
63
|
+
* corruption is already an AppError by the time it reaches here), this
|
|
64
|
+
* requests a full integrity check on the next open and returns a typed
|
|
65
|
+
* ERR_DB_FAILURE the caller should report instead of the raw error. Any other
|
|
66
|
+
* error is returned unchanged so non-corruption failures are unaffected.
|
|
67
|
+
*
|
|
68
|
+
* The marker request here is redundant with the Database corruption observer
|
|
69
|
+
* (attachRuntimeCorruptionObserver) for an UNWRAPPED raw error, since that
|
|
70
|
+
* observer already fired deeper in the call stack; it is kept because
|
|
71
|
+
* `convertRuntimeSqliteCorruption` is the only place with reliable access to
|
|
72
|
+
* whether scheduling succeeded, which decides which `nextAction` to publish.
|
|
73
|
+
*/
|
|
74
|
+
export declare function convertRuntimeSqliteCorruption(error: unknown, sqlitePath: string, logger?: Logger): unknown;
|
|
14
75
|
export declare function openDatabase(config: DatabaseConfig, logger?: Logger): InitializedDatabase;
|
|
15
|
-
export {};
|